feat: Add support for Frigate reviews / detections [initial PR] (#2315)
- Add support for Frigate reviews / detections. - Add support for GenAI metadata. - Significant internal refactor to more flexible "UnifiedQuery" to allow mixing cameras with simple metadata and review metadata (e.g. a timeline view of a Frigate camera with reviews, and a Reolink camera with simple metadata). - Add support for folder media as camera media. There are a few more PRs to commit prior to this going live, but commiting this for now due to the scale of the change. BREAKING CHANGE: `media_type` and `events_type` are retired under `live`, `viewer` and `timeline` configuration sections, instead media type is associated (once) with the camera under `media`.
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
||||
CameraEndpointsContext,
|
||||
CameraManagerCameraMetadata,
|
||||
CameraQuery,
|
||||
DefaultQueryParameters,
|
||||
Engine,
|
||||
EngineOptions,
|
||||
EventQuery,
|
||||
@@ -49,6 +50,7 @@ import {
|
||||
PartialQueryConcreteType,
|
||||
PartialRecordingQuery,
|
||||
PartialRecordingSegmentsQuery,
|
||||
PartialReviewQuery,
|
||||
QueryResults,
|
||||
QueryResultsType,
|
||||
QueryReturnType,
|
||||
@@ -60,6 +62,8 @@ import {
|
||||
RecordingSegmentsQueryResults,
|
||||
RecordingSegmentsQueryResultsMap,
|
||||
ResultsMap,
|
||||
ReviewQuery,
|
||||
ReviewQueryResults,
|
||||
} from './types.js';
|
||||
|
||||
export class CameraQueryClassifier {
|
||||
@@ -83,6 +87,11 @@ export class CameraQueryClassifier {
|
||||
): query is MediaMetadataQuery {
|
||||
return query.type === QueryType.MediaMetadata;
|
||||
}
|
||||
public static isReviewQuery(
|
||||
query: CameraQuery | PartialCameraQuery,
|
||||
): query is ReviewQuery {
|
||||
return query.type === QueryType.Review;
|
||||
}
|
||||
}
|
||||
|
||||
export class QueryResultClassifier {
|
||||
@@ -106,9 +115,14 @@ export class QueryResultClassifier {
|
||||
): queryResults is MediaMetadataQueryResults {
|
||||
return queryResults?.type === QueryResultsType.MediaMetadata;
|
||||
}
|
||||
public static isReviewQueryResult(
|
||||
queryResults?: QueryResults | null,
|
||||
): queryResults is ReviewQueryResults {
|
||||
return queryResults?.type === QueryResultsType.Review;
|
||||
}
|
||||
}
|
||||
|
||||
export interface ExtendedMediaQueryResult<T extends MediaQuery> {
|
||||
interface ExtendedMediaQueryResult<T extends MediaQuery> {
|
||||
queries: T[];
|
||||
results: ViewItem[];
|
||||
}
|
||||
@@ -308,6 +322,17 @@ export class CameraManager {
|
||||
});
|
||||
}
|
||||
|
||||
public getDefaultQueryParameters(
|
||||
cameraID: string,
|
||||
queryType: QueryType,
|
||||
): DefaultQueryParameters {
|
||||
const camera = this._store.getCamera(cameraID);
|
||||
if (!camera) {
|
||||
return {};
|
||||
}
|
||||
return camera.getEngine().getDefaultQueryParameters(camera, queryType);
|
||||
}
|
||||
|
||||
public generateDefaultRecordingQueries(
|
||||
cameraIDs: string | Set<string>,
|
||||
partialQuery?: PartialRecordingQuery,
|
||||
@@ -328,6 +353,16 @@ export class CameraManager {
|
||||
});
|
||||
}
|
||||
|
||||
public generateDefaultReviewQueries(
|
||||
cameraIDs: string | Set<string>,
|
||||
partialQuery?: PartialReviewQuery,
|
||||
): ReviewQuery[] | null {
|
||||
return this._generateDefaultQueries(cameraIDs, {
|
||||
type: QueryType.Review,
|
||||
...partialQuery,
|
||||
});
|
||||
}
|
||||
|
||||
protected _generateDefaultQueries<PQT extends PartialCameraQuery>(
|
||||
cameraIDs: string | Set<string>,
|
||||
partialQuery: PQT,
|
||||
@@ -356,6 +391,12 @@ export class CameraManager {
|
||||
cameraIDs,
|
||||
partialQuery,
|
||||
);
|
||||
} else if (CameraQueryClassifier.isReviewQuery(partialQuery)) {
|
||||
queries = engine.generateDefaultReviewQuery(
|
||||
this._store,
|
||||
cameraIDs,
|
||||
partialQuery,
|
||||
);
|
||||
}
|
||||
|
||||
for (const query of queries ?? []) {
|
||||
@@ -568,6 +609,33 @@ export class CameraManager {
|
||||
);
|
||||
}
|
||||
|
||||
public async reviewMedia(media: ViewMedia, reviewed: boolean): Promise<void> {
|
||||
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
||||
const engine = this._store.getEngineForMedia(media);
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
|
||||
if (!cameraConfig || !engine || !hass) {
|
||||
return;
|
||||
}
|
||||
|
||||
const queryStartTime = new Date();
|
||||
|
||||
await this._requestLimit.add(() =>
|
||||
engine.reviewMedia(hass, cameraConfig, media, reviewed),
|
||||
);
|
||||
|
||||
log(
|
||||
this._api.getConfigManager().getCardWideConfig(),
|
||||
'Advanced Camera Card CameraManager review media request (',
|
||||
`Duration: ${(new Date().getTime() - queryStartTime.getTime()) / 1000}s,`,
|
||||
'Media:',
|
||||
media.getID(),
|
||||
', Reviewed:',
|
||||
reviewed,
|
||||
')',
|
||||
);
|
||||
}
|
||||
|
||||
public areMediaQueriesResultsFresh<T extends MediaQuery>(
|
||||
resultsTimestamp: Date,
|
||||
queries: T[] | null,
|
||||
@@ -667,6 +735,13 @@ export class CameraManager {
|
||||
query,
|
||||
engineOptions,
|
||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||
} else if (CameraQueryClassifier.isReviewQuery(query)) {
|
||||
engineResult = (await engine.getReviews(
|
||||
hass,
|
||||
this._store,
|
||||
query,
|
||||
engineOptions,
|
||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||
}
|
||||
|
||||
engineResult?.forEach((value, key) => results.set(key, value));
|
||||
@@ -739,6 +814,11 @@ export class CameraManager {
|
||||
QueryResultClassifier.isRecordingQueryResult(result)
|
||||
) {
|
||||
media = engine.generateMediaFromRecordings(hass, this._store, query, result);
|
||||
} else if (
|
||||
CameraQueryClassifier.isReviewQuery(query) &&
|
||||
QueryResultClassifier.isReviewQueryResult(result)
|
||||
) {
|
||||
media = engine.generateMediaFromReviews(hass, this._store, query, result);
|
||||
}
|
||||
if (media) {
|
||||
mediaArray.push(...media);
|
||||
|
||||
Reference in New Issue
Block a user