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:
@@ -15,10 +15,8 @@ export class FoldersExecutor {
|
||||
this._ha = engines?.ha ?? new HAFoldersEngine();
|
||||
}
|
||||
|
||||
public generateDefaultFolderQuery(folder: FolderConfig): FolderQuery | null {
|
||||
return (
|
||||
this._getFolderEngine(folder.type)?.generateDefaultFolderQuery(folder) ?? null
|
||||
);
|
||||
public getDefaultQueryParameters(folder: FolderConfig): FolderQuery | null {
|
||||
return this._getFolderEngine(folder.type)?.getDefaultQueryParameters(folder) ?? null;
|
||||
}
|
||||
|
||||
public generateChildFolderQuery(
|
||||
@@ -79,6 +77,15 @@ export class FoldersExecutor {
|
||||
);
|
||||
}
|
||||
|
||||
public areResultsFresh(resultsTimestamp: Date, query: FolderQuery): boolean {
|
||||
return (
|
||||
this._getFolderEngine(query.folder.type)?.areResultsFresh(
|
||||
resultsTimestamp,
|
||||
query,
|
||||
) ?? true
|
||||
);
|
||||
}
|
||||
|
||||
private _getFolderEngine(type?: FolderType): FoldersEngine | null {
|
||||
switch (type) {
|
||||
case folderTypeSchema.enum.ha:
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { sub } from 'date-fns';
|
||||
import { NonEmptyTuple } from 'type-fest';
|
||||
import { ConditionState } from '../../../conditions/types';
|
||||
import {
|
||||
FolderConfig,
|
||||
folderTypeSchema,
|
||||
HA_MEDIA_SOURCE_ROOT,
|
||||
HAFolderPathComponent,
|
||||
HAFolderConfig,
|
||||
HAFolderPathComponent,
|
||||
} from '../../../config/schema/folders';
|
||||
import { getViewItemsFromBrowseMediaArray } from '../../../ha/browse-media/browse-media-to-view-media';
|
||||
import { BrowseMediaViewFolder } from '../../../ha/browse-media/item';
|
||||
import {
|
||||
BrowseMedia,
|
||||
BROWSE_MEDIA_CACHE_SECONDS,
|
||||
BrowseMediaCache,
|
||||
BrowseMediaMetadata,
|
||||
RichBrowseMedia,
|
||||
@@ -22,16 +23,18 @@ import {
|
||||
} from '../../../ha/browse-media/walker';
|
||||
import { getMediaDownloadPath } from '../../../ha/download';
|
||||
import { HomeAssistant } from '../../../ha/types';
|
||||
import { QuerySource } from '../../../query-source.js';
|
||||
import { Endpoint } from '../../../types';
|
||||
|
||||
import { ViewFolder, ViewItem } from '../../../view/item';
|
||||
import { ViewItemClassifier } from '../../../view/item-classifier';
|
||||
import { ViewItemCapabilities } from '../../../view/types';
|
||||
import {
|
||||
DownloadHelpers,
|
||||
EngineOptions,
|
||||
FolderPathComponent,
|
||||
FolderQuery,
|
||||
FoldersEngine,
|
||||
FolderPathComponent,
|
||||
} from '../types';
|
||||
import { MediaMatcher } from './media-matcher';
|
||||
import { MetadataGenerator } from './metadata-generator.js';
|
||||
@@ -83,17 +86,18 @@ export class HAFoldersEngine implements FoldersEngine {
|
||||
return;
|
||||
}
|
||||
|
||||
public generateDefaultFolderQuery(folder: FolderConfig): FolderQuery | null {
|
||||
public getDefaultQueryParameters(folder: FolderConfig): FolderQuery | null {
|
||||
if (folder.type !== folderTypeSchema.enum.ha) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
source: QuerySource.Folder,
|
||||
folder,
|
||||
path: this.getDefaultFolderPathComponents(folder.ha),
|
||||
path: this._getDefaultPathComponents(folder.ha),
|
||||
};
|
||||
}
|
||||
|
||||
private getDefaultFolderPathComponents(
|
||||
private _getDefaultPathComponents(
|
||||
haFolderConfig?: HAFolderConfig,
|
||||
): NonEmptyTuple<FolderPathComponent> {
|
||||
const shouldAddDefaultRoot =
|
||||
@@ -158,14 +162,21 @@ export class HAFoldersEngine implements FoldersEngine {
|
||||
targets: BrowseMediaTarget<BrowseMediaMetadata>[],
|
||||
): BrowseMediaStep<BrowseMediaMetadata>[] => {
|
||||
const nextComponent = pathComponents.shift();
|
||||
const limit = query.limit ?? null;
|
||||
|
||||
return [
|
||||
{
|
||||
targets,
|
||||
metadataGenerator: (media: BrowseMedia, parent?: BrowseMedia) =>
|
||||
metadataGenerator: (media, parent) =>
|
||||
this._metadataGenerator.generate(media, parent, nextComponent?.ha?.parsers),
|
||||
|
||||
// At the final step (no nextComponent), apply limit via earlyExit.
|
||||
...(limit && {
|
||||
earlyExit: (media) => media.length >= limit,
|
||||
}),
|
||||
|
||||
...(nextComponent && {
|
||||
matcher: (media: RichBrowseMedia<BrowseMediaMetadata>) =>
|
||||
matcher: (media) =>
|
||||
this._mediaMatcher.match(hass, media, {
|
||||
matchers: nextComponent.ha?.matchers,
|
||||
// Set foldersOnly to true if there are more stages in the path,
|
||||
@@ -187,9 +198,11 @@ export class HAFoldersEngine implements FoldersEngine {
|
||||
},
|
||||
);
|
||||
|
||||
return getViewItemsFromBrowseMediaArray(browseMedia, {
|
||||
const results = getViewItemsFromBrowseMediaArray(browseMedia, {
|
||||
folder: query.folder,
|
||||
path: query.path,
|
||||
});
|
||||
return query.limit ? results.slice(0, query.limit) : results;
|
||||
}
|
||||
|
||||
public generateChildFolderQuery(
|
||||
@@ -202,7 +215,7 @@ export class HAFoldersEngine implements FoldersEngine {
|
||||
}
|
||||
|
||||
// Get the full configured path to find parsers/matchers for this depth.
|
||||
const fullPath = this.getDefaultFolderPathComponents(query.folder.ha);
|
||||
const fullPath = this._getDefaultPathComponents(query.folder.ha);
|
||||
const nextConfiguredComponent = fullPath[query.path.length];
|
||||
|
||||
// Use the configured component's parsers/matchers if available, otherwise
|
||||
@@ -214,4 +227,11 @@ export class HAFoldersEngine implements FoldersEngine {
|
||||
path: [...query.path, { folder, ha }],
|
||||
};
|
||||
}
|
||||
|
||||
public areResultsFresh(resultsTimestamp: Date, query: FolderQuery): boolean {
|
||||
return (
|
||||
!!query &&
|
||||
resultsTimestamp >= sub(new Date(), { seconds: BROWSE_MEDIA_CACHE_SECONDS })
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ import { cloneDeep } from 'lodash-es';
|
||||
import { ConditionState } from '../../conditions/types';
|
||||
import { FolderConfig, FolderConfigWithoutID } from '../../config/schema/folders';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { hasUnsupportedFilters } from '../../query-source.js';
|
||||
import { Endpoint } from '../../types';
|
||||
import { getFolderID } from '../../utils/folder';
|
||||
import { ViewFolder, ViewItem } from '../../view/item';
|
||||
import { ViewItemCapabilities } from '../../view/types';
|
||||
import { CardFoldersAPI } from '../types';
|
||||
@@ -26,7 +28,7 @@ export class FoldersManager {
|
||||
public addFolders(folders: FolderConfigWithoutID[]): void {
|
||||
for (const folder of folders) {
|
||||
const folderNumber = this._folders.size;
|
||||
const id = folder.id ?? `folder/${folderNumber.toString()}`;
|
||||
const id = getFolderID(folder, folderNumber);
|
||||
if (this._folders.has(id)) {
|
||||
throw new FolderInitializationError(
|
||||
localize('error.duplicate_folder_id'),
|
||||
@@ -57,9 +59,9 @@ export class FoldersManager {
|
||||
: this._folders.values().next().value ?? null;
|
||||
}
|
||||
|
||||
public generateDefaultFolderQuery(folder?: FolderConfig): FolderQuery | null {
|
||||
public getDefaultQueryParameters(folder?: FolderConfig): FolderQuery | null {
|
||||
const _folder = folder ?? this.getFolder();
|
||||
return _folder ? this._executor.generateDefaultFolderQuery(_folder) : null;
|
||||
return _folder ? this._executor.getDefaultQueryParameters(_folder) : null;
|
||||
}
|
||||
|
||||
public generateChildFolderQuery(
|
||||
@@ -74,12 +76,20 @@ export class FoldersManager {
|
||||
conditionState?: ConditionState,
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<ViewItem[] | null> {
|
||||
if (hasUnsupportedFilters(query)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
return hass
|
||||
? this._executor.expandFolder(hass, query, conditionState, engineOptions)
|
||||
: null;
|
||||
}
|
||||
|
||||
public areResultsFresh(resultsTimestamp: Date, query: FolderQuery): boolean {
|
||||
return this._executor.areResultsFresh(resultsTimestamp, query);
|
||||
}
|
||||
|
||||
public getItemCapabilities(item: ViewItem): ViewItemCapabilities | null {
|
||||
return this._executor.getItemCapabilities(item);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ConditionState } from '../../conditions/types';
|
||||
import { FolderConfig, HAFolderPathComponent } from '../../config/schema/folders';
|
||||
import { ResolvedMediaCache } from '../../ha/resolved-media';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { BaseQuery, QueryFilters, QuerySource } from '../../query-source';
|
||||
import { Endpoint } from '../../types';
|
||||
import { AdvancedCameraCardError } from '../../types.js';
|
||||
import { ViewFolder, ViewItem } from '../../view/item';
|
||||
@@ -30,12 +31,14 @@ export interface FolderPathComponent extends FolderPathComponentMetadata {
|
||||
folder?: ViewFolder;
|
||||
}
|
||||
|
||||
export interface FolderQuery {
|
||||
export interface FolderQuery extends BaseQuery, QueryFilters {
|
||||
source: QuerySource.Folder;
|
||||
folder: FolderConfig;
|
||||
|
||||
// A trail of paths to navigate back to the "root", with the last path being
|
||||
// the path that this query directly refers to.
|
||||
path: NonEmptyTuple<FolderPathComponent>;
|
||||
limit?: number;
|
||||
}
|
||||
|
||||
// ===============
|
||||
@@ -47,7 +50,8 @@ export interface DownloadHelpers {
|
||||
}
|
||||
|
||||
export interface FoldersEngine {
|
||||
generateDefaultFolderQuery(folder: FolderConfig): FolderQuery | null;
|
||||
getDefaultQueryParameters(folder: FolderConfig): FolderQuery | null;
|
||||
|
||||
generateChildFolderQuery(query: FolderQuery, folder: ViewFolder): FolderQuery | null;
|
||||
|
||||
expandFolder(
|
||||
@@ -63,5 +67,8 @@ export interface FoldersEngine {
|
||||
item: ViewItem,
|
||||
options?: DownloadHelpers,
|
||||
): Promise<Endpoint | null>;
|
||||
|
||||
favorite(hass: HomeAssistant | null, item: ViewItem, favorite: boolean): Promise<void>;
|
||||
|
||||
areResultsFresh(resultsTimestamp: Date, query: FolderQuery): boolean;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user