mirror of
https://github.com/home-assistant/frontend.git
synced 2026-10-26 07:34:13 +00:00
Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7044f38bf8 | ||
|
|
5104db47f4 | ||
|
|
30e1e2f1f8 | ||
|
|
45f85a47fe |
No files matched your search
+32
-124
@@ -1,6 +1,5 @@
|
||||
import {
|
||||
addDays,
|
||||
addHours,
|
||||
addMilliseconds,
|
||||
addMonths,
|
||||
addYears,
|
||||
@@ -803,14 +802,21 @@ const scheduleHourlyRefresh = (collection: EnergyCollection) => {
|
||||
}
|
||||
|
||||
if (collection._active && (!collection.end || collection.end > new Date())) {
|
||||
// The stats are created every hour
|
||||
// Schedule a refresh for 20 minutes past the hour
|
||||
// If the end is larger than the current time.
|
||||
// Long-term hourly stats land around :20. Before that, Core can already
|
||||
// stitch the open hour from short-term rows once the first 5-minute
|
||||
// bucket exists (recorder compiles at :05:10), so prefer an early
|
||||
// refresh after midnight / early-hour fetches, then resume :20.
|
||||
const nextFetch = new Date();
|
||||
if (nextFetch.getMinutes() >= 20) {
|
||||
nextFetch.setHours(nextFetch.getHours() + 1);
|
||||
const firstShortTermRefresh = new Date(nextFetch);
|
||||
firstShortTermRefresh.setMinutes(5, 15, 0);
|
||||
if (nextFetch < firstShortTermRefresh) {
|
||||
nextFetch.setTime(firstShortTermRefresh.getTime());
|
||||
} else {
|
||||
if (nextFetch.getMinutes() >= 20) {
|
||||
nextFetch.setHours(nextFetch.getHours() + 1);
|
||||
}
|
||||
nextFetch.setMinutes(20, 0, 0);
|
||||
}
|
||||
nextFetch.setMinutes(20, 0, 0);
|
||||
|
||||
collection._refreshTimeout = window.setTimeout(
|
||||
() => collection.refresh(),
|
||||
@@ -858,104 +864,30 @@ export const getEnergyDefaultPeriodStorageKey = (
|
||||
return `energy-default-period-${key}`;
|
||||
};
|
||||
|
||||
// When today's first hourly statistic becomes available (01:00 in the
|
||||
// configured timezone). Rolling the statistics view over at midnight would
|
||||
// show an empty graph.
|
||||
export const getEnergyFirstStatisticAt = (
|
||||
now: Date,
|
||||
locale: HomeAssistant["locale"],
|
||||
config: HomeAssistant["config"]
|
||||
): Date => addHours(calcDate(now, startOfDay, locale, config), 1);
|
||||
|
||||
// The statistics Energy view shows yesterday until 01:00 so the graph is not
|
||||
// empty. The real-time "Now" view never does this — it has live data.
|
||||
export const shouldFallbackEnergyPeriodToYesterday = (
|
||||
midnightRollover: boolean,
|
||||
now: Date,
|
||||
locale: HomeAssistant["locale"],
|
||||
config: HomeAssistant["config"]
|
||||
): boolean =>
|
||||
!midnightRollover &&
|
||||
now.getTime() < getEnergyFirstStatisticAt(now, locale, config).getTime();
|
||||
|
||||
// Live day used while a rollover timer is scheduled (today, or the hour-0
|
||||
// yesterday fallback). Custom dates do not use this. If the user already
|
||||
// picked today during hour 0, keep today rather than snapping back.
|
||||
// Live day used while a rollover timer is scheduled. Custom dates do not use
|
||||
// this. Always today — Core fills the unfinished current hour from short-term
|
||||
// stats in statistics_during_period, so hour/day charts are not empty after
|
||||
// the first short-term bucket.
|
||||
export const getEnergyLiveDayPeriod = (
|
||||
midnightRollover: boolean,
|
||||
now: Date,
|
||||
locale: HomeAssistant["locale"],
|
||||
config: HomeAssistant["config"],
|
||||
currentStart: Date
|
||||
): { start: Date; end: Date } => {
|
||||
const todayStart = calcDate(now, startOfDay, locale, config);
|
||||
if (
|
||||
shouldFallbackEnergyPeriodToYesterday(
|
||||
midnightRollover,
|
||||
now,
|
||||
locale,
|
||||
config
|
||||
) &&
|
||||
currentStart.getTime() !== todayStart.getTime()
|
||||
) {
|
||||
const yesterday = calcDate(now, addDays, locale, config, -1);
|
||||
return {
|
||||
start: calcDate(yesterday, startOfDay, locale, config),
|
||||
end: calcDate(yesterday, endOfDay, locale, config),
|
||||
};
|
||||
}
|
||||
return {
|
||||
start: todayStart,
|
||||
end: calcDate(now, endOfDay, locale, config),
|
||||
};
|
||||
};
|
||||
config: HomeAssistant["config"]
|
||||
): { start: Date; end: Date } => ({
|
||||
start: calcDate(now, startOfDay, locale, config),
|
||||
end: calcDate(now, endOfDay, locale, config),
|
||||
});
|
||||
|
||||
// When does the collection's day period need to roll over to the next day?
|
||||
// With `midnightRollover` (the real-time "Now" view) it rolls over right at
|
||||
// midnight. Otherwise it waits an hour, until the new day's first hourly
|
||||
// statistic exists — rolling over at midnight would show an empty graph.
|
||||
// Pass `periodStart` when the collection is on a specific day: hour-0
|
||||
// yesterday (and any older stale live day) must wake at today 01:00, not
|
||||
// tomorrow 01:00. Keep tomorrow 01:00 only when the user already picked today.
|
||||
// Next midnight in the configured zone, not browser-local addDays, so a DST
|
||||
// transition cannot skip a server-tz day.
|
||||
export const getNextEnergyPeriodStart = (
|
||||
midnightRollover: boolean,
|
||||
now: Date,
|
||||
locale: HomeAssistant["locale"],
|
||||
config: HomeAssistant["config"],
|
||||
periodStart?: Date
|
||||
): Date => {
|
||||
const todayStart = calcDate(now, startOfDay, locale, config);
|
||||
if (
|
||||
periodStart &&
|
||||
shouldFallbackEnergyPeriodToYesterday(
|
||||
midnightRollover,
|
||||
now,
|
||||
locale,
|
||||
config
|
||||
) &&
|
||||
periodStart.getTime() !== todayStart.getTime()
|
||||
) {
|
||||
return getEnergyFirstStatisticAt(now, locale, config);
|
||||
}
|
||||
// Next midnight in the configured zone, not browser-local addDays, so a
|
||||
// DST transition cannot skip a server-tz day.
|
||||
const nextMidnight = addMilliseconds(
|
||||
calcDate(now, endOfDay, locale, config),
|
||||
1
|
||||
);
|
||||
return midnightRollover ? nextMidnight : addHours(nextMidnight, 1);
|
||||
};
|
||||
config: HomeAssistant["config"]
|
||||
): Date => addMilliseconds(calcDate(now, endOfDay, locale, config), 1);
|
||||
|
||||
export const getEnergyDataCollection = (
|
||||
hass: HomeAssistant,
|
||||
options: {
|
||||
prefs?: EnergyPreferences;
|
||||
key?: string;
|
||||
// The real-time "Now" view opts in to rolling its day period over at
|
||||
// midnight rather than an hour later (it shows live data, so it always
|
||||
// tracks today and never falls back to yesterday in the first hour).
|
||||
midnightRollover?: boolean;
|
||||
} = {}
|
||||
options: { prefs?: EnergyPreferences; key?: string } = {}
|
||||
): EnergyCollection => {
|
||||
const [key, collectionKey] = convertCollectionKeyToConnection(
|
||||
hass,
|
||||
@@ -965,8 +897,6 @@ export const getEnergyDataCollection = (
|
||||
return (hass.connection as any)[key];
|
||||
}
|
||||
|
||||
const midnightRollover = options.midnightRollover ?? false;
|
||||
|
||||
energyCollectionKeys.add(collectionKey);
|
||||
|
||||
const collection = getCollection<EnergyData>(
|
||||
@@ -1005,18 +935,12 @@ export const getEnergyDataCollection = (
|
||||
collection._active = 0;
|
||||
collection.prefs = options.prefs;
|
||||
|
||||
// True while the collection is tracking the rolling "today" (or hour-0
|
||||
// yesterday) day. Cleared when the user picks a custom range.
|
||||
// True while the collection is tracking the rolling "today" day. Cleared
|
||||
// when the user picks a custom range.
|
||||
let followLiveDay = false;
|
||||
|
||||
const applyLiveDayPeriod = (now: Date): boolean => {
|
||||
const live = getEnergyLiveDayPeriod(
|
||||
midnightRollover,
|
||||
now,
|
||||
hass.locale,
|
||||
hass.config,
|
||||
collection.start
|
||||
);
|
||||
const live = getEnergyLiveDayPeriod(now, hass.locale, hass.config);
|
||||
const changed =
|
||||
collection.start.getTime() !== live.start.getTime() ||
|
||||
collection.end?.getTime() !== live.end.getTime();
|
||||
@@ -1045,11 +969,9 @@ export const getEnergyDataCollection = (
|
||||
Math.max(
|
||||
0,
|
||||
getNextEnergyPeriodStart(
|
||||
midnightRollover,
|
||||
scheduledAt,
|
||||
hass.locale,
|
||||
hass.config,
|
||||
collection.start
|
||||
hass.config
|
||||
).getTime() - scheduledAt.getTime()
|
||||
)
|
||||
);
|
||||
@@ -1093,26 +1015,12 @@ export const getEnergyDataCollection = (
|
||||
};
|
||||
};
|
||||
|
||||
// Set start to start of today if we have data for today, otherwise yesterday.
|
||||
// The real-time "Now" view always tracks today; it shows live data even
|
||||
// before today's first statistic exists, so it never falls back to yesterday.
|
||||
const now = new Date();
|
||||
const preferredPeriod =
|
||||
(localStorage.getItem(
|
||||
getEnergyDefaultPeriodStorageKey(hass, options.key)
|
||||
) as DateRange) || "today";
|
||||
const period =
|
||||
preferredPeriod === "today" &&
|
||||
shouldFallbackEnergyPeriodToYesterday(
|
||||
midnightRollover,
|
||||
now,
|
||||
hass.locale,
|
||||
hass.config
|
||||
)
|
||||
? "yesterday"
|
||||
: preferredPeriod;
|
||||
|
||||
const [start, end] = calcDateRange(hass.locale, hass.config, period);
|
||||
const [start, end] = calcDateRange(hass.locale, hass.config, preferredPeriod);
|
||||
collection.start = calcDate(start, startOfDay, hass.locale, hass.config);
|
||||
collection.end = calcDate(end, endOfDay, hass.locale, hass.config);
|
||||
followLiveDay = preferredPeriod === "today";
|
||||
|
||||
@@ -158,9 +158,6 @@ async function fetchEnergyPrefs(
|
||||
): Promise<EnergyPreferences> {
|
||||
const collection = getEnergyDataCollection(hass, {
|
||||
key: defaultCollection || DEFAULT_ENERGY_COLLECTION_KEY,
|
||||
// When landing directly on the "Now" view this warms its real-time
|
||||
// collection, so it must be created with midnight rollover too.
|
||||
midnightRollover: defaultCollection === DEFAULT_POWER_COLLECTION_KEY,
|
||||
});
|
||||
|
||||
return await new Promise<EnergyPreferences>((resolve) => {
|
||||
|
||||
@@ -34,8 +34,6 @@ export class PowerViewStrategy extends ReactiveElement {
|
||||
|
||||
const energyCollection = getEnergyDataCollection(hass, {
|
||||
key: collectionKey,
|
||||
// The "Now" view is real-time; roll its day period over at midnight.
|
||||
midnightRollover: true,
|
||||
});
|
||||
if (!energyCollection.prefs) {
|
||||
await energyCollection.refresh();
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
addDays,
|
||||
addHours,
|
||||
addMilliseconds,
|
||||
endOfDay,
|
||||
startOfDay,
|
||||
} from "date-fns";
|
||||
import { addDays, addMilliseconds, endOfDay, startOfDay } from "date-fns";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import { afterAll, assert, beforeAll, describe, it } from "vitest";
|
||||
|
||||
@@ -18,7 +12,6 @@ import {
|
||||
TimeZone,
|
||||
} from "../../src/data/translation";
|
||||
import {
|
||||
getEnergyFirstStatisticAt,
|
||||
getEnergyLiveDayPeriod,
|
||||
getNextEnergyPeriodStart,
|
||||
} from "../../src/data/energy";
|
||||
@@ -55,62 +48,52 @@ describe("energy period DST (Europe/Berlin local TZ)", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("schedules tomorrow 01:00 in the server zone, not via browser-local addDays", () => {
|
||||
it("schedules tomorrow midnight in the server zone, not via browser-local addDays", () => {
|
||||
// 23:30 JST on 24 Oct 2026. Europe/Berlin falls back on 25 Oct; local
|
||||
// addDays(now, 1) then startOfDay in Tokyo skips to 26 Oct 01:00 JST.
|
||||
// addDays(now, 1) then startOfDay in Tokyo skips to 26 Oct midnight JST.
|
||||
const now = new Date("2026-10-24T14:30:00.000Z");
|
||||
|
||||
// Compare to the tz-internal formula rather than a hardcoded instant:
|
||||
// the formula is the production invariant, and a pinned ISO string would
|
||||
// not explain why UTC CI cannot catch a raw addDays(now, 1) regression.
|
||||
const tzInternal = addHours(
|
||||
addMilliseconds(calcDate(now, endOfDay, locale, tokyoConfig), 1),
|
||||
const tzInternal = addMilliseconds(
|
||||
calcDate(now, endOfDay, locale, tokyoConfig),
|
||||
1
|
||||
);
|
||||
const browserLocalAddDays = getEnergyFirstStatisticAt(
|
||||
const browserLocalAddDays = calcDate(
|
||||
addDays(now, 1),
|
||||
startOfDay,
|
||||
locale,
|
||||
tokyoConfig
|
||||
);
|
||||
const actual = getNextEnergyPeriodStart(false, now, locale, tokyoConfig);
|
||||
const actual = getNextEnergyPeriodStart(now, locale, tokyoConfig);
|
||||
|
||||
assert.equal(actual.getTime(), tzInternal.getTime());
|
||||
assert.notEqual(actual.getTime(), browserLocalAddDays.getTime());
|
||||
assert.equal(
|
||||
actual.getTime(),
|
||||
new Date("2026-10-24T16:00:00.000Z").getTime()
|
||||
new Date("2026-10-24T15:00:00.000Z").getTime()
|
||||
);
|
||||
assert.equal(
|
||||
browserLocalAddDays.getTime(),
|
||||
new Date("2026-10-25T16:00:00.000Z").getTime()
|
||||
new Date("2026-10-25T15:00:00.000Z").getTime()
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves yesterday in the server zone, not via browser-local addDays", () => {
|
||||
// 00:30 JST on 26 Oct 2026. Browser-local addDays can land two days back.
|
||||
it("resolves today in the server zone, not via browser-local addDays", () => {
|
||||
// 00:30 JST on 26 Oct 2026. Browser-local yesterday can land two days back.
|
||||
const now = new Date("2026-10-25T15:30:00.000Z");
|
||||
|
||||
const tzInternal = calcDate(
|
||||
calcDate(now, addDays, locale, tokyoConfig, -1),
|
||||
startOfDay,
|
||||
locale,
|
||||
tokyoConfig
|
||||
);
|
||||
const browserLocalAddDays = calcDate(
|
||||
const tzInternal = calcDate(now, startOfDay, locale, tokyoConfig);
|
||||
const browserLocalYesterday = calcDate(
|
||||
addDays(now, -1),
|
||||
startOfDay,
|
||||
locale,
|
||||
tokyoConfig
|
||||
);
|
||||
const live = getEnergyLiveDayPeriod(
|
||||
false,
|
||||
now,
|
||||
locale,
|
||||
tokyoConfig,
|
||||
new Date(0)
|
||||
);
|
||||
const live = getEnergyLiveDayPeriod(now, locale, tokyoConfig);
|
||||
|
||||
assert.equal(live.start.getTime(), tzInternal.getTime());
|
||||
assert.notEqual(live.start.getTime(), browserLocalAddDays.getTime());
|
||||
assert.notEqual(live.start.getTime(), browserLocalYesterday.getTime());
|
||||
});
|
||||
});
|
||||
+38
-186
@@ -20,9 +20,7 @@ import {
|
||||
formatPowerShort,
|
||||
getNextEnergyPeriodStart,
|
||||
getEnergyDefaultPeriodStorageKey,
|
||||
getEnergyFirstStatisticAt,
|
||||
getEnergyLiveDayPeriod,
|
||||
shouldFallbackEnergyPeriodToYesterday,
|
||||
getEnergyDataCollection,
|
||||
EMPTY_PREFERENCES,
|
||||
} from "../../src/data/energy";
|
||||
@@ -905,207 +903,64 @@ describe("getNextEnergyPeriodStart", () => {
|
||||
energyPeriodConfig
|
||||
).getTime() === date.getTime();
|
||||
|
||||
it("rolls the real-time view over at midnight, statistics an hour later", () => {
|
||||
it("schedules the next midnight in the configured zone", () => {
|
||||
const now = new Date("2026-06-19T15:30:00-04:00");
|
||||
|
||||
const realTime = getNextEnergyPeriodStart(
|
||||
true,
|
||||
now,
|
||||
energyPeriodLocale,
|
||||
energyPeriodConfig
|
||||
);
|
||||
const statistics = getNextEnergyPeriodStart(
|
||||
false,
|
||||
const next = getNextEnergyPeriodStart(
|
||||
now,
|
||||
energyPeriodLocale,
|
||||
energyPeriodConfig
|
||||
);
|
||||
|
||||
// Real-time rolls over exactly at the next midnight.
|
||||
assert.isTrue(isMidnight(realTime));
|
||||
assert.isTrue(isMidnight(next));
|
||||
assert.equal(
|
||||
realTime.getTime(),
|
||||
next.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", () => {
|
||||
it("advances to the following midnight when called after midnight", () => {
|
||||
const now = new Date("2026-06-20T00:30:00-04:00");
|
||||
|
||||
const realTime = getNextEnergyPeriodStart(
|
||||
true,
|
||||
const next = getNextEnergyPeriodStart(
|
||||
now,
|
||||
energyPeriodLocale,
|
||||
energyPeriodConfig
|
||||
);
|
||||
|
||||
assert.isTrue(isMidnight(realTime));
|
||||
assert.isTrue(isMidnight(next));
|
||||
// Next midnight is June 21, not the already-passed June 20 midnight.
|
||||
assert.equal(
|
||||
realTime.getTime(),
|
||||
next.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", () => {
|
||||
it("returns today during hour 0", () => {
|
||||
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
|
||||
energyPeriodConfig
|
||||
);
|
||||
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", () => {
|
||||
it("returns today after 01:00", () => {
|
||||
const now = new Date("2026-06-20T10:00:00-04:00");
|
||||
const live = getEnergyLiveDayPeriod(
|
||||
false,
|
||||
now,
|
||||
energyPeriodLocale,
|
||||
energyPeriodConfig,
|
||||
energyPeriodDay(now, -2).start
|
||||
energyPeriodConfig
|
||||
);
|
||||
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", () => {
|
||||
@@ -1114,11 +969,7 @@ describe("getEnergyDataCollection live day", () => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const createCollection = (
|
||||
key: string,
|
||||
preset?: string,
|
||||
midnightRollover = false
|
||||
) => {
|
||||
const createCollection = (key: string, preset?: string) => {
|
||||
const hass = createMockHass();
|
||||
hass.locale = energyPeriodLocale;
|
||||
hass.config = { ...hass.config, time_zone: "America/New_York" };
|
||||
@@ -1143,7 +994,6 @@ describe("getEnergyDataCollection live day", () => {
|
||||
collection: getEnergyDataCollection(hass, {
|
||||
key,
|
||||
prefs: EMPTY_PREFERENCES,
|
||||
midnightRollover,
|
||||
}),
|
||||
callWS,
|
||||
};
|
||||
@@ -1152,10 +1002,18 @@ describe("getEnergyDataCollection live day", () => {
|
||||
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 () => {
|
||||
it("starts today during hour 0 and rolls over at midnight", async () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-06-20T00:30:00-04:00"));
|
||||
|
||||
assert.equal(
|
||||
createCollection("energy_hour0").collection.start.getTime(),
|
||||
energyPeriodDay(new Date()).start.getTime()
|
||||
);
|
||||
|
||||
// Create near end of day so a 30-minute advance hits midnight without
|
||||
// also firing many hourly :20 refreshes.
|
||||
vi.setSystemTime(new Date("2026-06-20T23:30:00-04:00"));
|
||||
const { collection, callWS } = createCollection("energy_timer");
|
||||
const refresh = vi.spyOn(collection, "refresh");
|
||||
const unsub = collection.subscribe(() => undefined);
|
||||
@@ -1165,7 +1023,7 @@ describe("getEnergyDataCollection live day", () => {
|
||||
|
||||
assert.equal(
|
||||
collection.start.getTime(),
|
||||
energyPeriodDay(new Date(), -1).start.getTime()
|
||||
energyPeriodDay(new Date()).start.getTime()
|
||||
);
|
||||
|
||||
await vi.advanceTimersByTimeAsync(30 * 60 * 1000);
|
||||
@@ -1175,7 +1033,21 @@ describe("getEnergyDataCollection live day", () => {
|
||||
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.
|
||||
// The midnight callback must refresh() so getEnergyData runs for the new day.
|
||||
assert.equal(refresh.mock.calls.length, 1);
|
||||
assert.equal(energyInfoFetches(callWS).length, 1);
|
||||
|
||||
// Midnight refresh is before the first short-term bucket. Schedule a
|
||||
// follow-up after recorder's :05:10 compile, then resume :20 cadence.
|
||||
refresh.mockClear();
|
||||
callWS.mockClear();
|
||||
await vi.advanceTimersByTimeAsync(5 * 60 * 1000 + 15 * 1000);
|
||||
assert.equal(refresh.mock.calls.length, 1);
|
||||
assert.equal(energyInfoFetches(callWS).length, 1);
|
||||
|
||||
refresh.mockClear();
|
||||
callWS.mockClear();
|
||||
await vi.advanceTimersByTimeAsync(14 * 60 * 1000 + 45 * 1000);
|
||||
assert.equal(refresh.mock.calls.length, 1);
|
||||
assert.equal(energyInfoFetches(callWS).length, 1);
|
||||
|
||||
@@ -1284,26 +1156,6 @@ describe("getEnergyDataCollection live day", () => {
|
||||
|
||||
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", () => {
|
||||
|
||||
Reference in new issue
Block a user