131 lines
4 KiB
TypeScript
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;
|
|
}
|