evcc-io/assets/js/utils/forecast.ts

131 lines
4 KiB
TypeScript

import type {
Forecast,
ForecastSlot,
SolarDetails,
UiForecast,
UiForecastSlot,
UiTimeseriesEntry,
UiSolarDetails,
} from "@/types/evcc";
import deepCopy from "./deepClone";
export enum ForecastType {
Solar = "solar",
Price = "price",
Co2 = "co2",
}
function expandSlots(slots?: ForecastSlot[]): UiForecastSlot[] | undefined {
return slots?.map(([start, end, value]) => ({ start: start * 1000, end: end * 1000, value }));
}
function expandSolar(solar?: SolarDetails): UiSolarDetails | undefined {
if (!solar) return undefined;
const { timeseries, ...rest } = solar;
return {
...rest,
timeseries: timeseries?.map(([ts, val]) => ({ ts: ts * 1000, val })),
};
}
// The API publishes forecast slots as [start, end, value] and solar entries as
// [ts, val] with timestamps in unix seconds. Expand them to objects with unix
// milliseconds so components can keep using named fields and `new Date(...)`.
export function expandForecast(forecast?: Forecast): UiForecast {
const { grid, co2, solar, planner, feedin, temperature } = forecast ?? {};
return {
grid: expandSlots(grid),
co2: expandSlots(co2),
solar: expandSolar(solar),
planner: expandSlots(planner),
feedin: expandSlots(feedin),
temperature: expandSlots(temperature),
};
}
// return the date in local YYYY-MM-DD format
function toDayString(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
// return only slots that are on a given date, ignores slots that are in the past
export function filterEntriesByDate(
entries: UiTimeseriesEntry[],
dayString: string
): UiTimeseriesEntry[] {
const now = new Date();
return entries.filter(({ ts }) => {
const isPast = new Date(ts) < now;
const dateMatches = toDayString(new Date(ts)) === dayString;
return !isPast && dateMatches;
});
}
// return the date in local YYYY-MM-DD format
export function dayStringByOffset(day: number): string {
const date = new Date();
date.setDate(date.getDate() + day);
return toDayString(date);
}
// return the highest slot for a given day (0 = today, 1 = tomorrow, etc.)
export function highestSlotIndexByDay(entries: UiTimeseriesEntry[], day: number = 0): number {
const dayString = dayStringByOffset(day);
const dayEntries = filterEntriesByDate(entries, dayString);
const sortedEntries = dayEntries.sort((a, b) => b.val - a.val);
const highestEntry = sortedEntries[0];
if (!highestEntry) return -1;
return entries.findIndex((entry) => entry.ts === highestEntry.ts);
}
export function adjustedSolar(solar?: UiSolarDetails): UiSolarDetails | undefined {
if (!solar?.scale) return solar;
const { scale } = solar;
const result = deepCopy(solar);
if (result.today) result.today.energy *= scale;
if (result.tomorrow) result.tomorrow.energy *= scale;
if (result.dayAfterTomorrow) result.dayAfterTomorrow.energy *= scale;
if (result.timeseries) {
result.timeseries.forEach((entry) => {
entry.val *= scale;
});
}
result.scale = 1 / scale; // invert to allow back-adjustment
return result;
}
export function isStaticTariff(slots?: UiForecastSlot[]): boolean {
if (!Array.isArray(slots) || slots.length === 0) return false;
const firstValue = slots[0].value;
return slots.every((slot) => slot.value === firstValue);
}
export interface SlotWithValue {
start: string | number | Date;
value: number;
}
// find index of first slot with lowest sum over span (e.g. span=4 for 1h with 15min slots)
export function findLowestSumSlotIndex(slots: SlotWithValue[], span: number): number {
if (slots.length < span) return -1;
let lowestSum = Infinity;
let lowestIndex = -1;
for (let i = 0; i <= slots.length - span; i++) {
const sum = slots.slice(i, i + span).reduce((acc, slot) => acc + slot.value, 0);
if (sum < lowestSum) {
lowestSum = sum;
lowestIndex = i;
}
}
return lowestIndex;
}