Add sub_label support.

This commit is contained in:
Dermot Duffy
2023-03-05 09:52:55 -08:00
parent 44155dea00
commit 1ea961265b
9 changed files with 89 additions and 5 deletions
@@ -448,6 +448,7 @@ export class FrigateCameraManagerEngine
cameras: Array.from(this._getFrigateCameraNamesForCameraIDs(cameras, cameraIDs)), cameras: Array.from(this._getFrigateCameraNamesForCameraIDs(cameras, cameraIDs)),
...(query.what && { labels: Array.from(query.what) }), ...(query.what && { labels: Array.from(query.what) }),
...(query.where && { zones: Array.from(query.where) }), ...(query.where && { zones: Array.from(query.where) }),
...(query.tags && { sub_labels: Array.from(query.tags) }),
...(query.end && { before: Math.floor(query.end.getTime() / 1000) }), ...(query.end && { before: Math.floor(query.end.getTime() / 1000) }),
...(query.start && { after: Math.floor(query.start.getTime() / 1000) }), ...(query.start && { after: Math.floor(query.start.getTime() / 1000) }),
...(query.limit && { limit: query.limit }), ...(query.limit && { limit: query.limit }),
@@ -704,6 +705,7 @@ export class FrigateCameraManagerEngine
cameraID, cameraID,
cameraConfig, cameraConfig,
event, event,
event.sub_label ? this._splitSubLabels(event.sub_label) : undefined,
); );
if (media) { if (media) {
output.push(media); output.push(media);
@@ -795,6 +797,16 @@ export class FrigateCameraManagerEngine
return cameraConfig; return cameraConfig;
} }
protected _splitSubLabels(input: string): string[] {
// A note on Frigate sub_labels: As of Frigate v0.12 sub_labels is a string
// (not an array) per event, but may contain comma-separated values (e.g.
// double-take (https://github.com/jakowenko/double-take) identifying two
// people in the same photo). When we search for multiple sub_labels, the
// integration will comma-join them together, then the Frigate backend will
// do the magic to match exactly or against a comma-separated part.
return input.split(',').map((s) => s.trim());
}
public async getMediaMetadata( public async getMediaMetadata(
hass: HomeAssistant, hass: HomeAssistant,
cameras: CameraConfigs, cameras: CameraConfigs,
@@ -814,6 +826,7 @@ export class FrigateCameraManagerEngine
const what: Set<string> = new Set(); const what: Set<string> = new Set();
const where: Set<string> = new Set(); const where: Set<string> = new Set();
const days: Set<string> = new Set(); const days: Set<string> = new Set();
const tags: Set<string> = new Set();
const instances = this._buildInstanceToCameraIDMapFromQuery( const instances = this._buildInstanceToCameraIDMapFromQuery(
cameras, cameras,
@@ -840,6 +853,9 @@ export class FrigateCameraManagerEngine
if (entry.day) { if (entry.day) {
days.add(entry.day); days.add(entry.day);
} }
if (entry.sub_label) {
this._splitSubLabels(entry.sub_label).forEach(tags.add, tags);
}
} }
}; };
@@ -880,6 +896,7 @@ export class FrigateCameraManagerEngine
...(what.size && { what: what }), ...(what.size && { what: what }),
...(where.size && { where: where }), ...(where.size && { where: where }),
...(days.size && { days: days }), ...(days.size && { days: days }),
...(tags.size && { tags: tags }),
}, },
expiry: add(new Date(), { seconds: MEDIA_METADATA_REQUEST_CACHE_AGE_SECONDS }), expiry: add(new Date(), { seconds: MEDIA_METADATA_REQUEST_CACHE_AGE_SECONDS }),
cached: false, cached: false,
+12
View File
@@ -21,6 +21,7 @@ export class FrigateEventViewMedia extends ViewMedia implements EventViewMedia {
protected _event: FrigateEvent; protected _event: FrigateEvent;
protected _contentID: string; protected _contentID: string;
protected _thumbnail: string; protected _thumbnail: string;
protected _subLabels: string[] | null;
constructor( constructor(
mediaType: ViewMediaType, mediaType: ViewMediaType,
@@ -28,11 +29,17 @@ export class FrigateEventViewMedia extends ViewMedia implements EventViewMedia {
event: FrigateEvent, event: FrigateEvent,
contentID: string, contentID: string,
thumbnail: string, thumbnail: string,
// See 'A note on Frigate sub_labels' in engine-frigate.ts for more
// details about why sub-labels are treated specially. By taking in
// subLabels as an array here, we can keep a single place that splits
// sublabels (`_splitSubLabels` in engine-frigate.ts).
subLabels?: string[],
) { ) {
super(mediaType, cameraID); super(mediaType, cameraID);
this._event = event; this._event = event;
this._contentID = contentID; this._contentID = contentID;
this._thumbnail = thumbnail; this._thumbnail = thumbnail;
this._subLabels = subLabels ?? null;
} }
public hasClip(): boolean { public hasClip(): boolean {
@@ -72,6 +79,9 @@ export class FrigateEventViewMedia extends ViewMedia implements EventViewMedia {
public getScore(): number | null { public getScore(): number | null {
return this._event.top_score; return this._event.top_score;
} }
public getTags(): string[] | null {
return this._subLabels;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
public isGroupableWith(that: EventViewMedia): boolean { public isGroupableWith(that: EventViewMedia): boolean {
@@ -130,6 +140,7 @@ export class FrigateViewMediaFactory {
cameraID: string, cameraID: string,
cameraConfig: CameraConfig, cameraConfig: CameraConfig,
event: FrigateEvent, event: FrigateEvent,
subLabels?: string[],
): FrigateEventViewMedia | null { ): FrigateEventViewMedia | null {
if ( if (
(mediaType === 'clip' && !event.has_clip) || (mediaType === 'clip' && !event.has_clip) ||
@@ -151,6 +162,7 @@ export class FrigateViewMediaFactory {
mediaType === 'clip' ? 'clips' : 'snapshots', mediaType === 'clip' ? 'clips' : 'snapshots',
), ),
getEventThumbnailURL(cameraConfig.frigate.client_id, event), getEventThumbnailURL(cameraConfig.frigate.client_id, event),
subLabels,
); );
} }
+2
View File
@@ -19,6 +19,7 @@ const eventSchema = z.object({
has_snapshot: z.boolean(), has_snapshot: z.boolean(),
id: z.string(), id: z.string(),
label: z.string(), label: z.string(),
sub_label: z.string().nullable(),
start_time: z.number(), start_time: z.number(),
top_score: z.number(), top_score: z.number(),
zones: z.string().array(), zones: z.string().array(),
@@ -68,6 +69,7 @@ export const eventSummarySchema = z
camera: z.string(), camera: z.string(),
day: z.string(), day: z.string(),
label: z.string(), label: z.string(),
sub_label: z.string().nullable(),
zones: z.string().array(), zones: z.string().array(),
}) })
.array(); .array();
+5
View File
@@ -279,6 +279,7 @@ export class CameraManager {
} }
public async getMediaMetadata(hass: HomeAssistant): Promise<MediaMetadata | null> { public async getMediaMetadata(hass: HomeAssistant): Promise<MediaMetadata | null> {
const tags: Set<string> = new Set();
const what: Set<string> = new Set(); const what: Set<string> = new Set();
const where: Set<string> = new Set(); const where: Set<string> = new Set();
const days: Set<string> = new Set(); const days: Set<string> = new Set();
@@ -291,6 +292,9 @@ export class CameraManager {
const results = await this._handleQuery(hass, query); const results = await this._handleQuery(hass, query);
for (const result of results?.values() ?? []) { for (const result of results?.values() ?? []) {
if (result.metadata.tags) {
result.metadata.tags.forEach(tags.add, tags);
}
if (result.metadata.what) { if (result.metadata.what) {
result.metadata.what.forEach(what.add, what); result.metadata.what.forEach(what.add, what);
} }
@@ -306,6 +310,7 @@ export class CameraManager {
return null; return null;
} }
return { return {
...(tags.size && { tags: tags }),
...(what.size && { what: what }), ...(what.size && { what: what }),
...(where.size && { where: where }), ...(where.size && { where: where }),
...(days.size && { days: days }), ...(days.size && { days: days }),
+5 -1
View File
@@ -84,9 +84,10 @@ export type RecordingSegmentsQueryResultsMap = ResultsMap<RecordingSegmentsQuery
export type MediaMetadataQueryResultsMap = ResultsMap<MediaMetadataQuery>; export type MediaMetadataQueryResultsMap = ResultsMap<MediaMetadataQuery>;
export interface MediaMetadata { export interface MediaMetadata {
days?: Set<string>;
tags?: Set<string>;
where?: Set<string>; where?: Set<string>;
what?: Set<string>; what?: Set<string>;
days?: Set<string>;
} }
interface BaseCapabilities { interface BaseCapabilities {
@@ -145,6 +146,9 @@ export interface EventQuery extends MediaQuery {
// Frigate equivalent: label // Frigate equivalent: label
what?: Set<string>; what?: Set<string>;
// Frigate equivalent: sub_label
tags?: Set<string>;
// Frigate equivalent: zone // Frigate equivalent: zone
where?: Set<string>; where?: Set<string>;
} }
+35 -3
View File
@@ -41,12 +41,13 @@ import orderBy from 'lodash-es/orderBy';
import { CardWideConfig } from '../types'; import { CardWideConfig } from '../types';
interface MediaFilterCoreDefaults { interface MediaFilterCoreDefaults {
mediaType?: MediaFilterMediaType;
cameraIDs?: string[]; cameraIDs?: string[];
what?: string[];
where?: string[];
favorite?: MediaFilterCoreFavoriteSelection; favorite?: MediaFilterCoreFavoriteSelection;
mediaType?: MediaFilterMediaType;
what?: string[];
when?: string; when?: string;
where?: string[];
tags?: string[];
} }
export enum MediaFilterCoreFavoriteSelection { export enum MediaFilterCoreFavoriteSelection {
@@ -100,6 +101,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
protected _refWhat: Ref<FrigateCardSelect> = createRef(); protected _refWhat: Ref<FrigateCardSelect> = createRef();
protected _refWhere: Ref<FrigateCardSelect> = createRef(); protected _refWhere: Ref<FrigateCardSelect> = createRef();
protected _refFavorite: Ref<FrigateCardSelect> = createRef(); protected _refFavorite: Ref<FrigateCardSelect> = createRef();
protected _refTags: Ref<FrigateCardSelect> = createRef();
constructor() { constructor() {
super(); super();
@@ -206,11 +208,13 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
) { ) {
const where = getArrayValueAsSet(this._refWhere.value?.value); const where = getArrayValueAsSet(this._refWhere.value?.value);
const what = getArrayValueAsSet(this._refWhat.value?.value); const what = getArrayValueAsSet(this._refWhat.value?.value);
const tags = getArrayValueAsSet(this._refTags.value?.value);
const queries = new EventMediaQueries([ const queries = new EventMediaQueries([
{ {
type: QueryType.Event, type: QueryType.Event,
cameraIDs: cameraIDs, cameraIDs: cameraIDs,
...(tags && { tags: tags }),
...(what && { what: what }), ...(what && { what: what }),
...(where && { where: where }), ...(where && { where: where }),
...(favorite !== null && { favorite: favorite }), ...(favorite !== null && { favorite: favorite }),
@@ -328,6 +332,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
let what: string[] | undefined; let what: string[] | undefined;
let where: string[] | undefined; let where: string[] | undefined;
let favorite: MediaFilterCoreFavoriteSelection | undefined; let favorite: MediaFilterCoreFavoriteSelection | undefined;
let tags: string[] | undefined;
const cameraIDSets = uniqWith( const cameraIDSets = uniqWith(
queries.map((query: DataQuery) => query.cameraIDs), queries.map((query: DataQuery) => query.cameraIDs),
@@ -385,6 +390,13 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
if (whereSets.length === 1 && queries[0].where?.size) { if (whereSets.length === 1 && queries[0].where?.size) {
where = [...queries[0].where]; where = [...queries[0].where];
} }
const tagsSets = uniqWith(
queries.map((query) => query.tags),
isEqual,
);
if (tagsSets.length === 1 && queries[0].tags?.size) {
tags = [...queries[0].tags];
}
} else if (MediaQueriesClassifier.areRecordingQueries(this.view.query)) { } else if (MediaQueriesClassifier.areRecordingQueries(this.view.query)) {
mediaType = MediaFilterMediaType.Recordings; mediaType = MediaFilterMediaType.Recordings;
} }
@@ -395,6 +407,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
...(what && { what: what }), ...(what && { what: what }),
...(where && { where: where }), ...(where && { where: where }),
...(favorite !== undefined && { favorite: favorite }), ...(favorite !== undefined && { favorite: favorite }),
...(tags && { tags: tags })
}; };
} }
@@ -461,6 +474,19 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
> >
</frigate-card-select>` </frigate-card-select>`
: ''} : ''}
${areEvents && this._mediaMetadataController.tagsOptions.length
? html` <frigate-card-select
${ref(this._refTags)}
label=${localize('media_filter.tag')}
placeholder=${localize('media_filter.select_tag')}
clearable
multiple
.options=${this._mediaMetadataController.tagsOptions}
.value=${this._defaults?.tags}
@frigate-card:select:change=${this._valueChangedHandler.bind(this)}
>
</frigate-card-select>`
: ''}
${areEvents && this._mediaMetadataController.whereOptions.length ${areEvents && this._mediaMetadataController.whereOptions.length
? html` <frigate-card-select ? html` <frigate-card-select
${ref(this._refWhere)} ${ref(this._refWhere)}
@@ -500,6 +526,7 @@ export class MediaMetadataController implements ReactiveController {
protected _hass: HomeAssistant; protected _hass: HomeAssistant;
protected _cameraManager: CameraManager; protected _cameraManager: CameraManager;
public tagsOptions: SelectOption[] = [];
public whenOptions: SelectOption[] = []; public whenOptions: SelectOption[] = [];
public whatOptions: SelectOption[] = []; public whatOptions: SelectOption[] = [];
public whereOptions: SelectOption[] = []; public whereOptions: SelectOption[] = [];
@@ -541,6 +568,11 @@ export class MediaMetadataController implements ReactiveController {
.sort() .sort()
.map((where) => ({ value: where, label: prettifyTitle(where) })); .map((where) => ({ value: where, label: prettifyTitle(where) }));
} }
if (metadata.tags) {
this.tagsOptions = [...metadata.tags]
.sort()
.map((tag) => ({ value: tag, label: prettifyTitle(tag) }));
}
if (metadata.days) { if (metadata.days) {
const yearMonths: Set<string> = new Set(); const yearMonths: Set<string> = new Set();
[...metadata.days].forEach((day) => { [...metadata.days].forEach((day) => {
+9 -1
View File
@@ -167,11 +167,13 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement {
const endTime = this.media.getEndTime(); const endTime = this.media.getEndTime();
const what = this.media.getWhat(); const what = this.media.getWhat();
const where = this.media.getWhere(); const where = this.media.getWhere();
const tags = this.media.getTags();
return html` return html`
${what ${what
? html` <div class="title" title=${localize('event.what')}> ? html` <div class="title" title=${localize('event.what')}>
${prettifyTitle(what.join(', '))} ${prettifyTitle(what.join(', ')) +
(tags ? ': ' + prettifyTitle(tags.join(', ')) : '')}
${score ? html`(${(score * 100).toFixed(2) + '%'})` : ''} ${score ? html`(${(score * 100).toFixed(2) + '%'})` : ''}
</div>` </div>`
: ``} : ``}
@@ -200,6 +202,12 @@ export class FrigateCardThumbnailDetailsEvent extends LitElement {
${prettifyTitle(where.join(', '))} ${prettifyTitle(where.join(', '))}
</div>` </div>`
: html``} : html``}
${tags
? html` <div title=${localize('event.tag')}>
<ha-icon .icon=${'mdi:tag'}></ha-icon>
${prettifyTitle(tags.join(', '))}
</div>`
: html``}
${this.seek ${this.seek
? html` <div title=${localize('event.seek')}> ? html` <div title=${localize('event.seek')}>
<ha-icon .icon=${'mdi:clock-fast'}></ha-icon> <ha-icon .icon=${'mdi:clock-fast'}></ha-icon>
+3
View File
@@ -398,6 +398,7 @@
"score": "Score", "score": "Score",
"seek": "Seek", "seek": "Seek",
"start": "Start", "start": "Start",
"tag": "Tag",
"what": "What", "what": "What",
"where": "Where" "where": "Where"
}, },
@@ -418,6 +419,8 @@
"select_what": "Select what...", "select_what": "Select what...",
"select_when": "Select when...", "select_when": "Select when...",
"select_where": "Select where...", "select_where": "Select where...",
"select_tag": "Select tag...",
"tag": "Tag",
"what": "What", "what": "What",
"when": "When", "when": "When",
"whens": { "whens": {
+1
View File
@@ -58,6 +58,7 @@ export class ViewMedia {
export interface EventViewMedia extends ViewMedia { export interface EventViewMedia extends ViewMedia {
getScore(): number | null; getScore(): number | null;
getWhat(): string[] | null; getWhat(): string[] | null;
getTags(): string[] | null;
isGroupableWith(that: EventViewMedia): boolean; isGroupableWith(that: EventViewMedia): boolean;
hasClip(): boolean | null; hasClip(): boolean | null;
} }