Merge pull request #992 from dermotduffy/sublavels
Add sub_label ("tags") support
This commit is contained in:
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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 }),
|
||||||
|
|||||||
@@ -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>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) => {
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user