ha-frontend/src/data/cached-history.ts

230 lines
5.6 KiB
TypeScript

import { HassEntity } from "home-assistant-js-websocket";
import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import {
computeHistory,
fetchRecent,
HistoryResult,
LineChartUnit,
TimelineEntity,
} from "./history";
export interface CacheConfig {
cacheKey: string;
hoursToShow: number;
}
interface CachedResults {
prom: Promise<HistoryResult>;
startTime: Date;
endTime: Date;
language: string;
data: HistoryResult;
}
// This is a different interface, a different cache :(
interface RecentCacheResults {
created: number;
language: string;
data: Promise<HistoryResult>;
}
const RECENT_THRESHOLD = 60000; // 1 minute
const RECENT_CACHE: { [cacheKey: string]: RecentCacheResults } = {};
const stateHistoryCache: { [cacheKey: string]: CachedResults } = {};
// Cached type 1 unction. Without cache config.
export const getRecent = (
hass: HomeAssistant,
entityId: string,
startTime: Date,
endTime: Date,
localize: LocalizeFunc,
language: string
) => {
const cacheKey = entityId;
const cache = RECENT_CACHE[cacheKey];
if (
cache &&
Date.now() - cache.created < RECENT_THRESHOLD &&
cache.language === language
) {
return cache.data;
}
const prom = fetchRecent(hass, entityId, startTime, endTime).then(
(stateHistory) => computeHistory(hass, stateHistory, localize),
(err) => {
delete RECENT_CACHE[entityId];
throw err;
}
);
RECENT_CACHE[cacheKey] = {
created: Date.now(),
language,
data: prom,
};
return prom;
};
// Cache type 2 functionality
function getEmptyCache(
language: string,
startTime: Date,
endTime: Date
): CachedResults {
return {
prom: Promise.resolve({ line: [], timeline: [] }),
language,
startTime,
endTime,
data: { line: [], timeline: [] },
};
}
export const getRecentWithCache = (
hass: HomeAssistant,
entityId: string,
cacheConfig: CacheConfig,
localize: LocalizeFunc,
language: string
) => {
const cacheKey = cacheConfig.cacheKey;
const endTime = new Date();
const startTime = new Date(endTime);
startTime.setHours(startTime.getHours() - cacheConfig.hoursToShow);
let toFetchStartTime = startTime;
let appendingToCache = false;
let cache = stateHistoryCache[cacheKey + `_${cacheConfig.hoursToShow}`];
if (
cache &&
toFetchStartTime >= cache.startTime &&
toFetchStartTime <= cache.endTime &&
cache.language === language
) {
toFetchStartTime = cache.endTime;
appendingToCache = true;
// This pretty much never happens as endTime is usually set to now
if (endTime <= cache.endTime) {
return cache.prom;
}
} else {
cache = stateHistoryCache[cacheKey] = getEmptyCache(
language,
startTime,
endTime
);
}
const curCacheProm = cache.prom;
const genProm = async () => {
let fetchedHistory: HassEntity[][];
try {
const results = await Promise.all([
curCacheProm,
fetchRecent(
hass,
entityId,
toFetchStartTime,
endTime,
appendingToCache
),
]);
fetchedHistory = results[1];
} catch (err: any) {
delete stateHistoryCache[cacheKey];
throw err;
}
const stateHistory = computeHistory(hass, fetchedHistory, localize);
if (appendingToCache) {
mergeLine(stateHistory.line, cache.data.line);
mergeTimeline(stateHistory.timeline, cache.data.timeline);
pruneStartTime(startTime, cache.data);
} else {
cache.data = stateHistory;
}
return cache.data;
};
cache.prom = genProm();
cache.startTime = startTime;
cache.endTime = endTime;
return cache.prom;
};
const mergeLine = (
historyLines: LineChartUnit[],
cacheLines: LineChartUnit[]
) => {
historyLines.forEach((line) => {
const unit = line.unit;
const oldLine = cacheLines.find((cacheLine) => cacheLine.unit === unit);
if (oldLine) {
line.data.forEach((entity) => {
const oldEntity = oldLine.data.find(
(cacheEntity) => entity.entity_id === cacheEntity.entity_id
);
if (oldEntity) {
oldEntity.states = oldEntity.states.concat(entity.states);
} else {
oldLine.data.push(entity);
}
});
} else {
cacheLines.push(line);
}
});
};
const mergeTimeline = (
historyTimelines: TimelineEntity[],
cacheTimelines: TimelineEntity[]
) => {
historyTimelines.forEach((timeline) => {
const oldTimeline = cacheTimelines.find(
(cacheTimeline) => cacheTimeline.entity_id === timeline.entity_id
);
if (oldTimeline) {
oldTimeline.data = oldTimeline.data.concat(timeline.data);
} else {
cacheTimelines.push(timeline);
}
});
};
const pruneArray = (originalStartTime: Date, arr) => {
if (arr.length === 0) {
return arr;
}
const changedAfterStartTime = arr.findIndex(
(state) => new Date(state.last_changed) > originalStartTime
);
if (changedAfterStartTime === 0) {
// If all changes happened after originalStartTime then we are done.
return arr;
}
// If all changes happened at or before originalStartTime. Use last index.
const updateIndex =
changedAfterStartTime === -1 ? arr.length - 1 : changedAfterStartTime - 1;
arr[updateIndex].last_changed = originalStartTime;
return arr.slice(updateIndex);
};
const pruneStartTime = (originalStartTime: Date, cacheData: HistoryResult) => {
cacheData.line.forEach((line) => {
line.data.forEach((entity) => {
entity.states = pruneArray(originalStartTime, entity.states);
});
});
cacheData.timeline.forEach((timeline) => {
timeline.data = pruneArray(originalStartTime, timeline.data);
});
};