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:
@@ -5,7 +5,11 @@ import { FullscreenManager } from '../card-controller/fullscreen/fullscreen-mana
|
||||
import { MediaPlayerManager } from '../card-controller/media-player-manager';
|
||||
import { MicrophoneManager } from '../card-controller/microphone-manager';
|
||||
import { ViewManager } from '../card-controller/view/view-manager';
|
||||
import { VIEWS_USER_SPECIFIED } from '../config/schema/common/const';
|
||||
import {
|
||||
AdvancedCameraCardView,
|
||||
VIEWS_USER_SPECIFIED,
|
||||
} from '../config/schema/common/const';
|
||||
import { MenuItemBase } from '../config/schema/elements/custom/menu/base';
|
||||
import { MenuItem } from '../config/schema/elements/custom/menu/types';
|
||||
import { AdvancedCameraCardConfig } from '../config/schema/types';
|
||||
import { getEntityTitle } from '../ha/get-entity-title';
|
||||
@@ -19,6 +23,7 @@ import {
|
||||
createMediaPlayerAction,
|
||||
createPTZControlsAction,
|
||||
createPTZMultiAction,
|
||||
createSetReviewAction,
|
||||
createViewAction,
|
||||
isAdvancedCameraCardCustomAction,
|
||||
} from '../utils/action';
|
||||
@@ -27,7 +32,6 @@ import { isBeingCasted } from '../utils/casting';
|
||||
import { getPTZTarget } from '../utils/ptz';
|
||||
import { getStreamCameraID, hasSubstream } from '../utils/substream';
|
||||
import { ViewItemClassifier } from '../view/item-classifier';
|
||||
import { QueryClassifier } from '../view/query-classifier';
|
||||
import { View } from '../view/view';
|
||||
import { getCameraIDsForViewName, isViewSupportedByCamera } from '../view/view-support';
|
||||
|
||||
@@ -77,9 +81,12 @@ export class MenuButtonController {
|
||||
this._getClipsButton(config, cameraManager, foldersManager, options?.view),
|
||||
this._getSnapshotsButton(config, cameraManager, foldersManager, options?.view),
|
||||
this._getRecordingsButton(config, cameraManager, foldersManager, options?.view),
|
||||
this._getReviewsButton(config, cameraManager, foldersManager, options?.view),
|
||||
this._getImageButton(config, cameraManager, foldersManager, options?.view),
|
||||
this._getTimelineButton(config, cameraManager, foldersManager, options?.view),
|
||||
this._getDownloadButton(config, cameraManager, options?.view),
|
||||
this._getInfoButton(config, cameraManager, options?.view),
|
||||
this._getSetReviewButton(config, options?.view),
|
||||
this._getCameraUIButton(config, options?.showCameraUIButton),
|
||||
this._getMicrophoneButton(
|
||||
config,
|
||||
@@ -245,17 +252,45 @@ export class MenuButtonController {
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine if a media button (clips/snapshots) should be shown. These are
|
||||
* hidden by default if reviews are supported.
|
||||
*/
|
||||
protected _shouldShowEventMediaButton(
|
||||
viewName: 'clips' | 'snapshots',
|
||||
buttonConfig: MenuItemBase,
|
||||
cameraManager: CameraManager,
|
||||
foldersManager: FoldersManager,
|
||||
view?: View | null,
|
||||
): boolean {
|
||||
const supportsEventView =
|
||||
view &&
|
||||
isViewSupportedByCamera(viewName, cameraManager, foldersManager, view.camera);
|
||||
const supportsReviewsView =
|
||||
view &&
|
||||
isViewSupportedByCamera('reviews', cameraManager, foldersManager, view.camera);
|
||||
|
||||
// Show if: explicitly enabled OR (supports view AND reviews not supported)
|
||||
return !!supportsEventView && (buttonConfig.enabled || !supportsReviewsView);
|
||||
}
|
||||
|
||||
protected _getClipsButton(
|
||||
config: AdvancedCameraCardConfig,
|
||||
cameraManager: CameraManager,
|
||||
foldersManager: FoldersManager,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
return view &&
|
||||
isViewSupportedByCamera('clips', cameraManager, foldersManager, view.camera)
|
||||
return this._shouldShowEventMediaButton(
|
||||
'clips',
|
||||
config.menu.buttons.clips,
|
||||
cameraManager,
|
||||
foldersManager,
|
||||
view,
|
||||
)
|
||||
? {
|
||||
icon: 'mdi:filmstrip',
|
||||
...config.menu.buttons.clips,
|
||||
enabled: true,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.view.views.clips'),
|
||||
style: view?.is('clips') ? this._getEmphasizedStyle() : {},
|
||||
@@ -271,11 +306,17 @@ export class MenuButtonController {
|
||||
foldersManager: FoldersManager,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
return view &&
|
||||
isViewSupportedByCamera('snapshots', cameraManager, foldersManager, view.camera)
|
||||
return this._shouldShowEventMediaButton(
|
||||
'snapshots',
|
||||
config.menu.buttons.snapshots,
|
||||
cameraManager,
|
||||
foldersManager,
|
||||
view,
|
||||
)
|
||||
? {
|
||||
icon: 'mdi:camera',
|
||||
...config.menu.buttons.snapshots,
|
||||
enabled: true,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.view.views.snapshots'),
|
||||
style: view?.is('snapshots') ? this._getEmphasizedStyle() : {},
|
||||
@@ -305,6 +346,26 @@ export class MenuButtonController {
|
||||
: null;
|
||||
}
|
||||
|
||||
protected _getReviewsButton(
|
||||
config: AdvancedCameraCardConfig,
|
||||
cameraManager: CameraManager,
|
||||
foldersManager: FoldersManager,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
return view &&
|
||||
isViewSupportedByCamera('reviews', cameraManager, foldersManager, view.camera)
|
||||
? {
|
||||
icon: 'mdi:play-box-multiple',
|
||||
...config.menu.buttons.reviews,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.view.views.reviews'),
|
||||
style: view.is('reviews') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createViewAction('reviews'),
|
||||
hold_action: createViewAction('review'),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
protected _getImageButton(
|
||||
config: AdvancedCameraCardConfig,
|
||||
cameraManager: CameraManager,
|
||||
@@ -365,6 +426,49 @@ export class MenuButtonController {
|
||||
return null;
|
||||
}
|
||||
|
||||
protected _getInfoButton(
|
||||
config: AdvancedCameraCardConfig,
|
||||
_cameraManager: CameraManager,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
const selectedItem = view?.queryResults?.getSelectedResult();
|
||||
if (!ViewItemClassifier.isMedia(selectedItem)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
icon: 'mdi:information-outline',
|
||||
...config.menu.buttons.info,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.menu.buttons.info'),
|
||||
tap_action: createGeneralAction('info'),
|
||||
};
|
||||
}
|
||||
|
||||
protected _getSetReviewButton(
|
||||
config: AdvancedCameraCardConfig,
|
||||
view?: View | null,
|
||||
): MenuItem | null {
|
||||
const selectedItem = view?.queryResults?.getSelectedResult();
|
||||
if (!view?.isViewerView() || !ViewItemClassifier.isReview(selectedItem)) {
|
||||
return null;
|
||||
}
|
||||
const isReviewed = selectedItem.isReviewed();
|
||||
if (isReviewed === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return {
|
||||
icon: isReviewed ? 'mdi:check-circle' : 'mdi:check-circle-outline',
|
||||
...config.menu.buttons.set_review,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: isReviewed
|
||||
? localize('common.set_reviews.unreviewed')
|
||||
: localize('common.set_reviews.reviewed'),
|
||||
tap_action: createSetReviewAction(),
|
||||
style: isReviewed ? this._getEmphasizedStyle() : {},
|
||||
};
|
||||
}
|
||||
|
||||
protected _getCameraUIButton(
|
||||
config: AdvancedCameraCardConfig,
|
||||
showCameraUIButton?: boolean,
|
||||
@@ -667,9 +771,8 @@ export class MenuButtonController {
|
||||
}
|
||||
|
||||
if (folders.length === 1) {
|
||||
const isSelected =
|
||||
QueryClassifier.isFolderQuery(view?.query) &&
|
||||
view.query.getQuery()?.folder.id === folders[0][0];
|
||||
const folderID = folders[0][0];
|
||||
const isSelected = !!view?.query?.getFolderQueries(folderID).length;
|
||||
const folder = folders[0][1];
|
||||
|
||||
return {
|
||||
@@ -684,9 +787,7 @@ export class MenuButtonController {
|
||||
}
|
||||
|
||||
const submenuItems = folders.map(([id, folder]) => {
|
||||
const isSelected =
|
||||
QueryClassifier.isFolderQuery(view?.query) &&
|
||||
view.query.getQuery()?.folder.id === id;
|
||||
const isSelected = !!view?.query?.getFolderQueries(id).length;
|
||||
|
||||
return {
|
||||
enabled: true,
|
||||
@@ -736,6 +837,7 @@ export class MenuButtonController {
|
||||
button: MenuItem,
|
||||
options?: MenuButtonControllerOptions,
|
||||
): StyleInfo {
|
||||
// Review
|
||||
for (const actionSet of [
|
||||
button.tap_action,
|
||||
button.double_tap_action,
|
||||
@@ -752,7 +854,9 @@ export class MenuButtonController {
|
||||
VIEWS_USER_SPECIFIED.some(
|
||||
(viewName) =>
|
||||
viewName === action.advanced_camera_card_action &&
|
||||
options?.view?.is(action.advanced_camera_card_action),
|
||||
options?.view?.is(
|
||||
action.advanced_camera_card_action as AdvancedCameraCardView,
|
||||
),
|
||||
) ||
|
||||
(action.advanced_camera_card_action === 'default' &&
|
||||
options?.view?.is(config.view.default)) ||
|
||||
|
||||
Reference in New Issue
Block a user