feat: Add severity media filter (#2323)
This commit is contained in:
@@ -6,12 +6,12 @@ import { CameraConfig } from '../../config/schema/cameras';
|
|||||||
import { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
|
import { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||||
import { HomeAssistant } from '../../ha/types';
|
import { HomeAssistant } from '../../ha/types';
|
||||||
import { localize } from '../../localize/localize';
|
import { localize } from '../../localize/localize';
|
||||||
|
import { SEVERITIES } from '../../severity';
|
||||||
import {
|
import {
|
||||||
CapabilitiesRaw,
|
CapabilitiesRaw,
|
||||||
Endpoint,
|
Endpoint,
|
||||||
PTZCapabilities,
|
PTZCapabilities,
|
||||||
PTZMovementType,
|
PTZMovementType,
|
||||||
SEVERITIES,
|
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import { errorToConsole } from '../../utils/basic';
|
import { errorToConsole } from '../../utils/basic';
|
||||||
import { Camera, CameraInitializationOptions } from '../camera';
|
import { Camera, CameraInitializationOptions } from '../camera';
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ import { FrigateViewMediaClassifier } from './media-classifier';
|
|||||||
import {
|
import {
|
||||||
NativeFrigateEventQuery,
|
NativeFrigateEventQuery,
|
||||||
NativeFrigateRecordingSegmentsQuery,
|
NativeFrigateRecordingSegmentsQuery,
|
||||||
NativeFrigateReviewQuery,
|
|
||||||
getEventSummary,
|
getEventSummary,
|
||||||
getEvents,
|
getEvents,
|
||||||
getRecordingSegments,
|
getRecordingSegments,
|
||||||
@@ -86,7 +85,7 @@ const RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
|||||||
const MEDIA_METADATA_REQUEST_CACHE_AGE_SECONDS = 60;
|
const MEDIA_METADATA_REQUEST_CACHE_AGE_SECONDS = 60;
|
||||||
const REVIEW_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
const REVIEW_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
||||||
|
|
||||||
class FrigateQueryResultsClassifier {
|
export class FrigateQueryResultsClassifier {
|
||||||
public static isFrigateEventQueryResults(
|
public static isFrigateEventQueryResults(
|
||||||
results: QueryResults,
|
results: QueryResults,
|
||||||
): results is FrigateEventQueryResults {
|
): results is FrigateEventQueryResults {
|
||||||
@@ -484,7 +483,14 @@ export class FrigateCameraManagerEngine
|
|||||||
query: ReviewQuery,
|
query: ReviewQuery,
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<ReviewQueryResultsMap | null> {
|
): Promise<ReviewQueryResultsMap | null> {
|
||||||
if (hasUnsupportedFilters(query, { what: true, where: true, reviewed: true })) {
|
if (
|
||||||
|
hasUnsupportedFilters(query, {
|
||||||
|
what: true,
|
||||||
|
where: true,
|
||||||
|
reviewed: true,
|
||||||
|
severity: true,
|
||||||
|
})
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -505,23 +511,36 @@ export class FrigateCameraManagerEngine
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const nativeQuery: NativeFrigateReviewQuery = {
|
const severities = query.severity?.size ? Array.from(query.severity) : [undefined];
|
||||||
instance_id: instanceID,
|
|
||||||
cameras: Array.from(this._getFrigateCameraNamesForCameraIDs(store, cameraIDs)),
|
// Frigate only supports querying for a single severity, so we generate
|
||||||
...(query.what && { labels: Array.from(query.what) }),
|
// multiple queries and combine the results.
|
||||||
...(query.where && { zones: Array.from(query.where) }),
|
const reviewPromises = severities
|
||||||
...(query.end && { before: Math.floor(query.end.getTime() / 1000) }),
|
// Frigate does not support a 'low' severity.
|
||||||
...(query.start && { after: Math.floor(query.start.getTime() / 1000) }),
|
.filter((severity) => severity !== 'low')
|
||||||
...(query.limit && { limit: query.limit }),
|
.map((severity) => (!!severity ? FRIGATE_SEVERITY_MAP[severity] : undefined))
|
||||||
...(query.severity && { severity: FRIGATE_SEVERITY_MAP[query.severity] }),
|
.map(
|
||||||
...(query.reviewed !== undefined && { reviewed: query.reviewed }),
|
async (frigateSeverity) =>
|
||||||
};
|
await getReviews(hass, {
|
||||||
|
instance_id: instanceID,
|
||||||
|
cameras: Array.from(
|
||||||
|
this._getFrigateCameraNamesForCameraIDs(store, cameraIDs),
|
||||||
|
),
|
||||||
|
...(query.what && { labels: Array.from(query.what) }),
|
||||||
|
...(query.where && { zones: Array.from(query.where) }),
|
||||||
|
...(query.end && { before: Math.floor(query.end.getTime() / 1000) }),
|
||||||
|
...(query.start && { after: Math.floor(query.start.getTime() / 1000) }),
|
||||||
|
...(query.limit && { limit: query.limit }),
|
||||||
|
severity: frigateSeverity,
|
||||||
|
...(query.reviewed !== undefined && { reviewed: query.reviewed }),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
const result: FrigateReviewQueryResults = {
|
const result: FrigateReviewQueryResults = {
|
||||||
type: QueryResultsType.Review,
|
type: QueryResultsType.Review,
|
||||||
engine: Engine.Frigate,
|
engine: Engine.Frigate,
|
||||||
instanceID: instanceID,
|
instanceID: instanceID,
|
||||||
reviews: await getReviews(hass, nativeQuery),
|
reviews: (await Promise.all(reviewPromises)).flat(),
|
||||||
expiry: add(new Date(), { seconds: REVIEW_REQUEST_CACHE_MAX_AGE_SECONDS }),
|
expiry: add(new Date(), { seconds: REVIEW_REQUEST_CACHE_MAX_AGE_SECONDS }),
|
||||||
cached: false,
|
cached: false,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { fromUnixTime } from 'date-fns';
|
import { fromUnixTime } from 'date-fns';
|
||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { CameraConfig } from '../../config/schema/cameras';
|
import { CameraConfig } from '../../config/schema/cameras';
|
||||||
import { Severity } from '../../types';
|
import { Severity } from '../../severity';
|
||||||
import {
|
import {
|
||||||
EventViewMedia,
|
EventViewMedia,
|
||||||
RecordingViewMedia,
|
RecordingViewMedia,
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ export const getPTZInfo = async (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface NativeFrigateReviewQuery {
|
interface NativeFrigateReviewQuery {
|
||||||
instance_id: string;
|
instance_id: string;
|
||||||
cameras?: string[];
|
cameras?: string[];
|
||||||
labels?: string[];
|
labels?: string[];
|
||||||
|
|||||||
@@ -146,7 +146,6 @@ export interface FrigateRecordingSegmentsQueryResults
|
|||||||
export const FRIGATE_SEVERITY_MAP = {
|
export const FRIGATE_SEVERITY_MAP = {
|
||||||
high: 'alert',
|
high: 'alert',
|
||||||
medium: 'detection',
|
medium: 'detection',
|
||||||
low: 'significant_motion',
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export type FrigateReviewSeverity =
|
export type FrigateReviewSeverity =
|
||||||
@@ -170,7 +169,7 @@ const frigateReviewDataSchema = z.object({
|
|||||||
const frigateReviewSchema = z.object({
|
const frigateReviewSchema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
camera: z.string(),
|
camera: z.string(),
|
||||||
severity: z.enum(['alert', 'detection', 'significant_motion']),
|
severity: z.enum(['alert', 'detection']),
|
||||||
start_time: z.number(),
|
start_time: z.number(),
|
||||||
end_time: z.number().nullable(),
|
end_time: z.number().nullable(),
|
||||||
thumb_path: z.string().nullable(),
|
thumb_path: z.string().nullable(),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { toZonedTime } from 'date-fns-tz';
|
import { toZonedTime } from 'date-fns-tz';
|
||||||
import { CameraConfig } from '../../config/schema/cameras';
|
import { CameraConfig } from '../../config/schema/cameras';
|
||||||
import { Severity } from '../../types';
|
import { Severity } from '../../severity';
|
||||||
import { formatDateAndTime, prettifyTitle } from '../../utils/basic';
|
import { formatDateAndTime, prettifyTitle } from '../../utils/basic';
|
||||||
import {
|
import {
|
||||||
FrigateEvent,
|
FrigateEvent,
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { ExpiringEqualityCache } from '../cache/expiring-cache';
|
|||||||
import { SSLCiphers } from '../config/schema/cameras';
|
import { SSLCiphers } from '../config/schema/cameras';
|
||||||
import { AdvancedCameraCardView } from '../config/schema/common/const';
|
import { AdvancedCameraCardView } from '../config/schema/common/const';
|
||||||
import { BaseQuery, QueryFilters, QuerySource } from '../query-source';
|
import { BaseQuery, QueryFilters, QuerySource } from '../query-source';
|
||||||
import { CapabilityKey, Endpoint, Icon, Severity } from '../types';
|
import { CapabilityKey, Endpoint, Icon } from '../types';
|
||||||
import { ViewMedia } from '../view/item';
|
import { ViewMedia } from '../view/item';
|
||||||
|
|
||||||
// ====
|
// ====
|
||||||
@@ -252,8 +252,6 @@ export interface MediaMetadataQueryResults extends QueryResults {
|
|||||||
|
|
||||||
export interface ReviewQuery extends MediaQuery {
|
export interface ReviewQuery extends MediaQuery {
|
||||||
type: QueryType.Review;
|
type: QueryType.Review;
|
||||||
|
|
||||||
severity?: Severity;
|
|
||||||
}
|
}
|
||||||
export type PartialReviewQuery = Partial<ReviewQuery>;
|
export type PartialReviewQuery = Partial<ReviewQuery>;
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import {
|
|||||||
sub,
|
sub,
|
||||||
} from 'date-fns';
|
} from 'date-fns';
|
||||||
import { LitElement } from 'lit';
|
import { LitElement } from 'lit';
|
||||||
import { isEqual, orderBy, uniqWith } from 'lodash-es';
|
import { isEqual, orderBy } from 'lodash-es';
|
||||||
import { CameraManager, CameraQueryClassifier } from '../camera-manager/manager';
|
import { CameraManager, CameraQueryClassifier } from '../camera-manager/manager';
|
||||||
import { DateRange, PartialDateRange } from '../camera-manager/range';
|
import { DateRange, PartialDateRange } from '../camera-manager/range';
|
||||||
import {
|
import {
|
||||||
@@ -25,6 +25,7 @@ import { ViewManagerInterface } from '../card-controller/view/types';
|
|||||||
import { SelectOption, SelectValues } from '../components/select';
|
import { SelectOption, SelectValues } from '../components/select';
|
||||||
import { CardWideConfig } from '../config/schema/types';
|
import { CardWideConfig } from '../config/schema/types';
|
||||||
import { localize } from '../localize/localize';
|
import { localize } from '../localize/localize';
|
||||||
|
import { SEVERITIES, Severity } from '../severity';
|
||||||
import { ViewMediaType } from '../types';
|
import { ViewMediaType } from '../types';
|
||||||
import { errorToConsole, formatDate, prettifyTitle } from '../utils/basic';
|
import { errorToConsole, formatDate, prettifyTitle } from '../utils/basic';
|
||||||
import { UnifiedQueryBuilder } from '../view/unified-query-builder';
|
import { UnifiedQueryBuilder } from '../view/unified-query-builder';
|
||||||
@@ -38,6 +39,7 @@ export interface MediaFilterCoreDefaults {
|
|||||||
when?: string;
|
when?: string;
|
||||||
where?: string[];
|
where?: string[];
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
severity?: Severity[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum MediaFilterCoreFavoriteSelection {
|
export enum MediaFilterCoreFavoriteSelection {
|
||||||
@@ -80,6 +82,7 @@ export class MediaFilterController {
|
|||||||
protected _tagsOptions: SelectOption[] = [];
|
protected _tagsOptions: SelectOption[] = [];
|
||||||
protected _favoriteOptions: SelectOption[];
|
protected _favoriteOptions: SelectOption[];
|
||||||
protected _reviewedOptions: SelectOption[];
|
protected _reviewedOptions: SelectOption[];
|
||||||
|
protected _severityOptions: SelectOption[];
|
||||||
|
|
||||||
protected _defaults: MediaFilterCoreDefaults | null = null;
|
protected _defaults: MediaFilterCoreDefaults | null = null;
|
||||||
protected _viewManager: ViewManagerInterface | null = null;
|
protected _viewManager: ViewManagerInterface | null = null;
|
||||||
@@ -125,6 +128,10 @@ export class MediaFilterController {
|
|||||||
label: localize('media_filter.not_reviewed'),
|
label: localize('media_filter.not_reviewed'),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
this._severityOptions = SEVERITIES.map((severity) => ({
|
||||||
|
value: severity,
|
||||||
|
label: localize(`common.severities.${severity}`),
|
||||||
|
}));
|
||||||
this._staticWhenOptions = [
|
this._staticWhenOptions = [
|
||||||
{
|
{
|
||||||
value: MediaFilterCoreWhen.Today,
|
value: MediaFilterCoreWhen.Today,
|
||||||
@@ -174,6 +181,9 @@ export class MediaFilterController {
|
|||||||
public getReviewedOptions(): SelectOption[] {
|
public getReviewedOptions(): SelectOption[] {
|
||||||
return this._reviewedOptions;
|
return this._reviewedOptions;
|
||||||
}
|
}
|
||||||
|
public getSeverityOptions(): SelectOption[] {
|
||||||
|
return this._severityOptions;
|
||||||
|
}
|
||||||
public getDefaults(): MediaFilterCoreDefaults | null {
|
public getDefaults(): MediaFilterCoreDefaults | null {
|
||||||
return this._defaults;
|
return this._defaults;
|
||||||
}
|
}
|
||||||
@@ -198,6 +208,7 @@ export class MediaFilterController {
|
|||||||
where?: SelectValues;
|
where?: SelectValues;
|
||||||
what?: SelectValues;
|
what?: SelectValues;
|
||||||
tags?: SelectValues;
|
tags?: SelectValues;
|
||||||
|
severity?: SelectValues;
|
||||||
},
|
},
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
_ev?: unknown,
|
_ev?: unknown,
|
||||||
@@ -221,6 +232,7 @@ export class MediaFilterController {
|
|||||||
const where = getArrayValueAsSet(values.where);
|
const where = getArrayValueAsSet(values.where);
|
||||||
const what = getArrayValueAsSet(values.what);
|
const what = getArrayValueAsSet(values.what);
|
||||||
const tags = getArrayValueAsSet(values.tags);
|
const tags = getArrayValueAsSet(values.tags);
|
||||||
|
const severity = getArrayValueAsSet<Severity>(values.severity);
|
||||||
const limit = cardWideConfig.performance?.features.media_chunk_size;
|
const limit = cardWideConfig.performance?.features.media_chunk_size;
|
||||||
|
|
||||||
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
|
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
|
||||||
@@ -236,6 +248,7 @@ export class MediaFilterController {
|
|||||||
...(tags && { tags }),
|
...(tags && { tags }),
|
||||||
...(what && { what }),
|
...(what && { what }),
|
||||||
...(where && { where }),
|
...(where && { where }),
|
||||||
|
...(severity && { severity }),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -280,6 +293,7 @@ export class MediaFilterController {
|
|||||||
let favorite: MediaFilterCoreFavoriteSelection | undefined;
|
let favorite: MediaFilterCoreFavoriteSelection | undefined;
|
||||||
let reviewed: MediaFilterCoreReviewedSelection | undefined;
|
let reviewed: MediaFilterCoreReviewedSelection | undefined;
|
||||||
let tags: string[] | undefined;
|
let tags: string[] | undefined;
|
||||||
|
let severity: Severity[] | undefined;
|
||||||
|
|
||||||
const cameraIDsFromQuery = query.getAllCameraIDs();
|
const cameraIDsFromQuery = query.getAllCameraIDs();
|
||||||
|
|
||||||
@@ -326,44 +340,38 @@ export class MediaFilterController {
|
|||||||
mediaTypes.push(MediaFilterMediaType.Reviews);
|
mediaTypes.push(MediaFilterMediaType.Reviews);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventQueries.length > 0) {
|
const whatSets = eventQueries.map((q) => q.what);
|
||||||
const whatSets = uniqWith(
|
if (this._hasSingleUniqueValue(whatSets) && whatSets[0]?.size) {
|
||||||
eventQueries.map((q) => q.what),
|
what = [...whatSets[0]];
|
||||||
isEqual,
|
}
|
||||||
);
|
|
||||||
if (whatSets.length === 1 && eventQueries[0].what?.size) {
|
const whereSets = eventQueries.map((q) => q.where);
|
||||||
what = [...eventQueries[0].what];
|
if (this._hasSingleUniqueValue(whereSets) && whereSets[0]?.size) {
|
||||||
}
|
where = [...whereSets[0]];
|
||||||
const whereSets = uniqWith(
|
}
|
||||||
eventQueries.map((q) => q.where),
|
|
||||||
isEqual,
|
const tagsSets = eventQueries.map((q) => q.tags);
|
||||||
);
|
if (this._hasSingleUniqueValue(tagsSets) && tagsSets[0]?.size) {
|
||||||
if (whereSets.length === 1 && eventQueries[0].where?.size) {
|
tags = [...tagsSets[0]];
|
||||||
where = [...eventQueries[0].where];
|
|
||||||
}
|
|
||||||
const tagsSets = uniqWith(
|
|
||||||
eventQueries.map((q) => q.tags),
|
|
||||||
isEqual,
|
|
||||||
);
|
|
||||||
if (tagsSets.length === 1 && eventQueries[0].tags?.size) {
|
|
||||||
tags = [...eventQueries[0].tags];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract reviewed from review queries (only if explicitly set to true/false)
|
// Extract reviewed from review queries (only if explicitly set to true/false)
|
||||||
const reviewQueries = query.getMediaQueries<ReviewQuery>({ type: QueryType.Review });
|
const reviewQueries = query.getMediaQueries<ReviewQuery>({ type: QueryType.Review });
|
||||||
if (reviewQueries.length > 0) {
|
const reviewedValues = new Set(reviewQueries.map((q) => q.reviewed));
|
||||||
const reviewedValues = new Set(reviewQueries.map((q) => q.reviewed));
|
if (reviewedValues.size === 1) {
|
||||||
if (reviewedValues.size === 1) {
|
const rev = [...reviewedValues][0];
|
||||||
const rev = [...reviewedValues][0];
|
if (rev !== undefined) {
|
||||||
if (rev !== undefined) {
|
reviewed = rev
|
||||||
reviewed = rev
|
? MediaFilterCoreReviewedSelection.Reviewed
|
||||||
? MediaFilterCoreReviewedSelection.Reviewed
|
: MediaFilterCoreReviewedSelection.NotReviewed;
|
||||||
: MediaFilterCoreReviewedSelection.NotReviewed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const severitySets = reviewQueries.map((q) => q.severity);
|
||||||
|
if (this._hasSingleUniqueValue(severitySets) && severitySets[0]?.size) {
|
||||||
|
severity = [...severitySets[0]];
|
||||||
|
}
|
||||||
|
|
||||||
this._defaults = {
|
this._defaults = {
|
||||||
...(mediaTypes.length && { mediaTypes }),
|
...(mediaTypes.length && { mediaTypes }),
|
||||||
...(cameraIDs && { cameraIDs }),
|
...(cameraIDs && { cameraIDs }),
|
||||||
@@ -372,6 +380,7 @@ export class MediaFilterController {
|
|||||||
...(favorite !== undefined && { favorite }),
|
...(favorite !== undefined && { favorite }),
|
||||||
...(reviewed !== undefined && { reviewed }),
|
...(reviewed !== undefined && { reviewed }),
|
||||||
...(tags && { tags }),
|
...(tags && { tags }),
|
||||||
|
...(severity && { severity }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -489,4 +498,11 @@ export class MediaFilterController {
|
|||||||
return this._stringToDateRange(values.selected);
|
return this._stringToDateRange(values.selected);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected _hasSingleUniqueValue(sets: (Set<unknown> | undefined)[]): boolean {
|
||||||
|
if (sets.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return sets.every((s) => isEqual(s, sets[0]));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ class AdvancedCameraCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
protected _refWhere: Ref<AdvancedCameraCardSelect> = createRef();
|
protected _refWhere: Ref<AdvancedCameraCardSelect> = createRef();
|
||||||
protected _refFavorite: Ref<AdvancedCameraCardSelect> = createRef();
|
protected _refFavorite: Ref<AdvancedCameraCardSelect> = createRef();
|
||||||
protected _refReviewed: Ref<AdvancedCameraCardSelect> = createRef();
|
protected _refReviewed: Ref<AdvancedCameraCardSelect> = createRef();
|
||||||
|
protected _refSeverity: Ref<AdvancedCameraCardSelect> = createRef();
|
||||||
protected _refTags: Ref<AdvancedCameraCardSelect> = createRef();
|
protected _refTags: Ref<AdvancedCameraCardSelect> = createRef();
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
@@ -119,6 +120,7 @@ class AdvancedCameraCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
where: this._refWhere.value?.value ?? undefined,
|
where: this._refWhere.value?.value ?? undefined,
|
||||||
what: this._refWhat.value?.value ?? undefined,
|
what: this._refWhat.value?.value ?? undefined,
|
||||||
tags: this._refTags.value?.value ?? undefined,
|
tags: this._refTags.value?.value ?? undefined,
|
||||||
|
severity: this._refSeverity.value?.value ?? undefined,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -260,7 +262,22 @@ class AdvancedCameraCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
clearable
|
clearable
|
||||||
@advanced-camera-card:select:change=${() => valueChange()}
|
@advanced-camera-card:select:change=${() => valueChange()}
|
||||||
>
|
>
|
||||||
</advanced-camera-card-select>`;
|
</advanced-camera-card-select>
|
||||||
|
${this._mediaFilterController.getSeverityOptions().length
|
||||||
|
? html`
|
||||||
|
<advanced-camera-card-select
|
||||||
|
${ref(this._refSeverity)}
|
||||||
|
label=${localize('common.severity')}
|
||||||
|
placeholder=${localize('media_filter.select_severity')}
|
||||||
|
.options=${this._mediaFilterController.getSeverityOptions()}
|
||||||
|
.initialValue=${defaults?.severity}
|
||||||
|
clearable
|
||||||
|
multiple
|
||||||
|
@advanced-camera-card:select:change=${() => valueChange()}
|
||||||
|
>
|
||||||
|
</advanced-camera-card-select>
|
||||||
|
`
|
||||||
|
: ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
static get styles(): CSSResultGroup {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import overlayMessageStyle from '../scss/overlay-message.scss';
|
import overlayMessageStyle from '../scss/overlay-message.scss';
|
||||||
import { MetadataField, OverlayMessage, OverlayMessageControl } from '../types.js';
|
import { MetadataField, OverlayMessage, OverlayMessageControl } from '../types.js';
|
||||||
@@ -90,8 +91,13 @@ export class AdvancedCameraCardOverlayMessage extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _renderDetail(detail: MetadataField, isHeading = false): TemplateResult {
|
protected _renderDetail(detail: MetadataField, isHeading = false): TemplateResult {
|
||||||
|
const classes = {
|
||||||
|
detail: true,
|
||||||
|
heading: isHeading,
|
||||||
|
[`emphasis-${detail.emphasis}`]: !!detail.emphasis,
|
||||||
|
};
|
||||||
return html`
|
return html`
|
||||||
<div class="detail ${isHeading ? 'heading' : ''}">
|
<div class="${classMap(classes)}">
|
||||||
${detail.icon
|
${detail.icon
|
||||||
? html`<advanced-camera-card-icon
|
? html`<advanced-camera-card-icon
|
||||||
title=${detail.hint ?? ''}
|
title=${detail.hint ?? ''}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { SEVERITIES } from '../../../types';
|
import { SEVERITIES } from '../../../severity';
|
||||||
|
|
||||||
export const severitySchema = z.enum(SEVERITIES);
|
export const severitySchema = z.enum(SEVERITIES);
|
||||||
|
|||||||
@@ -777,6 +777,7 @@
|
|||||||
"select_favorite": "Select favorite...",
|
"select_favorite": "Select favorite...",
|
||||||
"select_media_type": "Select media type...",
|
"select_media_type": "Select media type...",
|
||||||
"select_reviewed": "Select reviewed...",
|
"select_reviewed": "Select reviewed...",
|
||||||
|
"select_severity": "Select severity...",
|
||||||
"select_tag": "Select tag...",
|
"select_tag": "Select tag...",
|
||||||
"select_what": "Select what...",
|
"select_what": "Select what...",
|
||||||
"select_when": "Select when...",
|
"select_when": "Select when...",
|
||||||
|
|||||||
+6
-1
@@ -1,3 +1,5 @@
|
|||||||
|
import { Severity } from './severity';
|
||||||
|
|
||||||
export enum QuerySource {
|
export enum QuerySource {
|
||||||
// Camera queries are handled by CameraManager.
|
// Camera queries are handled by CameraManager.
|
||||||
Camera = 'camera',
|
Camera = 'camera',
|
||||||
@@ -20,6 +22,7 @@ export interface QueryFilters {
|
|||||||
what?: Set<string>;
|
what?: Set<string>;
|
||||||
where?: Set<string>;
|
where?: Set<string>;
|
||||||
reviewed?: boolean;
|
reviewed?: boolean;
|
||||||
|
severity?: Set<Severity>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -32,6 +35,7 @@ interface SupportedFilters {
|
|||||||
what?: boolean;
|
what?: boolean;
|
||||||
where?: boolean;
|
where?: boolean;
|
||||||
reviewed?: boolean;
|
reviewed?: boolean;
|
||||||
|
severity?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -47,6 +51,7 @@ export const hasUnsupportedFilters = (
|
|||||||
(!supported.tags && !!query.tags?.size) ||
|
(!supported.tags && !!query.tags?.size) ||
|
||||||
(!supported.what && !!query.what?.size) ||
|
(!supported.what && !!query.what?.size) ||
|
||||||
(!supported.where && !!query.where?.size) ||
|
(!supported.where && !!query.where?.size) ||
|
||||||
(!supported.reviewed && query.reviewed !== undefined)
|
(!supported.reviewed && query.reviewed !== undefined) ||
|
||||||
|
(!supported.severity && !!query.severity?.size)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -260,6 +260,19 @@
|
|||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.emphasis-low advanced-camera-card-icon {
|
||||||
|
color: var(--advanced-camera-card-foreground-severity-low-color);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
&.emphasis-medium advanced-camera-card-icon {
|
||||||
|
color: var(--advanced-camera-card-foreground-severity-medium-color);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
&.emphasis-high advanced-camera-card-icon {
|
||||||
|
color: var(--advanced-camera-card-foreground-severity-high-color);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.description {
|
.description {
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
export const SEVERITIES = ['high', 'medium', 'low'] as const;
|
||||||
|
export type Severity = (typeof SEVERITIES)[number];
|
||||||
+1
-3
@@ -1,6 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import type { EffectOptions } from './components-lib/effects/types';
|
import type { EffectOptions } from './components-lib/effects/types';
|
||||||
import type { LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from './ha/types';
|
import type { LovelaceCard, LovelaceCardConfig, LovelaceCardEditor } from './ha/types';
|
||||||
|
import { Severity } from './severity';
|
||||||
|
|
||||||
// UI-facing media types for galleries and views.
|
// UI-facing media types for galleries and views.
|
||||||
export const VIEW_MEDIA_TYPES = ['clips', 'snapshots', 'recordings', 'reviews'] as const;
|
export const VIEW_MEDIA_TYPES = ['clips', 'snapshots', 'recordings', 'reviews'] as const;
|
||||||
@@ -221,6 +222,3 @@ export interface EffectsControllerAPI {
|
|||||||
stopEffect(effect: EffectName): void;
|
stopEffect(effect: EffectName): void;
|
||||||
toggleEffect(effect: EffectName, options?: EffectOptions): Promise<void>;
|
toggleEffect(effect: EffectName, options?: EffectOptions): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SEVERITIES = ['high', 'medium', 'low'] as const;
|
|
||||||
export type Severity = (typeof SEVERITIES)[number];
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
import { clone } from 'lodash-es';
|
import { clone } from 'lodash-es';
|
||||||
import { FolderPathComponent } from '../card-controller/folders/types';
|
import { FolderPathComponent } from '../card-controller/folders/types';
|
||||||
import { FolderConfig } from '../config/schema/folders';
|
import { FolderConfig } from '../config/schema/folders';
|
||||||
import { Severity } from '../types';
|
import { Severity } from '../severity';
|
||||||
|
|
||||||
export enum ViewMediaType {
|
export enum ViewMediaType {
|
||||||
Clip = 'clip',
|
Clip = 'clip',
|
||||||
|
|||||||
@@ -537,6 +537,7 @@ export class UnifiedQueryBuilder {
|
|||||||
...(options?.what && { what: options.what }),
|
...(options?.what && { what: options.what }),
|
||||||
...(options?.where && { where: options.where }),
|
...(options?.where && { where: options.where }),
|
||||||
...(options?.reviewed !== undefined && { reviewed: options.reviewed }),
|
...(options?.reviewed !== undefined && { reviewed: options.reviewed }),
|
||||||
|
...(options?.severity && { severity: options.severity }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { afterEach, assert, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { RecordingSegmentsCache } from '../../../src/camera-manager/cache';
|
import { RecordingSegmentsCache } from '../../../src/camera-manager/cache';
|
||||||
import { FrigateCameraManagerEngine } from '../../../src/camera-manager/frigate/engine-frigate';
|
import {
|
||||||
|
FrigateCameraManagerEngine,
|
||||||
|
FrigateQueryResultsClassifier,
|
||||||
|
} from '../../../src/camera-manager/frigate/engine-frigate';
|
||||||
import {
|
import {
|
||||||
FrigateEventViewMedia,
|
FrigateEventViewMedia,
|
||||||
FrigateRecordingViewMedia,
|
FrigateRecordingViewMedia,
|
||||||
} from '../../../src/camera-manager/frigate/media';
|
} from '../../../src/camera-manager/frigate/media';
|
||||||
import { FrigateEvent, eventSchema } from '../../../src/camera-manager/frigate/types.js';
|
import { getReviews } from '../../../src/camera-manager/frigate/requests';
|
||||||
import { CameraManagerRequestCache } from '../../../src/camera-manager/types';
|
import {
|
||||||
|
FrigateEvent,
|
||||||
|
FrigateReview,
|
||||||
|
eventSchema,
|
||||||
|
} from '../../../src/camera-manager/frigate/types.js';
|
||||||
|
import { CameraManagerStore } from '../../../src/camera-manager/store';
|
||||||
|
import { CameraManagerRequestCache, QueryType } from '../../../src/camera-manager/types';
|
||||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||||
import { CameraConfig } from '../../../src/config/schema/cameras';
|
import { CameraConfig } from '../../../src/config/schema/cameras';
|
||||||
import { RawAdvancedCameraCardConfig } from '../../../src/config/types';
|
import { RawAdvancedCameraCardConfig } from '../../../src/config/types';
|
||||||
|
import { QuerySource } from '../../../src/query-source';
|
||||||
|
import { Severity } from '../../../src/severity';
|
||||||
import { ViewMedia, ViewMediaType } from '../../../src/view/item';
|
import { ViewMedia, ViewMediaType } from '../../../src/view/item';
|
||||||
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
|
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
|
||||||
import { createCameraConfig, createHASS } from '../../test-utils';
|
import { createCameraConfig, createHASS } from '../../test-utils';
|
||||||
|
|
||||||
|
vi.mock('../../../src/camera-manager/frigate/requests');
|
||||||
|
|
||||||
const createEngine = (): FrigateCameraManagerEngine => {
|
const createEngine = (): FrigateCameraManagerEngine => {
|
||||||
return new FrigateCameraManagerEngine(
|
return new FrigateCameraManagerEngine(
|
||||||
new EntityRegistryManagerMock(),
|
new EntityRegistryManagerMock(),
|
||||||
@@ -56,6 +70,17 @@ const createEvent = (): FrigateEvent => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createFrigateReview = (id: string): FrigateReview => ({
|
||||||
|
id,
|
||||||
|
camera: 'camera-1',
|
||||||
|
start_time: 100,
|
||||||
|
end_time: 200,
|
||||||
|
severity: 'alert',
|
||||||
|
thumb_path: 'thumb.jpg',
|
||||||
|
data: { objects: [], zones: [] },
|
||||||
|
has_been_reviewed: false,
|
||||||
|
});
|
||||||
|
|
||||||
const createClipMedia = (): FrigateEventViewMedia => {
|
const createClipMedia = (): FrigateEventViewMedia => {
|
||||||
return new FrigateEventViewMedia(
|
return new FrigateEventViewMedia(
|
||||||
ViewMediaType.Clip,
|
ViewMediaType.Clip,
|
||||||
@@ -140,3 +165,169 @@ describe('getMediaDownloadPath', () => {
|
|||||||
expect(endpoint).toBeNull();
|
expect(endpoint).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getReviews', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should fan out requests for multiple severities', async () => {
|
||||||
|
const engine = createEngine();
|
||||||
|
const hass = createHASS();
|
||||||
|
const cameraConfig = createCameraConfig({
|
||||||
|
frigate: { camera_name: 'camera-1', client_id: 'client-1' },
|
||||||
|
});
|
||||||
|
const store = mock<CameraManagerStore>();
|
||||||
|
store.getCameraIDs.mockReturnValue(new Set(['camera-1']));
|
||||||
|
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera-1']));
|
||||||
|
store.getCameraConfig.mockReturnValue(cameraConfig);
|
||||||
|
store.getCameraConfigs.mockImplementation(function* () {
|
||||||
|
yield cameraConfig;
|
||||||
|
});
|
||||||
|
store.getCameraConfigEntries.mockImplementation(function* () {
|
||||||
|
yield ['camera-1', cameraConfig];
|
||||||
|
});
|
||||||
|
store.hasCameraID.mockReturnValue(true);
|
||||||
|
|
||||||
|
const reviewHigh = createFrigateReview('review-high');
|
||||||
|
const reviewMedium = createFrigateReview('review-medium');
|
||||||
|
|
||||||
|
vi.mocked(getReviews)
|
||||||
|
.mockResolvedValueOnce([reviewHigh])
|
||||||
|
.mockResolvedValueOnce([reviewMedium]);
|
||||||
|
|
||||||
|
const resultsMap = await engine.getReviews(hass, store, {
|
||||||
|
type: QueryType.Review,
|
||||||
|
source: QuerySource.Camera,
|
||||||
|
cameraIDs: new Set(['camera-1']),
|
||||||
|
severity: new Set(['high', 'medium']),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getReviews).toHaveBeenCalledTimes(2);
|
||||||
|
expect(getReviews).toHaveBeenCalledWith(
|
||||||
|
hass,
|
||||||
|
expect.objectContaining({
|
||||||
|
severity: 'alert',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
expect(getReviews).toHaveBeenCalledWith(
|
||||||
|
hass,
|
||||||
|
expect.objectContaining({
|
||||||
|
severity: 'detection',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = resultsMap?.values().next().value;
|
||||||
|
|
||||||
|
assert(result);
|
||||||
|
assert(FrigateQueryResultsClassifier.isFrigateReviewQueryResults(result));
|
||||||
|
|
||||||
|
expect(result.reviews).toHaveLength(2);
|
||||||
|
expect(result.reviews).toContainEqual(reviewHigh);
|
||||||
|
expect(result.reviews).toContainEqual(reviewMedium);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle single severity', async () => {
|
||||||
|
const engine = createEngine();
|
||||||
|
const hass = createHASS();
|
||||||
|
const cameraConfig = createCameraConfig({
|
||||||
|
frigate: { camera_name: 'camera-1', client_id: 'client-1' },
|
||||||
|
});
|
||||||
|
const store = mock<CameraManagerStore>();
|
||||||
|
store.getCameraIDs.mockReturnValue(new Set(['camera-1']));
|
||||||
|
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera-1']));
|
||||||
|
store.getCameraConfig.mockReturnValue(cameraConfig);
|
||||||
|
store.getCameraConfigs.mockImplementation(function* () {
|
||||||
|
yield cameraConfig;
|
||||||
|
});
|
||||||
|
store.getCameraConfigEntries.mockImplementation(function* () {
|
||||||
|
yield ['camera-1', cameraConfig];
|
||||||
|
});
|
||||||
|
store.hasCameraID.mockReturnValue(true);
|
||||||
|
|
||||||
|
const review = createFrigateReview('review-low');
|
||||||
|
|
||||||
|
vi.mocked(getReviews).mockResolvedValue([review]);
|
||||||
|
|
||||||
|
await engine.getReviews(hass, store, {
|
||||||
|
type: QueryType.Review,
|
||||||
|
source: QuerySource.Camera,
|
||||||
|
cameraIDs: new Set(['camera-1']),
|
||||||
|
severity: new Set<Severity>(['high']),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getReviews).toHaveBeenCalledTimes(1);
|
||||||
|
expect(getReviews).toHaveBeenCalledWith(
|
||||||
|
hass,
|
||||||
|
expect.objectContaining({
|
||||||
|
severity: 'alert',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ignore low severity', async () => {
|
||||||
|
const engine = createEngine();
|
||||||
|
const hass = createHASS();
|
||||||
|
const cameraConfig = createCameraConfig({
|
||||||
|
frigate: { camera_name: 'camera-1', client_id: 'client-1' },
|
||||||
|
});
|
||||||
|
const store = mock<CameraManagerStore>();
|
||||||
|
store.getCameraIDs.mockReturnValue(new Set(['camera-1']));
|
||||||
|
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera-1']));
|
||||||
|
store.getCameraConfig.mockReturnValue(cameraConfig);
|
||||||
|
store.getCameraConfigs.mockImplementation(function* () {
|
||||||
|
yield cameraConfig;
|
||||||
|
});
|
||||||
|
store.getCameraConfigEntries.mockImplementation(function* () {
|
||||||
|
yield ['camera-1', cameraConfig];
|
||||||
|
});
|
||||||
|
store.hasCameraID.mockReturnValue(true);
|
||||||
|
|
||||||
|
vi.mocked(getReviews).mockResolvedValue([]);
|
||||||
|
|
||||||
|
await engine.getReviews(hass, store, {
|
||||||
|
type: QueryType.Review,
|
||||||
|
source: QuerySource.Camera,
|
||||||
|
cameraIDs: new Set(['camera-1']),
|
||||||
|
severity: new Set<Severity>(['low']),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getReviews).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should query all valid severities when severity is undefined', async () => {
|
||||||
|
const engine = createEngine();
|
||||||
|
const hass = createHASS();
|
||||||
|
const cameraConfig = createCameraConfig({
|
||||||
|
frigate: { camera_name: 'camera-1', client_id: 'client-1' },
|
||||||
|
});
|
||||||
|
const store = mock<CameraManagerStore>();
|
||||||
|
store.getCameraIDs.mockReturnValue(new Set(['camera-1']));
|
||||||
|
store.getCameraIDsWithCapability.mockReturnValue(new Set(['camera-1']));
|
||||||
|
store.getCameraConfig.mockReturnValue(cameraConfig);
|
||||||
|
store.getCameraConfigs.mockImplementation(function* () {
|
||||||
|
yield cameraConfig;
|
||||||
|
});
|
||||||
|
store.getCameraConfigEntries.mockImplementation(function* () {
|
||||||
|
yield ['camera-1', cameraConfig];
|
||||||
|
});
|
||||||
|
store.hasCameraID.mockReturnValue(true);
|
||||||
|
|
||||||
|
vi.mocked(getReviews).mockResolvedValue([]);
|
||||||
|
|
||||||
|
await engine.getReviews(hass, store, {
|
||||||
|
type: QueryType.Review,
|
||||||
|
source: QuerySource.Camera,
|
||||||
|
cameraIDs: new Set(['camera-1']),
|
||||||
|
severity: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getReviews).toHaveBeenCalledTimes(1);
|
||||||
|
expect(getReviews).toHaveBeenCalledWith(
|
||||||
|
hass,
|
||||||
|
expect.objectContaining({
|
||||||
|
severity: undefined,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
MediaFilterMediaType,
|
MediaFilterMediaType,
|
||||||
} from '../../src/components-lib/media-filter-controller';
|
} from '../../src/components-lib/media-filter-controller';
|
||||||
import { QuerySource } from '../../src/query-source';
|
import { QuerySource } from '../../src/query-source';
|
||||||
|
import { Severity } from '../../src/severity';
|
||||||
import { UnifiedQuery } from '../../src/view/unified-query';
|
import { UnifiedQuery } from '../../src/view/unified-query';
|
||||||
import {
|
import {
|
||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
@@ -317,6 +318,17 @@ const queryDefaultTestCases: Array<[string, UnifiedQuery, MediaFilterCoreDefault
|
|||||||
mediaTypes: [MediaFilterMediaType.Reviews],
|
mediaTypes: [MediaFilterMediaType.Reviews],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'severity',
|
||||||
|
createQueryWithNodes([
|
||||||
|
{ type: QueryType.Review, severity: new Set<Severity>(['high']) },
|
||||||
|
]),
|
||||||
|
{
|
||||||
|
cameraIDs: ['camera.kitchen'],
|
||||||
|
mediaTypes: [MediaFilterMediaType.Reviews],
|
||||||
|
severity: ['high'],
|
||||||
|
},
|
||||||
|
],
|
||||||
];
|
];
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
@@ -382,6 +394,15 @@ describe('MediaFilterController', () => {
|
|||||||
const controller = new MediaFilterController(createLitElement());
|
const controller = new MediaFilterController(createLitElement());
|
||||||
expect(controller.getTagsOptions()).toEqual([]);
|
expect(controller.getTagsOptions()).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('severity', () => {
|
||||||
|
const controller = new MediaFilterController(createLitElement());
|
||||||
|
expect(controller.getSeverityOptions()).toEqual([
|
||||||
|
{ value: 'high', label: 'High' },
|
||||||
|
{ value: 'medium', label: 'Medium' },
|
||||||
|
{ value: 'low', label: 'Low' },
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should calculate correct dynamic options', () => {
|
describe('should calculate correct dynamic options', () => {
|
||||||
@@ -675,6 +696,36 @@ describe('MediaFilterController', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('with severity', async () => {
|
||||||
|
const host = createLitElement();
|
||||||
|
const cameraManager = createCameraManager(createCameraStore());
|
||||||
|
const viewManager = mock<ViewManager>();
|
||||||
|
viewManager.getView.mockReturnValue(createView());
|
||||||
|
|
||||||
|
const controller = new MediaFilterController(host);
|
||||||
|
controller.setViewManager(viewManager);
|
||||||
|
|
||||||
|
await controller.valueChangeHandler(
|
||||||
|
cameraManager,
|
||||||
|
mock<FoldersManager>(),
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
mediaTypes: [MediaFilterMediaType.Reviews],
|
||||||
|
when: {},
|
||||||
|
severity: ['high'],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(viewManager.setViewByParametersWithExistingQuery).toBeCalled();
|
||||||
|
|
||||||
|
const nodes = getQueryNodes(viewManager);
|
||||||
|
expect(nodes).toHaveLength(1);
|
||||||
|
expect(nodes?.[0]).toMatchObject({
|
||||||
|
type: QueryType.Review,
|
||||||
|
severity: new Set(['high']),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('with multiple media types', async () => {
|
it('with multiple media types', async () => {
|
||||||
const host = createLitElement();
|
const host = createLitElement();
|
||||||
const cameraManager = createCameraManager(createCameraStore());
|
const cameraManager = createCameraManager(createCameraStore());
|
||||||
|
|||||||
+1
-1
@@ -70,12 +70,12 @@ import { Device } from '../src/ha/registry/device/types';
|
|||||||
import { Entity, EntityRegistryManager } from '../src/ha/registry/entity/types';
|
import { Entity, EntityRegistryManager } from '../src/ha/registry/entity/types';
|
||||||
import { CurrentUser, HassStateDifference, HomeAssistant } from '../src/ha/types';
|
import { CurrentUser, HassStateDifference, HomeAssistant } from '../src/ha/types';
|
||||||
import { QuerySource } from '../src/query-source';
|
import { QuerySource } from '../src/query-source';
|
||||||
|
import { Severity } from '../src/severity';
|
||||||
import {
|
import {
|
||||||
CapabilitiesRaw,
|
CapabilitiesRaw,
|
||||||
EffectsControllerAPI,
|
EffectsControllerAPI,
|
||||||
Interaction,
|
Interaction,
|
||||||
MediaLoadedInfo,
|
MediaLoadedInfo,
|
||||||
Severity,
|
|
||||||
} from '../src/types';
|
} from '../src/types';
|
||||||
import {
|
import {
|
||||||
EventViewMedia,
|
EventViewMedia,
|
||||||
|
|||||||
@@ -462,6 +462,24 @@ describe('UnifiedQueryBuilder', () => {
|
|||||||
expect(nodes[0].what).toEqual(new Set(['person']));
|
expect(nodes[0].what).toEqual(new Set(['person']));
|
||||||
expect(nodes[0].where).toEqual(new Set(['zone']));
|
expect(nodes[0].where).toEqual(new Set(['zone']));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should apply severity filter', () => {
|
||||||
|
const { cameraManager, foldersManager } = createMocks();
|
||||||
|
const builder = new UnifiedQueryBuilder(cameraManager, foldersManager);
|
||||||
|
|
||||||
|
const query = builder.buildFilterQuery(
|
||||||
|
new Set(['camera.office']),
|
||||||
|
new Set(['clips']),
|
||||||
|
{
|
||||||
|
severity: new Set(['high']),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(query);
|
||||||
|
const nodes = query.getNodes();
|
||||||
|
expect(nodes).toHaveLength(1);
|
||||||
|
expect(nodes[0].severity).toEqual(new Set(['high']));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user