Initial code for major engine refactor.

This commit is contained in:
Dermot Duffy
2023-01-24 19:36:54 -08:00
parent 03de7b473a
commit 093925cb40
39 changed files with 3506 additions and 2459 deletions
+225
View File
@@ -0,0 +1,225 @@
import { HomeAssistant } from 'custom-card-helpers';
import sub from 'date-fns/sub';
import { DataSet } from 'vis-data';
import { IdType, TimelineItem, TimelineWindow } from 'vis-timeline/esnext';
import { CameraConfig, ClipsOrSnapshotsOrAll } from '../types';
import { DataManager } from './data/data-manager';
import { EventQuery } from './data/data-types';
import { RecordingSegment, RecordingSegments } from './frigate';
import { capEndDate, convertRangeToCacheFriendlyTimes } from './data/data-manager-util';
import { EventMediaQueries } from '../view';
import { ViewMedia } from '../view-media';
import { compressRanges, MemoryRangeSet } from './data/data-manager-range';
import { ModifyInterface } from './basic';
// Allow timeline freshness to be at least this number of seconds out of date
// (caching times in the data-engine may increase the effective delay).
const TIMELINE_FRESHNESS_TOLERANCE_SECONDS = 30;
// Number of seconds gap allowable in order to consider two recording segments
// to be consecutive. Some low performance cameras have trouble and without a
// generous allowance here the timeline may be littered with individual segments
// instead of clean recording blocks.
const TIMELINE_RECORDING_SEGMENT_CONSECUTIVE_TOLERANCE_SECONDS = 60;
export interface FrigateCardTimelineItem extends TimelineItem {
// Use numbers to avoid significant volumes of Date object construction (for
// high-quantity recording segments).
start: number;
end?: number;
media?: ViewMedia;
}
export class TimelineDataSource {
protected _dataManager: DataManager;
protected _dataset: DataSet<FrigateCardTimelineItem> = new DataSet();
// The ranges in which recordings have been calculated and added for.
protected _recordingRanges = new MemoryRangeSet();
protected _cameraIDs: Set<string>;
protected _mediaType: ClipsOrSnapshotsOrAll;
constructor(
dataManager: DataManager,
cameraIDs: Set<string>,
media: ClipsOrSnapshotsOrAll,
) {
this._dataManager = dataManager;
this._cameraIDs = cameraIDs;
this._mediaType = media;
}
get dataset(): DataSet<FrigateCardTimelineItem> {
return this._dataset;
}
public clearEvents(): void {
this._dataset.remove(
this._dataset.get({
filter: (item) => item.type !== 'background',
}),
);
}
public rewriteEvent(id: IdType): void {
// Hack: For timeline uses of the event dataset clustering may not update
// unless the dataset changes, artifically update the dataset to ensure the
// newly selected item cannot be included in a cluster.
// Hack2: Cannot use `updateOnly` here, as vis-data loses the object
// prototype, see: https://github.com/visjs/vis-data/issues/997 . Instead,
// remove then add.
const item = this._dataset.get(id);
if (item) {
this._dataset.remove(id);
this._dataset.add(item);
}
}
public async refresh(
hass: HomeAssistant,
cameras: Map<string, CameraConfig>,
window: TimelineWindow,
): Promise<void> {
await Promise.all([
this._refreshEvents(hass, cameras, window),
this._refreshRecordings(hass, window),
]);
}
public getTimelineEventQueries(window: TimelineWindow): EventQuery[] {
const _window = convertRangeToCacheFriendlyTimes(window, {
endCap: true,
});
return this._dataManager.generateDefaultEventQueries(this._cameraIDs, {
start: _window.start,
end: _window.end,
...(this._mediaType === 'clips' && { hasClip: true }),
...(this._mediaType === 'snapshots' && { hasSnapshot: true }),
});
}
protected async _refreshEvents(
hass: HomeAssistant,
cameras: Map<string, CameraConfig>,
window: TimelineWindow,
): Promise<void> {
const query = new EventMediaQueries(this.getTimelineEventQueries(window));
const results = await this._dataManager.executeMediaQuery(hass, query);
for (const media of results?.getResults() ?? []) {
const endTime = media.getEndTime();
const startTime = media.getStartTime();
const id = media.getID(cameras.get(media.getCameraID()));
if (id && startTime) {
this._dataset.update({
id: id,
group: media.getCameraID(),
content: '',
media: media,
start: startTime.getTime(),
type: endTime ? 'range' : 'point',
...(endTime && { end: endTime.getTime() }),
});
}
}
}
protected async _refreshRecordings(
hass: HomeAssistant,
window: TimelineWindow,
): Promise<void> {
type FrigateCardTimelineItemWithEnd = ModifyInterface<
FrigateCardTimelineItem,
{ end: number }
>;
const convertSegmentToRecording = (
cameraID: string,
segment: RecordingSegment,
): FrigateCardTimelineItemWithEnd => {
return {
id: `recording-${cameraID}-${segment.id}`,
group: cameraID,
start: segment.start_time * 1000,
end: segment.end_time * 1000,
content: '',
type: 'background',
};
};
const getExistingRecordingsForCameraID = (
cameraID: string,
): FrigateCardTimelineItemWithEnd[] => {
return this._dataset.get({
filter: (item) =>
item.type == 'background' && item.group === cameraID && item.end !== undefined,
}) as FrigateCardTimelineItemWithEnd[];
};
const deleteRecordingsForCameraID = (cameraID: string): void => {
this._dataset.remove(
this._dataset.get({
filter: (item) => item.type === 'background' && item.group === cameraID,
}),
);
};
const addRecordings = (recordings: FrigateCardTimelineItemWithEnd[]): void => {
this._dataset.add(recordings);
};
// Calculate an end date that's slightly short of the current time to allow
// for caching up to the freshness tolerance.
const end = sub(capEndDate(window.end), {
seconds: TIMELINE_FRESHNESS_TOLERANCE_SECONDS,
});
const hasCoverage = this._recordingRanges.hasCoverage({
start: window.start,
end: end,
});
if (hasCoverage) {
return;
}
const cacheFriendlyWindow = convertRangeToCacheFriendlyTimes(window, {
endCap: true,
});
const queries = this._dataManager.generateDefaultRecordingSegmentsQueries(
this._cameraIDs,
{
start: cacheFriendlyWindow.start,
end: cacheFriendlyWindow.end,
},
);
const results = await this._dataManager.getRecordingSegments(hass, queries);
const newSegments: Map<string, RecordingSegments> = new Map();
for (const [query, result] of results) {
let destination: RecordingSegments | undefined = newSegments.get(query.cameraID);
if (!destination) {
destination = [];
newSegments.set(query.cameraID, destination);
}
result.segments.forEach((segment) => destination?.push(segment));
}
for (const [cameraID, segments] of newSegments.entries()) {
const existingRecordings = getExistingRecordingsForCameraID(cameraID);
const mergedRecordings = existingRecordings.concat(
segments.map((segment) => convertSegmentToRecording(cameraID, segment)),
);
const compressedRecordings = compressRanges(
mergedRecordings,
TIMELINE_RECORDING_SEGMENT_CONSECUTIVE_TOLERANCE_SECONDS,
) as FrigateCardTimelineItemWithEnd[];
deleteRecordingsForCameraID(cameraID);
addRecordings(compressedRecordings);
}
this._recordingRanges.add({ start: window.start, end: end });
}
}