Merge pull request #1142 from dermotduffy/top-score-null
Fix Frigate event schema break (`top_score` === `null`)
This commit is contained in:
@@ -21,7 +21,7 @@ const eventSchema = z.object({
|
||||
label: z.string(),
|
||||
sub_label: z.string().nullable(),
|
||||
start_time: z.number(),
|
||||
top_score: z.number(),
|
||||
top_score: z.number().nullable(),
|
||||
zones: z.string().array(),
|
||||
retain_indefinitely: z.boolean().optional(),
|
||||
});
|
||||
|
||||
@@ -14,11 +14,11 @@ export const getEventTitle = (event: FrigateEvent): string => {
|
||||
? event.end_time - event.start_time
|
||||
: Date.now() / 1000 - event.start_time,
|
||||
);
|
||||
const score = event.top_score !== null ? ` ${Math.round(event.top_score * 100)}%` : '';
|
||||
|
||||
return `${formatDateAndTime(
|
||||
utcToZonedTime(event.start_time * 1000, localTimezone),
|
||||
)} [${durationSeconds}s, ${prettifyTitle(event.label)} ${Math.round(
|
||||
event.top_score * 100,
|
||||
)}%]`;
|
||||
)} [${durationSeconds}s, ${prettifyTitle(event.label)}${score}]`;
|
||||
};
|
||||
|
||||
export const getRecordingTitle = (
|
||||
@@ -93,5 +93,5 @@ export const getRecordingID = (
|
||||
// can be dedup'd from this id.
|
||||
return `${cameraConfig.frigate?.client_id ?? ''}/${
|
||||
cameraConfig.frigate.camera_name ?? ''
|
||||
}/${recording.startTime.getTime()}/${recording.endTime.getTime()}}`;
|
||||
}/${recording.startTime.getTime()}/${recording.endTime.getTime()}`;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,154 @@
|
||||
import add from 'date-fns/add';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import {
|
||||
getEventMediaContentID,
|
||||
getEventThumbnailURL,
|
||||
getEventTitle,
|
||||
getRecordingID,
|
||||
getRecordingMediaContentID,
|
||||
getRecordingTitle,
|
||||
} from '../../../src/camera-manager/frigate/util';
|
||||
import { CameraConfig } from '../../../src/types';
|
||||
import {
|
||||
createCameraConfig,
|
||||
createFrigateEvent,
|
||||
createFrigateRecording,
|
||||
} from '../../test-utils';
|
||||
|
||||
describe('getEventTitle', () => {
|
||||
const start = new Date('2023-05-06T10:43:00');
|
||||
const end = new Date('2023-05-06T10:44:12');
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
it('should get finished event title', () => {
|
||||
expect(
|
||||
getEventTitle(
|
||||
createFrigateEvent({
|
||||
start_time: start.getTime() / 1000,
|
||||
end_time: end.getTime() / 1000,
|
||||
top_score: 0.841796875,
|
||||
label: 'person',
|
||||
}),
|
||||
),
|
||||
).toBe('2023-05-06 10:43 [72s, Person 84%]');
|
||||
});
|
||||
it('should get in-progress event title', () => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(add(start, { seconds: 60 }));
|
||||
|
||||
expect(
|
||||
getEventTitle(
|
||||
createFrigateEvent({
|
||||
start_time: start.getTime() / 1000,
|
||||
end_time: null,
|
||||
top_score: 0.841796875,
|
||||
label: 'person',
|
||||
}),
|
||||
),
|
||||
).toBe('2023-05-06 10:43 [60s, Person 84%]');
|
||||
});
|
||||
it('should get scoreless event title', () => {
|
||||
expect(
|
||||
getEventTitle(
|
||||
createFrigateEvent({
|
||||
start_time: start.getTime() / 1000,
|
||||
end_time: end.getTime() / 1000,
|
||||
top_score: null,
|
||||
label: 'person',
|
||||
}),
|
||||
),
|
||||
).toBe('2023-05-06 10:43 [72s, Person]');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecordingTitle', () => {
|
||||
it('should get recording title', () => {
|
||||
expect(
|
||||
getRecordingTitle(
|
||||
'Kitchen',
|
||||
createFrigateRecording({
|
||||
startTime: new Date('2023-04-29T14:00:00'),
|
||||
}),
|
||||
),
|
||||
).toBe('Kitchen 2023-04-29 14:00');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEventThumbnailURL', () => {
|
||||
it('should get thumbnail URL', () => {
|
||||
expect(
|
||||
getEventThumbnailURL(
|
||||
'clientid',
|
||||
createFrigateEvent({
|
||||
id: '1683396875.643998-hmzrh5',
|
||||
}),
|
||||
),
|
||||
).toBe('/api/frigate/clientid/thumbnail/1683396875.643998-hmzrh5');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getEventMediaContentID', () => {
|
||||
it('should get event content ID', () => {
|
||||
expect(
|
||||
getEventMediaContentID(
|
||||
'clientid',
|
||||
'kitchen',
|
||||
createFrigateEvent({
|
||||
id: '1683396875.643998-hmzrh5',
|
||||
}),
|
||||
'clips',
|
||||
),
|
||||
).toBe(
|
||||
'media-source://frigate/clientid/event/clips/kitchen/1683396875.643998-hmzrh5',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecordingMediaContentID', () => {
|
||||
it('should get recording content ID', () => {
|
||||
expect(
|
||||
getRecordingMediaContentID(
|
||||
'clientid',
|
||||
'kitchen',
|
||||
createFrigateRecording({
|
||||
startTime: new Date('2023-04-29T14:00:00'),
|
||||
}),
|
||||
),
|
||||
).toBe('media-source://frigate/clientid/recordings/kitchen/2023-04-29/14');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecordingID', () => {
|
||||
it('should get recording ID', () => {
|
||||
expect(
|
||||
getRecordingID(
|
||||
createCameraConfig({
|
||||
frigate: {
|
||||
client_id: 'unique_client_id',
|
||||
camera_name: 'kitchen',
|
||||
},
|
||||
}),
|
||||
createFrigateRecording({
|
||||
startTime: new Date('2023-04-29T14:00:00Z'),
|
||||
endTime: new Date('2023-04-29T14:59:59Z'),
|
||||
}),
|
||||
),
|
||||
).toBe('unique_client_id/kitchen/1682776800000/1682780399000');
|
||||
});
|
||||
it('should get recording ID without client_id or camera_name', () => {
|
||||
// Note: This path is defended against in the code but should not happen in
|
||||
// practice as this would be a malformed (not-zod-parsed) camera config.
|
||||
const cameraConfig = mock<CameraConfig>();
|
||||
expect(
|
||||
getRecordingID(
|
||||
cameraConfig,
|
||||
createFrigateRecording({
|
||||
startTime: new Date('2023-04-29T14:00:00Z'),
|
||||
endTime: new Date('2023-04-29T14:59:59Z'),
|
||||
}),
|
||||
),
|
||||
).toBe('//1682776800000/1682780399000');
|
||||
});
|
||||
});
|
||||
@@ -1,6 +1,7 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { HassEntities, HassEntity } from 'home-assistant-js-websocket';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { FrigateEvent, FrigateRecording } from '../src/camera-manager/frigate/types';
|
||||
import {
|
||||
CameraConfig,
|
||||
FrigateCardCondition,
|
||||
@@ -60,3 +61,31 @@ export const createStateEntity = (entity?: Partial<HassEntity>): HassEntity => {
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export const createFrigateEvent = (event?: Partial<FrigateEvent>) => {
|
||||
return {
|
||||
camera: 'camera',
|
||||
end_time: 1683397124,
|
||||
false_positive: false,
|
||||
has_clip: true,
|
||||
has_snapshot: true,
|
||||
id: '1683396875.643998-hmzrh5',
|
||||
label: 'person',
|
||||
sub_label: null,
|
||||
start_time: 1683395000,
|
||||
top_score: 0.841796875,
|
||||
zones: [],
|
||||
retain_indefinitely: false,
|
||||
...event,
|
||||
};
|
||||
};
|
||||
|
||||
export const createFrigateRecording = (recording?: Partial<FrigateRecording>) => {
|
||||
return {
|
||||
cameraID: 'cameraID',
|
||||
startTime: new Date('2023-04-29T14:00:00'),
|
||||
endTime: new Date('2023-04-29T14:59:59'),
|
||||
events: 42,
|
||||
...recording,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user