mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-01 09:20:50 +01:00
* Fix energy dashboard staying on yesterday after midnight. * Catch up the energy live day before subscribe fetches. * Keep energy day math in the server timezone so DST cannot skip a live day. Browser-local addDays can jump a calendar day on a 23-hour DST fallback. Assert against tz-internal endOfDay/addDays under Europe/Berlin so UTC CI catches a regression, and prove the 01:00 timer and catch-up refresh fetch the live day rather than only updating collection.start. * Don't follow the live day from midnightRollover alone. A stored non-today preset would otherwise be discarded on the first subscribe. Drop tautological UTC DST tests that cannot fail.
1490 lines
38 KiB
TypeScript
1490 lines
38 KiB
TypeScript
import { addDays, endOfDay, startOfDay } from "date-fns";
|
|
import type { HassConfig } from "home-assistant-js-websocket";
|
|
import { afterEach, assert, describe, it, vi } from "vitest";
|
|
|
|
import { calcDate } from "../../src/common/datetime/calc_date";
|
|
import {
|
|
type FrontendLocaleData,
|
|
NumberFormat,
|
|
TimeFormat,
|
|
FirstWeekday,
|
|
DateFormat,
|
|
TimeZone,
|
|
} from "../../src/data/translation";
|
|
import {
|
|
computeConsumptionSingle,
|
|
computeEnergyLabel,
|
|
computeEnergyDeviceLabels,
|
|
formatConsumptionShort,
|
|
calculateSolarConsumedGauge,
|
|
formatPowerShort,
|
|
getNextEnergyPeriodStart,
|
|
getEnergyDefaultPeriodStorageKey,
|
|
getEnergyFirstStatisticAt,
|
|
getEnergyLiveDayPeriod,
|
|
shouldFallbackEnergyPeriodToYesterday,
|
|
getEnergyDataCollection,
|
|
EMPTY_PREFERENCES,
|
|
} from "../../src/data/energy";
|
|
import type { DeviceRegistryEntry } from "../../src/data/device/device_registry";
|
|
import type { EntityRegistryDisplayEntry } from "../../src/data/entity/entity_registry";
|
|
import type { StatisticsMetaData } from "../../src/data/recorder";
|
|
import type { HomeAssistant } from "../../src/types";
|
|
import { createMockEntityState, createMockHass } from "../fixtures/hass";
|
|
|
|
const checkConsumptionResult = (
|
|
input: {
|
|
from_grid: number | undefined;
|
|
to_grid: number | undefined;
|
|
solar: number | undefined;
|
|
to_battery: number | undefined;
|
|
from_battery: number | undefined;
|
|
},
|
|
exact = true
|
|
): {
|
|
grid_to_battery: number;
|
|
battery_to_grid: number;
|
|
solar_to_battery: number;
|
|
solar_to_grid: number;
|
|
used_solar: number;
|
|
used_grid: number;
|
|
used_battery: number;
|
|
used_total: number;
|
|
} => {
|
|
const result = computeConsumptionSingle(input);
|
|
if (exact) {
|
|
assert.equal(
|
|
result.used_total,
|
|
result.used_solar + result.used_battery + result.used_grid
|
|
);
|
|
assert.equal(
|
|
input.to_grid || 0,
|
|
result.solar_to_grid + result.battery_to_grid
|
|
);
|
|
assert.equal(
|
|
input.to_battery || 0,
|
|
result.grid_to_battery + result.solar_to_battery
|
|
);
|
|
assert.equal(
|
|
input.solar || 0,
|
|
result.solar_to_battery + result.solar_to_grid + result.used_solar
|
|
);
|
|
}
|
|
return result;
|
|
};
|
|
|
|
describe("Energy Short Format Test", () => {
|
|
// Create default to not have to specify a not relevant TimeFormat over and over again.
|
|
const defaultLocale: FrontendLocaleData = {
|
|
language: "en",
|
|
number_format: NumberFormat.language,
|
|
time_format: TimeFormat.language,
|
|
date_format: DateFormat.language,
|
|
time_zone: TimeZone.local,
|
|
first_weekday: FirstWeekday.language,
|
|
};
|
|
|
|
const hass = { locale: defaultLocale } as HomeAssistant;
|
|
it("No Unit conversion", () => {
|
|
assert.strictEqual(formatConsumptionShort(hass, 0, "Wh"), "0 Wh");
|
|
assert.strictEqual(formatConsumptionShort(hass, 0, "kWh"), "0 Wh");
|
|
assert.strictEqual(formatConsumptionShort(hass, 0, "kWh", "kWh"), "0 kWh");
|
|
assert.strictEqual(formatConsumptionShort(hass, 0, "GWh"), "0 Wh");
|
|
assert.strictEqual(formatConsumptionShort(hass, 0, "GWh", "GWh"), "0 GWh");
|
|
assert.strictEqual(formatConsumptionShort(hass, 0, "gal"), "0 gal");
|
|
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 10000.12345, "gal"),
|
|
"10,000 gal"
|
|
);
|
|
|
|
assert.strictEqual(formatConsumptionShort(hass, 1.2345, "kWh"), "1.23 kWh");
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 10.12345, "kWh"),
|
|
"10.1 kWh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 500.12345, "kWh"),
|
|
"500 kWh"
|
|
);
|
|
|
|
assert.strictEqual(formatConsumptionShort(hass, 10.01, "kWh"), "10 kWh");
|
|
});
|
|
it("Upward Unit conversion", () => {
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 1512.34567, "kWh"),
|
|
"1.51 MWh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 15123.4567, "kWh"),
|
|
"15.1 MWh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 151234.5678, "kWh"),
|
|
"151 MWh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 1512345.6789, "kWh"),
|
|
"1.51 GWh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 15123456789.9, "kWh"),
|
|
"15.1 TWh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 15123456789000.9, "kWh"),
|
|
"15,123 TWh"
|
|
);
|
|
});
|
|
it("Downward Unit conversion", () => {
|
|
assert.strictEqual(formatConsumptionShort(hass, 0.00012, "kWh"), "0.12 Wh");
|
|
assert.strictEqual(formatConsumptionShort(hass, 0.12345, "kWh"), "123 Wh");
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 0.00001234, "TWh"),
|
|
"12.3 MWh"
|
|
);
|
|
});
|
|
it("Negativ Consumption", () => {
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, -500.123, "kWh"),
|
|
"-500 kWh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, -1234.56, "kWh"),
|
|
"-1.23 MWh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, -0.001234, "kWh"),
|
|
"-1.23 Wh"
|
|
);
|
|
});
|
|
it("Conversion with target unit", () => {
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 0.00012, "kWh", "Wh"),
|
|
"0.12 Wh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 0.00012, "kWh", "kWh"),
|
|
"0 kWh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 0.01012, "kWh", "kWh"),
|
|
"0.01 kWh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 0.00012, "kWh", "MWh"),
|
|
"0 MWh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 10.12345, "kWh", "kWh"),
|
|
"10.1 kWh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 10.12345, "kWh", "ZZZZZWh"),
|
|
"10.1 kWh"
|
|
);
|
|
assert.strictEqual(
|
|
formatConsumptionShort(hass, 151234.5678, "kWh", "MWh"),
|
|
"151 MWh"
|
|
);
|
|
});
|
|
it("Power Short Format", () => {
|
|
assert.strictEqual(formatPowerShort(hass, 0), "0 W");
|
|
assert.strictEqual(formatPowerShort(hass, 10), "10 W");
|
|
assert.strictEqual(formatPowerShort(hass, 12.2), "12 W");
|
|
assert.strictEqual(formatPowerShort(hass, 999), "999 W");
|
|
assert.strictEqual(formatPowerShort(hass, 1000), "1 kW");
|
|
assert.strictEqual(formatPowerShort(hass, 1234), "1.234 kW");
|
|
assert.strictEqual(formatPowerShort(hass, 10_500), "10.5 kW");
|
|
assert.strictEqual(formatPowerShort(hass, 1_500_000), "1.5 MW");
|
|
assert.strictEqual(formatPowerShort(hass, -1500), "-1.5 kW");
|
|
});
|
|
});
|
|
|
|
describe("Energy Usage Calculation Tests", () => {
|
|
it("Consuming Energy From the Grid", () => {
|
|
[0, 5, 1000].forEach((x) => {
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: x,
|
|
to_grid: undefined,
|
|
solar: undefined,
|
|
to_battery: undefined,
|
|
from_battery: undefined,
|
|
}),
|
|
{
|
|
grid_to_battery: 0,
|
|
battery_to_grid: 0,
|
|
used_solar: 0,
|
|
used_grid: x,
|
|
used_battery: 0,
|
|
used_total: x,
|
|
solar_to_battery: 0,
|
|
solar_to_grid: 0,
|
|
}
|
|
);
|
|
});
|
|
});
|
|
it("Solar production, consuming some and returning the remainder to grid.", () => {
|
|
(
|
|
[
|
|
[2.99, false], // unsolveable : solar < to_grid
|
|
[3, true],
|
|
[10, true],
|
|
[100, true],
|
|
] as any
|
|
).forEach(([s, exact]) => {
|
|
assert.deepEqual(
|
|
checkConsumptionResult(
|
|
{
|
|
from_grid: 0,
|
|
to_grid: 3,
|
|
solar: s,
|
|
to_battery: undefined,
|
|
from_battery: undefined,
|
|
},
|
|
exact
|
|
),
|
|
{
|
|
grid_to_battery: 0,
|
|
battery_to_grid: 0,
|
|
used_solar: Math.min(s, Math.max(0, s - 3)),
|
|
used_grid: 0,
|
|
used_battery: 0,
|
|
used_total: s - 3,
|
|
solar_to_battery: 0,
|
|
solar_to_grid: Math.min(3, s),
|
|
}
|
|
);
|
|
});
|
|
});
|
|
it("Solar production with simultaneous grid consumption. Excess solar returned to the grid.", () => {
|
|
(
|
|
[
|
|
[0, 0, true],
|
|
[3, 0, true],
|
|
[0, 3, true],
|
|
[5, 4, true],
|
|
[4, 5, true],
|
|
[10, 3, true],
|
|
[3, 7, true],
|
|
[3, 7.1, false], // unsolveable: to_grid > solar
|
|
] as any
|
|
).forEach(([from_grid, to_grid, exact]) => {
|
|
assert.deepEqual(
|
|
checkConsumptionResult(
|
|
{
|
|
from_grid,
|
|
to_grid,
|
|
solar: 7,
|
|
to_battery: undefined,
|
|
from_battery: undefined,
|
|
},
|
|
exact
|
|
),
|
|
{
|
|
grid_to_battery: 0,
|
|
battery_to_grid: 0,
|
|
used_solar: Math.max(0, 7 - to_grid),
|
|
used_grid: from_grid - Math.max(0, to_grid - 7),
|
|
used_total: from_grid - to_grid + 7,
|
|
used_battery: 0,
|
|
solar_to_battery: 0,
|
|
solar_to_grid: Math.min(7, to_grid),
|
|
}
|
|
);
|
|
});
|
|
});
|
|
it("Charging the battery from the grid", () => {
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 5,
|
|
to_grid: 0,
|
|
solar: 0,
|
|
to_battery: 3,
|
|
from_battery: 0,
|
|
}),
|
|
{
|
|
grid_to_battery: 3,
|
|
battery_to_grid: 0,
|
|
used_solar: 0,
|
|
used_grid: 2,
|
|
used_battery: 0,
|
|
used_total: 2,
|
|
solar_to_battery: 0,
|
|
solar_to_grid: 0,
|
|
}
|
|
);
|
|
});
|
|
it("Consuming from the grid and battery simultaneously", () => {
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 5,
|
|
to_grid: 0,
|
|
solar: 0,
|
|
to_battery: 0,
|
|
from_battery: 5,
|
|
}),
|
|
{
|
|
grid_to_battery: 0,
|
|
battery_to_grid: 0,
|
|
used_solar: 0,
|
|
used_grid: 5,
|
|
used_battery: 5,
|
|
used_total: 10,
|
|
solar_to_battery: 0,
|
|
solar_to_grid: 0,
|
|
}
|
|
);
|
|
});
|
|
it("Consuming some battery and returning some battery to the grid", () => {
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 0,
|
|
to_grid: 4,
|
|
solar: 0,
|
|
to_battery: 0,
|
|
from_battery: 5,
|
|
}),
|
|
{
|
|
grid_to_battery: 0,
|
|
battery_to_grid: 4,
|
|
used_solar: 0,
|
|
used_grid: 0,
|
|
used_battery: 1,
|
|
used_total: 1,
|
|
solar_to_battery: 0,
|
|
solar_to_grid: 0,
|
|
}
|
|
);
|
|
});
|
|
it("Charging and discharging the battery to/from the grid in the same interval.", () => {
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 5,
|
|
to_grid: 1,
|
|
solar: 0,
|
|
to_battery: 3,
|
|
from_battery: 1,
|
|
}),
|
|
{
|
|
grid_to_battery: 3,
|
|
battery_to_grid: 1,
|
|
used_solar: 0,
|
|
used_grid: 2,
|
|
used_battery: 0,
|
|
used_total: 2,
|
|
solar_to_battery: 0,
|
|
solar_to_grid: 0,
|
|
}
|
|
);
|
|
});
|
|
|
|
it("Charging the battery with no solar sensor.", () => {
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 5,
|
|
to_grid: 0,
|
|
solar: undefined,
|
|
to_battery: 3,
|
|
from_battery: 0,
|
|
}),
|
|
{
|
|
grid_to_battery: 3,
|
|
battery_to_grid: 0,
|
|
used_solar: 0,
|
|
used_grid: 2,
|
|
used_battery: 0,
|
|
used_total: 2,
|
|
solar_to_battery: 0,
|
|
solar_to_grid: 0,
|
|
}
|
|
);
|
|
});
|
|
it("Discharging battery to grid while also consuming from grid.", () => {
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 5,
|
|
to_grid: 4,
|
|
solar: 0,
|
|
to_battery: 0,
|
|
from_battery: 4,
|
|
}),
|
|
{
|
|
grid_to_battery: 0,
|
|
battery_to_grid: 4,
|
|
used_solar: 0,
|
|
used_grid: 5,
|
|
used_battery: 0,
|
|
used_total: 5,
|
|
solar_to_grid: 0,
|
|
solar_to_battery: 0,
|
|
}
|
|
);
|
|
});
|
|
|
|
it("Grid, solar, and battery", () => {
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 5,
|
|
to_grid: 3,
|
|
solar: 7,
|
|
to_battery: 3,
|
|
from_battery: 0,
|
|
}),
|
|
{
|
|
grid_to_battery: 0,
|
|
battery_to_grid: 0,
|
|
used_solar: 1,
|
|
used_grid: 5,
|
|
used_battery: 0,
|
|
used_total: 6,
|
|
solar_to_battery: 3,
|
|
solar_to_grid: 3,
|
|
}
|
|
);
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 5,
|
|
to_grid: 3,
|
|
solar: 7,
|
|
to_battery: 3,
|
|
from_battery: 10,
|
|
}),
|
|
{
|
|
grid_to_battery: 0,
|
|
battery_to_grid: 0,
|
|
used_solar: 1,
|
|
used_grid: 5,
|
|
used_battery: 10,
|
|
used_total: 16,
|
|
solar_to_battery: 3,
|
|
solar_to_grid: 3,
|
|
}
|
|
);
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 2,
|
|
to_grid: 7,
|
|
solar: 7,
|
|
to_battery: 1,
|
|
from_battery: 1,
|
|
}),
|
|
{
|
|
grid_to_battery: 0,
|
|
battery_to_grid: 1,
|
|
used_solar: 0,
|
|
used_grid: 2,
|
|
used_battery: 0,
|
|
used_total: 2,
|
|
solar_to_battery: 1,
|
|
solar_to_grid: 6,
|
|
}
|
|
);
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 2,
|
|
to_grid: 7,
|
|
solar: 9,
|
|
to_battery: 1,
|
|
from_battery: 1,
|
|
}),
|
|
{
|
|
grid_to_battery: 0,
|
|
battery_to_grid: 0,
|
|
used_solar: 1,
|
|
used_grid: 2,
|
|
used_battery: 1,
|
|
used_total: 4,
|
|
solar_to_battery: 1,
|
|
solar_to_grid: 7,
|
|
}
|
|
);
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 5,
|
|
to_grid: 3,
|
|
solar: 1,
|
|
to_battery: 0,
|
|
from_battery: 2,
|
|
}),
|
|
{
|
|
grid_to_battery: 0,
|
|
battery_to_grid: 2,
|
|
used_solar: 0,
|
|
used_grid: 5,
|
|
used_battery: 0,
|
|
used_total: 5,
|
|
solar_to_battery: 0,
|
|
solar_to_grid: 1,
|
|
}
|
|
);
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 6,
|
|
to_grid: 0,
|
|
solar: 3,
|
|
to_battery: 6,
|
|
from_battery: 6,
|
|
}),
|
|
{
|
|
grid_to_battery: 3,
|
|
battery_to_grid: 0,
|
|
used_solar: 0,
|
|
used_grid: 3,
|
|
used_battery: 6,
|
|
solar_to_battery: 3,
|
|
solar_to_grid: 0,
|
|
used_total: 9,
|
|
}
|
|
);
|
|
});
|
|
it("Solar -> Battery -> Grid", () => {
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 0,
|
|
to_grid: 1,
|
|
solar: 1,
|
|
to_battery: 1,
|
|
from_battery: 1,
|
|
}),
|
|
{
|
|
grid_to_battery: 0,
|
|
battery_to_grid: 1,
|
|
used_solar: 0,
|
|
used_grid: 0,
|
|
used_battery: 0,
|
|
solar_to_battery: 1,
|
|
solar_to_grid: 0,
|
|
used_total: 0,
|
|
}
|
|
);
|
|
});
|
|
it("Solar -> Grid && Grid -> Battery", () => {
|
|
assert.deepEqual(
|
|
checkConsumptionResult({
|
|
from_grid: 1,
|
|
to_grid: 1,
|
|
solar: 1,
|
|
to_battery: 1,
|
|
from_battery: 0,
|
|
}),
|
|
{
|
|
grid_to_battery: 1,
|
|
battery_to_grid: 0,
|
|
used_solar: 0,
|
|
used_grid: 0,
|
|
used_battery: 0,
|
|
solar_to_battery: 0,
|
|
solar_to_grid: 1,
|
|
used_total: 0,
|
|
}
|
|
);
|
|
});
|
|
|
|
it("bug #25387", () => {
|
|
assert.deepEqual(
|
|
checkConsumptionResult(
|
|
{
|
|
from_grid: 0.059,
|
|
to_grid: 48.0259,
|
|
solar: 61.22,
|
|
to_battery: 5.716,
|
|
from_battery: 4.83,
|
|
},
|
|
false
|
|
),
|
|
{
|
|
grid_to_battery: 0,
|
|
battery_to_grid: 0,
|
|
used_solar: 7.478099999999998,
|
|
used_grid: 0.05899999999999572,
|
|
used_battery: 4.83,
|
|
solar_to_battery: 5.716,
|
|
solar_to_grid: 48.0259,
|
|
used_total: 12.367099999999994,
|
|
}
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("Self-consumed solar gauge tests", () => {
|
|
it("no battery", () => {
|
|
const hasBattery = false;
|
|
assert.deepEqual(
|
|
calculateSolarConsumedGauge(hasBattery, {
|
|
total: {},
|
|
timestamps: [0],
|
|
}),
|
|
undefined
|
|
);
|
|
assert.deepEqual(
|
|
calculateSolarConsumedGauge(hasBattery, {
|
|
solar: {
|
|
"0": 0,
|
|
},
|
|
total: {
|
|
solar: 0,
|
|
},
|
|
timestamps: [0],
|
|
}),
|
|
undefined
|
|
);
|
|
assert.deepEqual(
|
|
calculateSolarConsumedGauge(hasBattery, {
|
|
solar: {
|
|
"0": 1,
|
|
"1": 3,
|
|
},
|
|
total: {
|
|
solar: 4,
|
|
},
|
|
timestamps: [0, 1],
|
|
}),
|
|
100
|
|
);
|
|
assert.deepEqual(
|
|
calculateSolarConsumedGauge(hasBattery, {
|
|
solar: {
|
|
"0": 1,
|
|
"1": 3,
|
|
},
|
|
to_grid: {
|
|
"1": 1,
|
|
},
|
|
total: {
|
|
solar: 4,
|
|
to_grid: 1,
|
|
},
|
|
timestamps: [0, 1],
|
|
}),
|
|
75
|
|
);
|
|
assert.deepEqual(
|
|
calculateSolarConsumedGauge(hasBattery, {
|
|
solar: {
|
|
"0": 1,
|
|
"1": 3,
|
|
},
|
|
to_grid: {
|
|
"0": 1,
|
|
"1": 3,
|
|
},
|
|
total: {
|
|
solar: 4,
|
|
to_grid: 4,
|
|
},
|
|
timestamps: [0, 1],
|
|
}),
|
|
0
|
|
);
|
|
});
|
|
it("with battery", () => {
|
|
const hasBattery = true;
|
|
assert.deepEqual(
|
|
calculateSolarConsumedGauge(hasBattery, {
|
|
total: {},
|
|
timestamps: [0],
|
|
}),
|
|
undefined
|
|
);
|
|
assert.deepEqual(
|
|
calculateSolarConsumedGauge(hasBattery, {
|
|
solar: {
|
|
"0": 0,
|
|
},
|
|
total: {
|
|
solar: 0,
|
|
},
|
|
timestamps: [0],
|
|
}),
|
|
undefined
|
|
);
|
|
assert.deepEqual(
|
|
calculateSolarConsumedGauge(hasBattery, {
|
|
solar: {
|
|
"0": 1,
|
|
"1": 3,
|
|
},
|
|
total: {
|
|
solar: 4,
|
|
},
|
|
timestamps: [0, 1],
|
|
}),
|
|
100
|
|
);
|
|
assert.deepEqual(
|
|
calculateSolarConsumedGauge(hasBattery, {
|
|
solar: {
|
|
"0": 1,
|
|
"1": 3,
|
|
},
|
|
to_grid: {
|
|
"1": 1,
|
|
},
|
|
total: {
|
|
solar: 4,
|
|
},
|
|
timestamps: [0, 1],
|
|
}),
|
|
75
|
|
);
|
|
assert.deepEqual(
|
|
calculateSolarConsumedGauge(hasBattery, {
|
|
solar: {
|
|
"10": 1,
|
|
},
|
|
to_grid: {
|
|
"0": 1,
|
|
"1": 1,
|
|
"2": 1,
|
|
"3": 1,
|
|
},
|
|
from_battery: {
|
|
"0": 1,
|
|
"1": 1,
|
|
"2": 1,
|
|
"3": 1,
|
|
},
|
|
total: {
|
|
solar: 1,
|
|
},
|
|
timestamps: [0, 1, 2, 3, 10],
|
|
}),
|
|
// As the battery is discharged from unknown source, it does not affect solar production number.
|
|
100
|
|
);
|
|
assert.deepEqual(
|
|
calculateSolarConsumedGauge(hasBattery, {
|
|
solar: {
|
|
"0": 10,
|
|
},
|
|
to_battery: {
|
|
"0": 10,
|
|
},
|
|
to_grid: {
|
|
"1": 3,
|
|
"3": 1,
|
|
},
|
|
from_battery: {
|
|
"1": 3,
|
|
"2": 2,
|
|
"3": 2,
|
|
"4": 3,
|
|
"5": 100, // Unknown source, not counted
|
|
},
|
|
total: {
|
|
solar: 10,
|
|
},
|
|
timestamps: [0, 1, 2, 3, 4, 5],
|
|
}),
|
|
// As the battery is discharged from unknown source, it does not affect solar production number.
|
|
60
|
|
);
|
|
});
|
|
it("complex battery/solar/grid", () => {
|
|
const hasBattery = true;
|
|
|
|
const value = calculateSolarConsumedGauge(hasBattery, {
|
|
solar: {
|
|
"1": 6,
|
|
"2": 0,
|
|
"3": 7,
|
|
},
|
|
to_battery: {
|
|
"1": 5,
|
|
"2": 5,
|
|
"3": 7,
|
|
},
|
|
to_grid: {
|
|
"0": 5,
|
|
"10": 1,
|
|
"11": 1,
|
|
"12": 5,
|
|
"13": 3,
|
|
},
|
|
from_grid: {
|
|
"2": 5,
|
|
},
|
|
from_battery: {
|
|
"0": 5,
|
|
"10": 3,
|
|
"11": 4,
|
|
"12": 5,
|
|
"13": 5,
|
|
},
|
|
total: {
|
|
// Total is mostly don't care when hasBattery, only hourly values are used
|
|
solar: 13,
|
|
},
|
|
timestamps: [0, 1, 2, 3, 10, 11, 12, 13],
|
|
});
|
|
// "1" - consumed 1 solar, 5 sent to battery
|
|
// "10" - consumed 2/3 of solar energy stored in battery
|
|
// "11" - consumed 3/4 of solar energy stored in battery
|
|
// "12" - skipped as this is energy from grid, not counted
|
|
// "13" - consumed 2/5 of solar energy stored in battery
|
|
const expectedNumerator = 1 + 2 + 3 + 0 + 2; // 8
|
|
const expectedDenominator = 1 + 3 + 4 + 0 + 5; // 13
|
|
assert.equal(
|
|
Math.round(value!),
|
|
Math.round((expectedNumerator / expectedDenominator) * 100)
|
|
);
|
|
});
|
|
|
|
it("complex battery/solar/grid #2", () => {
|
|
const hasBattery = true;
|
|
const value = calculateSolarConsumedGauge(hasBattery, {
|
|
solar: {
|
|
"0": 100,
|
|
"2": 100,
|
|
},
|
|
to_battery: {
|
|
"0": 100,
|
|
"1": 100,
|
|
"2": 100,
|
|
},
|
|
to_grid: {
|
|
"10": 50,
|
|
},
|
|
from_grid: {
|
|
"1": 100,
|
|
},
|
|
from_battery: {
|
|
"10": 300,
|
|
},
|
|
total: {
|
|
solar: 200,
|
|
to_battery: 300,
|
|
to_grid: 50,
|
|
from_grid: 100,
|
|
from_battery: 300,
|
|
},
|
|
timestamps: [0, 1, 2, 10],
|
|
});
|
|
const expectedNumerator = 200 - 50;
|
|
const expectedDenominator = 200; // ignoring 100 from grid
|
|
assert.equal(
|
|
Math.round(value!),
|
|
Math.round((expectedNumerator / expectedDenominator) * 100)
|
|
);
|
|
});
|
|
});
|
|
|
|
// Pin the time zone (via TimeZone.server) so energy period tests do not
|
|
// depend on the machine's local zone.
|
|
const energyPeriodLocale: FrontendLocaleData = {
|
|
language: "en",
|
|
number_format: NumberFormat.language,
|
|
time_format: TimeFormat.language,
|
|
date_format: DateFormat.language,
|
|
time_zone: TimeZone.server,
|
|
first_weekday: FirstWeekday.language,
|
|
};
|
|
const energyPeriodConfig = { time_zone: "America/New_York" } as HassConfig;
|
|
const energyPeriodDay = (now: Date, offset = 0) => {
|
|
const day = calcDate(
|
|
now,
|
|
addDays,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig,
|
|
offset
|
|
);
|
|
return {
|
|
start: calcDate(day, startOfDay, energyPeriodLocale, energyPeriodConfig),
|
|
end: calcDate(day, endOfDay, energyPeriodLocale, energyPeriodConfig),
|
|
};
|
|
};
|
|
|
|
describe("getNextEnergyPeriodStart", () => {
|
|
const isMidnight = (date: Date) =>
|
|
calcDate(
|
|
date,
|
|
startOfDay,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig
|
|
).getTime() === date.getTime();
|
|
|
|
it("rolls the real-time view over at midnight, statistics an hour later", () => {
|
|
const now = new Date("2026-06-19T15:30:00-04:00");
|
|
|
|
const realTime = getNextEnergyPeriodStart(
|
|
true,
|
|
now,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig
|
|
);
|
|
const statistics = getNextEnergyPeriodStart(
|
|
false,
|
|
now,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig
|
|
);
|
|
|
|
// Real-time rolls over exactly at the next midnight.
|
|
assert.isTrue(isMidnight(realTime));
|
|
assert.equal(
|
|
realTime.getTime(),
|
|
new Date("2026-06-20T00:00:00-04:00").getTime()
|
|
);
|
|
|
|
// Statistics roll over an hour after midnight, on the same day boundary.
|
|
assert.equal(statistics.getTime() - realTime.getTime(), 60 * 60 * 1000);
|
|
assert.equal(
|
|
calcDate(
|
|
statistics,
|
|
startOfDay,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig
|
|
).getTime(),
|
|
realTime.getTime()
|
|
);
|
|
});
|
|
|
|
it("advances the real-time view to the next midnight when called after midnight", () => {
|
|
const now = new Date("2026-06-20T00:30:00-04:00");
|
|
|
|
const realTime = getNextEnergyPeriodStart(
|
|
true,
|
|
now,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig
|
|
);
|
|
|
|
assert.isTrue(isMidnight(realTime));
|
|
// Next midnight is June 21, not the already-passed June 20 midnight.
|
|
assert.equal(
|
|
realTime.getTime(),
|
|
new Date("2026-06-21T00:00:00-04:00").getTime()
|
|
);
|
|
});
|
|
|
|
it("wakes a non-today live day at today 01:00 during hour 0", () => {
|
|
const now = new Date("2026-06-20T00:30:00-04:00");
|
|
const todayOne = new Date("2026-06-20T01:00:00-04:00").getTime();
|
|
|
|
for (const offset of [-1, -2]) {
|
|
assert.equal(
|
|
getNextEnergyPeriodStart(
|
|
false,
|
|
now,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig,
|
|
energyPeriodDay(now, offset).start
|
|
).getTime(),
|
|
todayOne
|
|
);
|
|
}
|
|
});
|
|
|
|
it("keeps tomorrow 01:00 when statistics is already on today during hour 0", () => {
|
|
const now = new Date("2026-06-20T00:30:00-04:00");
|
|
|
|
assert.equal(
|
|
getNextEnergyPeriodStart(
|
|
false,
|
|
now,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig,
|
|
energyPeriodDay(now).start
|
|
).getTime(),
|
|
new Date("2026-06-21T01:00:00-04:00").getTime()
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("shouldFallbackEnergyPeriodToYesterday", () => {
|
|
it("is true for the statistics view before 01:00", () => {
|
|
const now = new Date("2026-06-20T00:30:00-04:00");
|
|
assert.isTrue(
|
|
shouldFallbackEnergyPeriodToYesterday(
|
|
false,
|
|
now,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig
|
|
)
|
|
);
|
|
assert.equal(
|
|
getEnergyFirstStatisticAt(
|
|
now,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig
|
|
).getTime(),
|
|
new Date("2026-06-20T01:00:00-04:00").getTime()
|
|
);
|
|
});
|
|
|
|
it("is false at 01:00 and for the real-time view", () => {
|
|
const atOne = new Date("2026-06-20T01:00:00-04:00");
|
|
const beforeOne = new Date("2026-06-20T00:30:00-04:00");
|
|
assert.isFalse(
|
|
shouldFallbackEnergyPeriodToYesterday(
|
|
false,
|
|
atOne,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig
|
|
)
|
|
);
|
|
assert.isFalse(
|
|
shouldFallbackEnergyPeriodToYesterday(
|
|
true,
|
|
beforeOne,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig
|
|
)
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("getEnergyLiveDayPeriod", () => {
|
|
it("keeps yesterday during hour 0 when that is the current period", () => {
|
|
const now = new Date("2026-06-20T00:30:00-04:00");
|
|
const { start, end } = energyPeriodDay(now, -1);
|
|
const live = getEnergyLiveDayPeriod(
|
|
false,
|
|
now,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig,
|
|
start
|
|
);
|
|
assert.equal(live.start.getTime(), start.getTime());
|
|
assert.equal(live.end.getTime(), end.getTime());
|
|
});
|
|
|
|
it("keeps today during hour 0 when the user already picked today", () => {
|
|
const now = new Date("2026-06-20T00:30:00-04:00");
|
|
const { start, end } = energyPeriodDay(now);
|
|
const live = getEnergyLiveDayPeriod(
|
|
false,
|
|
now,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig,
|
|
start
|
|
);
|
|
assert.equal(live.start.getTime(), start.getTime());
|
|
assert.equal(live.end.getTime(), end.getTime());
|
|
});
|
|
|
|
it("advances a stale yesterday to today after 01:00", () => {
|
|
const now = new Date("2026-06-20T10:00:00-04:00");
|
|
const live = getEnergyLiveDayPeriod(
|
|
false,
|
|
now,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig,
|
|
energyPeriodDay(now, -1).start
|
|
);
|
|
const expected = energyPeriodDay(now);
|
|
assert.equal(live.start.getTime(), expected.start.getTime());
|
|
assert.equal(live.end.getTime(), expected.end.getTime());
|
|
});
|
|
|
|
it("advances a two-day-old live day to today", () => {
|
|
const now = new Date("2026-06-20T10:00:00-04:00");
|
|
const live = getEnergyLiveDayPeriod(
|
|
false,
|
|
now,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig,
|
|
energyPeriodDay(now, -2).start
|
|
);
|
|
const expected = energyPeriodDay(now);
|
|
assert.equal(live.start.getTime(), expected.start.getTime());
|
|
assert.equal(live.end.getTime(), expected.end.getTime());
|
|
});
|
|
|
|
it("falls back to yesterday for a stale live day during hour 0", () => {
|
|
const now = new Date("2026-06-20T00:30:00-04:00");
|
|
const live = getEnergyLiveDayPeriod(
|
|
false,
|
|
now,
|
|
energyPeriodLocale,
|
|
energyPeriodConfig,
|
|
energyPeriodDay(now, -2).start
|
|
);
|
|
const expected = energyPeriodDay(now, -1);
|
|
assert.equal(live.start.getTime(), expected.start.getTime());
|
|
assert.equal(live.end.getTime(), expected.end.getTime());
|
|
});
|
|
});
|
|
|
|
describe("getEnergyDataCollection live day", () => {
|
|
afterEach(() => {
|
|
localStorage.clear();
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
const createCollection = (
|
|
key: string,
|
|
preset?: string,
|
|
midnightRollover = false
|
|
) => {
|
|
const hass = createMockHass();
|
|
hass.locale = energyPeriodLocale;
|
|
hass.config = { ...hass.config, time_zone: "America/New_York" };
|
|
const callWS = vi.fn(async (msg: { type: string }) => {
|
|
if (msg.type === "energy/info") {
|
|
return { cost_sensors: {}, solar_forecast_domains: [] };
|
|
}
|
|
throw new Error(`unexpected ${msg.type}`);
|
|
});
|
|
Object.assign(hass, {
|
|
connection: {
|
|
addEventListener: vi.fn(),
|
|
removeEventListener: vi.fn(),
|
|
connected: true,
|
|
},
|
|
callWS,
|
|
});
|
|
if (preset) {
|
|
localStorage.setItem(getEnergyDefaultPeriodStorageKey(hass, key), preset);
|
|
}
|
|
return {
|
|
collection: getEnergyDataCollection(hass, {
|
|
key,
|
|
prefs: EMPTY_PREFERENCES,
|
|
midnightRollover,
|
|
}),
|
|
callWS,
|
|
};
|
|
};
|
|
|
|
const energyInfoFetches = (callWS: ReturnType<typeof vi.fn>) =>
|
|
callWS.mock.calls.filter((call) => call[0].type === "energy/info");
|
|
|
|
it("advances hour-0 yesterday to today at 01:00 and fetches the new day", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-06-20T00:30:00-04:00"));
|
|
|
|
const { collection, callWS } = createCollection("energy_timer");
|
|
const refresh = vi.spyOn(collection, "refresh");
|
|
const unsub = collection.subscribe(() => undefined);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
refresh.mockClear();
|
|
callWS.mockClear();
|
|
|
|
assert.equal(
|
|
collection.start.getTime(),
|
|
energyPeriodDay(new Date(), -1).start.getTime()
|
|
);
|
|
|
|
await vi.advanceTimersByTimeAsync(30 * 60 * 1000);
|
|
|
|
assert.equal(
|
|
collection.start.getTime(),
|
|
energyPeriodDay(new Date()).start.getTime()
|
|
);
|
|
// Cards render EnergyData from the websocket store, not collection.start.
|
|
// The 01:00 callback must refresh() so getEnergyData runs for today.
|
|
assert.equal(refresh.mock.calls.length, 1);
|
|
assert.equal(energyInfoFetches(callWS).length, 1);
|
|
|
|
unsub();
|
|
});
|
|
|
|
it("catches up a stale live day on resubscribe", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-06-18T15:00:00-04:00"));
|
|
|
|
const { collection, callWS } = createCollection("energy_catchup");
|
|
const refresh = vi.spyOn(collection, "refresh");
|
|
let unsub = collection.subscribe(() => undefined);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
refresh.mockClear();
|
|
callWS.mockClear();
|
|
|
|
unsub();
|
|
vi.setSystemTime(new Date("2026-06-20T10:00:00-04:00"));
|
|
unsub = collection.subscribe(() => undefined);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
|
|
assert.equal(
|
|
collection.start.getTime(),
|
|
energyPeriodDay(new Date()).start.getTime()
|
|
);
|
|
assert.equal(refresh.mock.calls.length, 1);
|
|
assert.equal(energyInfoFetches(callWS).length, 1);
|
|
|
|
unsub();
|
|
});
|
|
|
|
it("does not double-refresh on a cold subscribe after the unsub grace", async () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-06-18T15:00:00-04:00"));
|
|
|
|
const { collection, callWS } = createCollection("energy_cold_refresh");
|
|
const refresh = vi.spyOn(collection, "refresh");
|
|
let unsub = collection.subscribe(() => undefined);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
unsub();
|
|
await vi.advanceTimersByTimeAsync(5000);
|
|
refresh.mockClear();
|
|
callWS.mockClear();
|
|
|
|
vi.setSystemTime(new Date("2026-06-20T10:00:00-04:00"));
|
|
unsub = collection.subscribe(() => undefined);
|
|
await vi.advanceTimersByTimeAsync(0);
|
|
|
|
assert.equal(
|
|
collection.start.getTime(),
|
|
energyPeriodDay(new Date()).start.getTime()
|
|
);
|
|
// The library's first fetch is not collection.refresh(); this spy only
|
|
// sees the extra refresh used during the unsub-grace re-subscribe.
|
|
assert.equal(refresh.mock.calls.length, 0);
|
|
assert.equal(energyInfoFetches(callWS).length, 1);
|
|
|
|
unsub();
|
|
});
|
|
|
|
it("keeps a custom setPeriod range overnight", () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-06-18T15:00:00-04:00"));
|
|
|
|
const { collection } = createCollection("energy_custom_period");
|
|
let unsub = collection.subscribe(() => undefined);
|
|
const custom = energyPeriodDay(new Date(), -5);
|
|
collection.setPeriod(custom.start, custom.end);
|
|
unsub();
|
|
|
|
vi.setSystemTime(new Date("2026-06-20T10:00:00-04:00"));
|
|
unsub = collection.subscribe(() => undefined);
|
|
|
|
assert.equal(collection.start.getTime(), custom.start.getTime());
|
|
|
|
unsub();
|
|
});
|
|
|
|
it("does not advance the period after the last subscriber leaves", () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-06-20T00:30:00-04:00"));
|
|
|
|
const { collection } = createCollection("energy_unsub_timer");
|
|
const unsub = collection.subscribe(() => undefined);
|
|
const start = collection.start.getTime();
|
|
unsub();
|
|
|
|
vi.advanceTimersByTime(48 * 60 * 60 * 1000);
|
|
|
|
assert.equal(collection.start.getTime(), start);
|
|
});
|
|
|
|
it("does not roll a remembered week preset over to today", () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-06-18T15:00:00-04:00"));
|
|
|
|
const { collection } = createCollection("energy_week_stored", "this_week");
|
|
const weekStart = collection.start.getTime();
|
|
const unsub = collection.subscribe(() => undefined);
|
|
|
|
vi.setSystemTime(new Date("2026-06-20T10:00:00-04:00"));
|
|
vi.advanceTimersByTime(48 * 60 * 60 * 1000);
|
|
|
|
assert.equal(collection.start.getTime(), weekStart);
|
|
|
|
unsub();
|
|
});
|
|
|
|
it("does not roll a remembered week preset over to today with midnightRollover", () => {
|
|
vi.useFakeTimers();
|
|
vi.setSystemTime(new Date("2026-06-18T15:00:00-04:00"));
|
|
|
|
const { collection } = createCollection(
|
|
"energy_week_stored_now",
|
|
"this_week",
|
|
true
|
|
);
|
|
const weekStart = collection.start.getTime();
|
|
const unsub = collection.subscribe(() => undefined);
|
|
|
|
vi.setSystemTime(new Date("2026-06-20T10:00:00-04:00"));
|
|
vi.advanceTimersByTime(48 * 60 * 60 * 1000);
|
|
|
|
assert.equal(collection.start.getTime(), weekStart);
|
|
|
|
unsub();
|
|
});
|
|
});
|
|
|
|
describe("getEnergyDefaultPeriodStorageKey", () => {
|
|
it("uses an explicit collection key", () => {
|
|
assert.equal(
|
|
getEnergyDefaultPeriodStorageKey(
|
|
{ panelUrl: "energy" } as HomeAssistant,
|
|
"energy_dashboard"
|
|
),
|
|
"energy-default-period-_energy_dashboard"
|
|
);
|
|
});
|
|
|
|
it("scopes to the panel when no collection key is given", () => {
|
|
assert.equal(
|
|
getEnergyDefaultPeriodStorageKey({
|
|
panelUrl: "my-dashboard",
|
|
} as HomeAssistant),
|
|
"energy-default-period-_energy_my-dashboard"
|
|
);
|
|
});
|
|
|
|
it("falls back to the global key without a panel url", () => {
|
|
assert.equal(
|
|
getEnergyDefaultPeriodStorageKey({} as HomeAssistant),
|
|
"energy-default-period-_energy"
|
|
);
|
|
});
|
|
|
|
it("rejects a collection key with the wrong prefix", () => {
|
|
assert.throws(() =>
|
|
getEnergyDefaultPeriodStorageKey({} as HomeAssistant, "dashboard")
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("computeEnergyLabel", () => {
|
|
const ENTITY_ID = "sensor.washer_energy";
|
|
|
|
const createEntry = (
|
|
entry: Partial<EntityRegistryDisplayEntry>
|
|
): EntityRegistryDisplayEntry =>
|
|
({
|
|
entity_id: ENTITY_ID,
|
|
labels: [],
|
|
...entry,
|
|
}) as EntityRegistryDisplayEntry;
|
|
|
|
const createDevice = (
|
|
device: Partial<DeviceRegistryEntry>
|
|
): DeviceRegistryEntry =>
|
|
({ id: "device1", name_by_user: null, ...device }) as DeviceRegistryEntry;
|
|
|
|
const createHass = (
|
|
friendlyName: string,
|
|
entry?: Partial<EntityRegistryDisplayEntry>,
|
|
device?: Partial<DeviceRegistryEntry>
|
|
) =>
|
|
createMockHass(
|
|
{
|
|
[ENTITY_ID]: createMockEntityState(ENTITY_ID, "1", {
|
|
friendly_name: friendlyName,
|
|
}),
|
|
},
|
|
{
|
|
entities: entry ? { [ENTITY_ID]: createEntry(entry) } : {},
|
|
devices: device ? { device1: createDevice(device) } : {},
|
|
}
|
|
);
|
|
|
|
it("composes the device and entity name", () => {
|
|
const hass = createHass(
|
|
"Washer Energy",
|
|
{ name: "Energy", device_id: "device1" },
|
|
{ name: "Washer" }
|
|
);
|
|
|
|
assert.equal(computeEnergyLabel(hass, ENTITY_ID), "Washer Energy");
|
|
});
|
|
|
|
it("uses the device name alone when the entity has no name of its own", () => {
|
|
const hass = createHass(
|
|
"Washer",
|
|
{ name: "Washer", device_id: "device1" },
|
|
{ name: "Washer" }
|
|
);
|
|
|
|
assert.equal(computeEnergyLabel(hass, ENTITY_ID), "Washer");
|
|
});
|
|
|
|
it("distinguishes entities sharing a name by their device", () => {
|
|
const hass = createHass(
|
|
"Energy",
|
|
{ name: "Energy", device_id: "device1" },
|
|
{ name: "Dishwasher" }
|
|
);
|
|
|
|
assert.equal(computeEnergyLabel(hass, ENTITY_ID), "Dishwasher Energy");
|
|
});
|
|
|
|
it("keeps a name set by the user", () => {
|
|
const hass = createHass(
|
|
"Washer Energy",
|
|
{ name: "Energy", device_id: "device1" },
|
|
{ name: "Washer" }
|
|
);
|
|
|
|
assert.equal(
|
|
computeEnergyLabel(hass, ENTITY_ID, undefined, "Laundry"),
|
|
"Laundry"
|
|
);
|
|
});
|
|
|
|
it("ignores an empty name", () => {
|
|
const hass = createHass(
|
|
"Washer Energy",
|
|
{ name: "Energy", device_id: "device1" },
|
|
{ name: "Washer" }
|
|
);
|
|
|
|
assert.equal(
|
|
computeEnergyLabel(hass, ENTITY_ID, undefined, ""),
|
|
"Washer Energy"
|
|
);
|
|
});
|
|
|
|
it("falls back to the friendly name for an entity outside the registry", () => {
|
|
const hass = createHass("Washer Energy");
|
|
|
|
assert.equal(computeEnergyLabel(hass, ENTITY_ID), "Washer Energy");
|
|
});
|
|
|
|
it("uses the statistic metadata name when there is no entity", () => {
|
|
const hass = createMockHass();
|
|
|
|
assert.equal(
|
|
computeEnergyLabel(hass, "external:solar", {
|
|
statistic_id: "external:solar",
|
|
name: "Solar production",
|
|
} as StatisticsMetaData),
|
|
"Solar production"
|
|
);
|
|
});
|
|
|
|
it("falls back to the statistic id when there is nothing to name it with", () => {
|
|
const hass = createMockHass();
|
|
|
|
assert.equal(computeEnergyLabel(hass, "external:solar"), "external:solar");
|
|
});
|
|
});
|
|
|
|
describe("computeEnergyDeviceLabels", () => {
|
|
const DEVICES = [
|
|
{
|
|
stat_consumption: "sensor.washer_energy",
|
|
stat_rate: "sensor.washer_power",
|
|
},
|
|
{ stat_consumption: "sensor.heater_energy", name: "Heater" },
|
|
];
|
|
|
|
const hass = createMockHass({
|
|
"sensor.washer_energy": createMockEntityState("sensor.washer_energy", "1", {
|
|
friendly_name: "Washer Energy",
|
|
}),
|
|
"sensor.washer_power": createMockEntityState("sensor.washer_power", "5", {
|
|
friendly_name: "Washer Power",
|
|
}),
|
|
});
|
|
|
|
it("keys labels by the consumption statistic", () => {
|
|
assert.deepEqual(computeEnergyDeviceLabels(hass, DEVICES), {
|
|
"sensor.washer_energy": "Washer Energy",
|
|
"sensor.heater_energy": "Heater",
|
|
});
|
|
});
|
|
|
|
it("keys labels by the rate statistic, skipping devices without one", () => {
|
|
assert.deepEqual(
|
|
computeEnergyDeviceLabels(hass, DEVICES, undefined, "stat_rate"),
|
|
{ "sensor.washer_power": "Washer Power" }
|
|
);
|
|
});
|
|
});
|