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:
@@ -53,7 +53,7 @@ export class ActionsManager implements ActionsExecutor {
|
||||
let specificActions: Actions | undefined = undefined;
|
||||
if (view?.is('live')) {
|
||||
specificActions = config?.live.actions;
|
||||
} else if (view?.isMediaGalleryView()) {
|
||||
} else if (view?.isGalleryView()) {
|
||||
specificActions = config?.media_gallery?.actions;
|
||||
} else if (view?.isViewerView()) {
|
||||
specificActions = config?.media_viewer.actions;
|
||||
|
||||
@@ -21,26 +21,26 @@ export class BaseAction<T extends ActionConfig> implements Action {
|
||||
|
||||
protected _shouldSeekConfirmation(api: CardActionsAPI): boolean {
|
||||
const hass = api.getHASSManager().getHASS();
|
||||
const action: ActionConfig = this._action;
|
||||
|
||||
return (
|
||||
(typeof this._action.confirmation === 'boolean' && this._action.confirmation) ||
|
||||
(typeof this._action.confirmation === 'object' &&
|
||||
(!this._action.confirmation.exemptions ||
|
||||
!this._action.confirmation.exemptions.some(
|
||||
(entry) => entry.user === hass?.user.id,
|
||||
)))
|
||||
(typeof action.confirmation === 'boolean' && action.confirmation) ||
|
||||
(typeof action.confirmation === 'object' &&
|
||||
(!action.confirmation.exemptions ||
|
||||
!action.confirmation.exemptions.some((entry) => entry.user === hass?.user.id)))
|
||||
);
|
||||
}
|
||||
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
if (this._shouldSeekConfirmation(api)) {
|
||||
const actionName = isAdvancedCameraCardCustomAction(this._action)
|
||||
? this._action.advanced_camera_card_action
|
||||
: this._action.action;
|
||||
const action: ActionConfig = this._action;
|
||||
const baseAction = action.action;
|
||||
const actionName = isAdvancedCameraCardCustomAction(action)
|
||||
? action.advanced_camera_card_action
|
||||
: baseAction;
|
||||
const text =
|
||||
(typeof this._action.confirmation === 'object'
|
||||
? this._action.confirmation.text
|
||||
: null) ?? `${localize('actions.confirmation')}: ${actionName}`;
|
||||
(typeof action.confirmation === 'object' ? action.confirmation.text : null) ??
|
||||
`${localize('actions.confirmation')}: ${actionName}`;
|
||||
if (!confirm(text)) {
|
||||
throw new ActionAbortError(localize('actions.abort'));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { MediaDetailsController } from '../../../components-lib/media/details-controller';
|
||||
import { GeneralActionConfig } from '../../../config/schema/actions/custom/general';
|
||||
import { ViewItemClassifier } from '../../../view/item-classifier';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { AdvancedCameraCardAction } from './base';
|
||||
|
||||
export class InfoAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
const item = api.getViewManager().getView()?.queryResults?.getSelectedResult();
|
||||
if (!ViewItemClassifier.isMedia(item)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new MediaDetailsController();
|
||||
controller.calculate(api.getCameraManager(), item);
|
||||
|
||||
api.getOverlayMessageManager().setMessage(
|
||||
controller.getMessage({
|
||||
hass: api.getHASSManager().getHASS() ?? undefined,
|
||||
viewItemManager: api.getViewItemManager(),
|
||||
viewManagerEpoch: api.getViewManager().getEpoch(),
|
||||
capabilities: api.getViewItemManager().getCapabilities(item),
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { SetReviewActionConfig } from '../../../config/schema/actions/custom/set-review';
|
||||
import { ViewItemClassifier } from '../../../view/item-classifier';
|
||||
import { CardActionsAPI } from '../../types';
|
||||
import { AdvancedCameraCardAction } from './base';
|
||||
|
||||
export class SetReviewAction extends AdvancedCameraCardAction<SetReviewActionConfig> {
|
||||
public async execute(api: CardActionsAPI): Promise<void> {
|
||||
await super.execute(api);
|
||||
|
||||
const view = api.getViewManager().getView();
|
||||
const queryResults = view?.queryResults;
|
||||
const item = queryResults?.getSelectedResult();
|
||||
|
||||
if (!ViewItemClassifier.isReview(item) || !queryResults) {
|
||||
return;
|
||||
}
|
||||
|
||||
const targetReviewedState = this._action.reviewed ?? !item.isReviewed();
|
||||
|
||||
await api.getViewItemManager().reviewMedia(item, targetReviewedState);
|
||||
|
||||
// Clone the item to ensure Lit detects the change.
|
||||
// Test-case: Setting a media item reviewed via the menu, should update the
|
||||
// reviewed state in a thumbnail.
|
||||
const clonedItem = item.clone();
|
||||
clonedItem.setReviewed(targetReviewedState);
|
||||
|
||||
api.getViewManager().setViewByParameters({
|
||||
params: {
|
||||
queryResults: queryResults.clone().replaceItem(item, clonedItem),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,9 +9,10 @@ import { CustomAction } from './actions/custom';
|
||||
import { DefaultAction } from './actions/default';
|
||||
import { DisplayModeSelectAction } from './actions/display-mode-select';
|
||||
import { DownloadAction } from './actions/download';
|
||||
import { ExpandAction } from './actions/expand';
|
||||
import { EffectAction } from './actions/effect';
|
||||
import { ExpandAction } from './actions/expand';
|
||||
import { FullscreenAction } from './actions/fullscreen';
|
||||
import { InfoAction } from './actions/info';
|
||||
import { InternalCallbackAction } from './actions/internal-callback';
|
||||
import { LogAction } from './actions/log';
|
||||
import { MediaPlayerAction } from './actions/media-player';
|
||||
@@ -33,6 +34,7 @@ import { PTZDigitalAction } from './actions/ptz-digital';
|
||||
import { PTZMultiAction } from './actions/ptz-multi';
|
||||
import { ReloadAction } from './actions/reload';
|
||||
import { ScreenshotAction } from './actions/screenshot';
|
||||
import { SetReviewAction } from './actions/set-review';
|
||||
import { SleepAction } from './actions/sleep';
|
||||
import { StatusBarAction } from './actions/status-bar';
|
||||
import { SubstreamOffAction } from './actions/substream-off';
|
||||
@@ -93,6 +95,8 @@ export class ActionFactory {
|
||||
case 'live':
|
||||
case 'recording':
|
||||
case 'recordings':
|
||||
case 'review':
|
||||
case 'reviews':
|
||||
case 'snapshot':
|
||||
case 'snapshots':
|
||||
case 'timeline':
|
||||
@@ -110,6 +114,8 @@ export class ActionFactory {
|
||||
return new ExpandAction(context, action, options?.config);
|
||||
case 'fullscreen':
|
||||
return new FullscreenAction(context, action, options?.config);
|
||||
case 'info':
|
||||
return new InfoAction(context, action, options?.config);
|
||||
case 'menu_toggle':
|
||||
return new MenuToggleAction(context, action, options?.config);
|
||||
case 'camera_select':
|
||||
@@ -156,6 +162,8 @@ export class ActionFactory {
|
||||
return new StatusBarAction(context, action, options?.config);
|
||||
case 'reload':
|
||||
return new ReloadAction(context, action, options?.config);
|
||||
case 'set_review':
|
||||
return new SetReviewAction(context, action, options?.config);
|
||||
case INTERNAL_CALLBACK_ACTION:
|
||||
return new InternalCallbackAction(context, action, options?.config);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user