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:
Dermot Duffy
2026-01-19 13:32:50 -08:00
committed by GitHub
parent 6eb0a87ca8
commit fc32727860
215 changed files with 14491 additions and 6160 deletions
+13 -95
View File
@@ -1,18 +1,11 @@
import { CSSResult, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { CameraManager } from '../../camera-manager/manager.js';
import { FoldersManager } from '../../card-controller/folders/manager.js';
import { ViewItemManager } from '../../card-controller/view/item-manager.js';
import { RemoveContextViewModifier } from '../../card-controller/view/modifiers/remove-context.js';
import { ViewManagerEpoch } from '../../card-controller/view/types.js';
import { dispatchAdvancedCameraCardErrorEvent } from '../../components-lib/message/dispatch.js';
import { HomeAssistant } from '../../ha/types.js';
import { localize } from '../../localize/localize.js';
import thumbnailStyle from '../../scss/thumbnail.scss';
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
import { errorToConsole } from '../../utils/basic.js';
import { ViewItemClassifier } from '../../view/item-classifier.js';
import { ViewItem } from '../../view/item.js';
import './details.js';
import './feature/feature.js';
@@ -56,6 +49,12 @@ export class AdvancedCameraCardThumbnail extends LitElement {
@property({ attribute: true, type: Boolean })
public show_download_control = false;
@property({ attribute: true, type: Boolean })
public show_review_control = false;
@property({ attribute: true, type: Boolean })
public show_info_control = false;
@property({ attribute: false })
public seek?: Date;
@@ -68,62 +67,21 @@ export class AdvancedCameraCardThumbnail extends LitElement {
return;
}
const starClasses = {
star: true,
starred: ViewItemClassifier.isMedia(this.item) && !!this.item?.isFavorite(),
};
const shouldShowTimelineControl =
this.show_timeline_control &&
((ViewItemClassifier.isEvent(this.item) && this.item.getStartTime()) ||
(ViewItemClassifier.isRecording(this.item) &&
this.item.getStartTime() &&
this.item.getEndTime()));
const mediaCapabilities = this.viewItemManager?.getCapabilities(this.item) ?? null;
const shouldShowFavoriteControl =
this.show_favorite_control &&
this.item &&
this.hass &&
mediaCapabilities?.canFavorite;
const shouldShowDownloadControl =
this.show_download_control &&
this.hass &&
this.item.getID() &&
mediaCapabilities?.canDownload;
return html`
<advanced-camera-card-thumbnail-feature
.cameraManager=${this.cameraManager}
.hasDetails=${this.details}
.hass=${this.hass}
.item=${this.item}
.viewItemManager=${this.viewItemManager}
.viewManagerEpoch=${this.viewManagerEpoch}
.show_favorite_control=${this.show_favorite_control}
.show_timeline_control=${this.show_timeline_control}
.show_download_control=${this.show_download_control}
.show_review_control=${this.show_review_control}
.show_info_control=${this.show_info_control}
>
</advanced-camera-card-thumbnail-feature>
${shouldShowFavoriteControl
? html` <advanced-camera-card-icon
class="${classMap(starClasses)}"
title=${localize('thumbnail.retain_indefinitely')}
.icon=${{ icon: this.item.isFavorite() ? 'mdi:star' : 'mdi:star-outline' }}
@click=${async (ev: Event) => {
stopEventFromActivatingCardWideActions(ev);
if (this.hass && this.item) {
try {
await this.viewItemManager?.favorite(
this.item,
!this.item.isFavorite(),
);
} catch (e) {
errorToConsole(e as Error);
return;
}
this.requestUpdate();
}
}}
/></advanced-camera-card-icon>`
: ``}
${this.details
? html`<advanced-camera-card-thumbnail-details
.hass=${this.hass}
@@ -132,46 +90,6 @@ export class AdvancedCameraCardThumbnail extends LitElement {
.seek=${this.seek}
></advanced-camera-card-thumbnail-details>`
: ''}
${shouldShowTimelineControl
? html`<advanced-camera-card-icon
class="timeline"
.icon=${{ icon: 'mdi:target' }}
title=${localize('thumbnail.timeline')}
@click=${(ev: Event) => {
stopEventFromActivatingCardWideActions(ev);
if (!this.viewManagerEpoch || !this.item) {
return;
}
this.viewManagerEpoch.manager.setViewByParameters({
params: {
view: 'timeline',
queryResults: this.viewManagerEpoch?.manager
.getView()
?.queryResults?.clone()
.selectResultIfFound((media) => media === this.item),
},
modifiers: [new RemoveContextViewModifier(['timeline'])],
});
}}
></advanced-camera-card-icon>`
: ''}
${shouldShowDownloadControl
? html` <advanced-camera-card-icon
class="download"
.icon=${{ icon: 'mdi:download' }}
title=${localize('thumbnail.download')}
@click=${async (ev: Event) => {
stopEventFromActivatingCardWideActions(ev);
if (this.hass && this.item) {
try {
this.viewItemManager?.download(this.item);
} catch (error: unknown) {
dispatchAdvancedCameraCardErrorEvent(this, error);
}
}
}}
></advanced-camera-card-icon>`
: ``}
`;
}