feat: Add event-based automation triggers (#2537)

This commit is contained in:
Dermot Duffy
2026-06-30 17:45:13 -07:00
committed by dermotduffy
parent b701366762
commit a31816c168
109 changed files with 4607 additions and 1623 deletions
+166 -200
View File
@@ -1,5 +1,5 @@
import { format } from 'date-fns';
import { assert, beforeEach, describe, expect, it, vi } from 'vitest';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CameraManagerEngine } from '../../../src/camera-manager/engine';
import { FrigateCamera } from '../../../src/camera-manager/frigate/camera';
@@ -18,8 +18,6 @@ import {
FrigateReviewWatcher,
} from '../../../src/camera-manager/frigate/watcher';
import { ActionsExecutor } from '../../../src/card-controller/actions/types';
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
import { PTZAction } from '../../../src/config/schema/actions/custom/ptz';
import { CameraTriggerMediaEventType } from '../../../src/config/schema/cameras';
import { Entity, EntityRegistryManager } from '../../../src/ha/registry/entity/types';
@@ -29,6 +27,7 @@ import {
createCameraConfig,
createCapabilities,
createHASS,
createHASSManager,
createRegistryEntity,
createStateEntity,
} from '../../test-utils';
@@ -42,7 +41,7 @@ const callEventWatcherCallback = (
): void => {
const mock = vi.mocked(eventWatcher.subscribe).mock;
expect(mock.calls.length).greaterThan(n);
mock.calls[n][1].callback(event);
mock.calls[n][0].callback(event);
};
const callReviewWatcherCallback = (
@@ -52,7 +51,7 @@ const callReviewWatcherCallback = (
): void => {
const mock = vi.mocked(reviewWatcher.subscribe).mock;
expect(mock.calls.length).greaterThan(n);
mock.calls[n][1].callback(review);
mock.calls[n][0].callback(review);
};
describe('FrigateCamera', () => {
@@ -69,10 +68,8 @@ describe('FrigateCamera', () => {
const beforeConfig = { ...config };
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -92,10 +89,8 @@ describe('FrigateCamera', () => {
expect(
async () =>
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
}),
@@ -118,10 +113,8 @@ describe('FrigateCamera', () => {
]);
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -144,10 +137,8 @@ describe('FrigateCamera', () => {
]);
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -170,10 +161,8 @@ describe('FrigateCamera', () => {
]);
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -191,15 +180,15 @@ describe('FrigateCamera', () => {
mock<CameraManagerEngine>(),
);
await camera.initialize({
hass: createHASS({
'camera.front_door': createStateEntity({
entity_id: 'camera.front_door',
attributes: { client_id: 'remote_frigate' },
hassManager: createHASSManager({
hass: createHASS({
'camera.front_door': createStateEntity({
entity_id: 'camera.front_door',
attributes: { client_id: 'remote_frigate' },
}),
}),
}),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -215,15 +204,15 @@ describe('FrigateCamera', () => {
mock<CameraManagerEngine>(),
);
await camera.initialize({
hass: createHASS({
'camera.front_door': createStateEntity({
entity_id: 'camera.front_door',
attributes: {},
hassManager: createHASSManager({
hass: createHASS({
'camera.front_door': createStateEntity({
entity_id: 'camera.front_door',
attributes: {},
}),
}),
}),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -238,10 +227,8 @@ describe('FrigateCamera', () => {
mock<CameraManagerEngine>(),
);
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -257,15 +244,15 @@ describe('FrigateCamera', () => {
mock<CameraManagerEngine>(),
);
await camera.initialize({
hass: createHASS({
'camera.front_door': createStateEntity({
entity_id: 'camera.front_door',
attributes: { client_id: 'something_else' },
hassManager: createHASSManager({
hass: createHASS({
'camera.front_door': createStateEntity({
entity_id: 'camera.front_door',
attributes: { client_id: 'something_else' },
}),
}),
}),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -281,16 +268,16 @@ describe('FrigateCamera', () => {
mock<CameraManagerEngine>(),
);
await camera.initialize({
hass: createHASS({
'camera.front_door': createStateEntity({
entity_id: 'camera.front_door',
state: 'unavailable',
attributes: {},
hassManager: createHASSManager({
hass: createHASS({
'camera.front_door': createStateEntity({
entity_id: 'camera.front_door',
state: 'unavailable',
attributes: {},
}),
}),
}),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -311,10 +298,8 @@ describe('FrigateCamera', () => {
);
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -340,10 +325,8 @@ describe('FrigateCamera', () => {
);
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -373,10 +356,8 @@ describe('FrigateCamera', () => {
vi.mocked(getPTZInfo).mockRejectedValue(new Error());
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -404,10 +385,8 @@ describe('FrigateCamera', () => {
});
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -442,10 +421,8 @@ describe('FrigateCamera', () => {
});
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -475,10 +452,8 @@ describe('FrigateCamera', () => {
});
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -512,10 +487,8 @@ describe('FrigateCamera', () => {
});
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -842,15 +815,12 @@ describe('FrigateCamera', () => {
const eventWatcher = mock<FrigateEventWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
expect(eventWatcher.subscribe).toBeCalledWith(
hass,
expect.objectContaining({
instanceID: 'CLIENT_ID',
}),
@@ -874,10 +844,8 @@ describe('FrigateCamera', () => {
const eventWatcher = mock<FrigateEventWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -901,10 +869,8 @@ describe('FrigateCamera', () => {
const eventWatcher = mock<FrigateEventWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -927,10 +893,8 @@ describe('FrigateCamera', () => {
const eventWatcher = mock<FrigateEventWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -953,10 +917,8 @@ describe('FrigateCamera', () => {
const eventWatcher = mock<FrigateEventWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -966,9 +928,10 @@ describe('FrigateCamera', () => {
expect(eventWatcher.unsubscribe).toBeCalled();
});
it('should unsubscribe on destroy while event subscription is pending', async () => {
it('should not subscribe when destroyed while base initialization is pending', async () => {
const camera = new FrigateCamera(
createCameraConfig({
camera_entity: 'camera.front_door',
frigate: { client_id: 'CLIENT_ID', camera_name: 'front_door' },
triggers: {
media_events: ['events'],
@@ -978,45 +941,40 @@ describe('FrigateCamera', () => {
mock<CameraManagerEngine>(),
);
const hass = createHASS();
let resolveSubscribe: () => void = () => {};
const eventWatcher = mock<FrigateEventWatcher>();
const reviewWatcher = mock<FrigateReviewWatcher>();
vi.mocked(eventWatcher.subscribe).mockReturnValue(
new Promise<void>((resolve) => {
resolveSubscribe = resolve;
// Pend base initialization on entity resolution so destroy() can flip
// `_destroyed` before initialize() reaches the subscribe calls.
let resolveEntity: () => void = () => {};
const entityRegistryManager = mock<EntityRegistryManager>();
vi.mocked(entityRegistryManager.getEntity).mockReturnValue(
new Promise((resolve) => {
resolveEntity = () => resolve(createRegistryEntity());
}),
);
const initializePromise = camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
hassManager: createHASSManager({ hass }),
entityRegistryManager: entityRegistryManager,
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: reviewWatcher,
// Pre-built so `_buildCapabilities` (which calls the un-mocked
// `liveProviderSupports2WayAudio`) is skipped and init reaches the
// pending Frigate event subscribe.
capabilityOptions: { capabilities: createCapabilities({ trigger: true }) },
});
await vi.waitFor(() => expect(eventWatcher.subscribe).toBeCalled());
await vi.waitFor(() => expect(entityRegistryManager.getEntity).toBeCalled());
await camera.destroy();
// Destroy iterated `_destroyCallbacks` and called the unsubscribe that
// was registered before the (still pending) event subscribe.
const subscribeCall = vi.mocked(eventWatcher.subscribe).mock.calls[0];
assert(subscribeCall);
expect(eventWatcher.unsubscribe).toBeCalledWith(subscribeCall[1]);
// `_destroyed` short-circuits initialize() after the pending await, so
// neither watcher is ever subscribed.
expect(eventWatcher.subscribe).not.toBeCalled();
expect(reviewWatcher.subscribe).not.toBeCalled();
resolveSubscribe();
resolveEntity();
await initializePromise;
// The subsequent `_subscribeToReviews` short-circuited on `_destroyed`,
// so the review watcher was never subscribed (and so never needs an
// unsubscribe -- which would otherwise be ordered before the subscribe
// in the per-key PQueue and leak the resulting subscription).
expect(eventWatcher.subscribe).not.toBeCalled();
expect(eventWatcher.unsubscribe).not.toBeCalled();
expect(reviewWatcher.subscribe).not.toBeCalled();
expect(reviewWatcher.unsubscribe).not.toBeCalled();
});
@@ -1106,10 +1064,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
const eventWatcher = mock<FrigateEventWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -1152,6 +1108,75 @@ describe('FrigateCamera', () => {
);
});
describe('should always forward end events to clear the trigger', () => {
it.each([
['with media still present', true, ['front_steps']],
['with no media present at end', false, ['front_steps']],
['even after the object left the configured zone', true, []],
])('%s', async (_name: string, hasClip: boolean, currentZones: string[]) => {
const eventCallback = vi.fn();
const camera = new FrigateCamera(
createCameraConfig({
id: 'CAMERA_1',
frigate: {
camera_name: 'camera.front_door',
zones: ['front_steps'],
},
triggers: {
media_events: ['clips'],
},
}),
mock<CameraManagerEngine>(),
{
eventCallback: eventCallback,
},
);
const hass = createHASS();
const eventWatcher = mock<FrigateEventWatcher>();
await camera.initialize({
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
// An 'end' clears the trigger regardless of the start criteria: the
// media may be unchanged or absent, and the object may have left the
// zone by now.
callEventWatcherCallback(eventWatcher, {
type: 'end',
before: {
id: 'event-1',
camera: 'camera.front_door',
snapshot: null,
has_clip: hasClip,
has_snapshot: false,
label: 'person',
current_zones: currentZones,
},
after: {
id: 'event-1',
camera: 'camera.front_door',
snapshot: null,
has_clip: hasClip,
has_snapshot: false,
label: 'person',
current_zones: currentZones,
},
});
expect(eventCallback).toBeCalledWith({
type: 'end',
cameraID: 'CAMERA_1',
id: 'event-1',
clip: false,
snapshot: false,
fidelity: 'high',
});
});
});
describe('should handle zones correctly', () => {
it.each([
['has no zone', [], false],
@@ -1179,10 +1204,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
const eventWatcher = mock<FrigateEventWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -1239,10 +1262,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
const eventWatcher = mock<FrigateEventWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -1296,10 +1317,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
const eventWatcher = mock<FrigateEventWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -1350,15 +1369,12 @@ describe('FrigateCamera', () => {
const reviewWatcher = mock<FrigateReviewWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
expect(reviewWatcher.subscribe).toBeCalledWith(
hass,
expect.objectContaining({
instanceID: 'CLIENT_ID',
}),
@@ -1384,10 +1400,8 @@ describe('FrigateCamera', () => {
const reviewWatcher = mock<FrigateReviewWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1414,10 +1428,8 @@ describe('FrigateCamera', () => {
const reviewWatcher = mock<FrigateReviewWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1449,10 +1461,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
const reviewWatcher = mock<FrigateReviewWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1514,10 +1524,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
const reviewWatcher = mock<FrigateReviewWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1587,10 +1595,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
const reviewWatcher = mock<FrigateReviewWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1660,10 +1666,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
const reviewWatcher = mock<FrigateReviewWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1719,10 +1723,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
const reviewWatcher = mock<FrigateReviewWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1777,10 +1779,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
const reviewWatcher = mock<FrigateReviewWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1839,10 +1839,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
const reviewWatcher = mock<FrigateReviewWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1898,10 +1896,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
const reviewWatcher = mock<FrigateReviewWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1965,10 +1961,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
const reviewWatcher = mock<FrigateReviewWatcher>();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -2044,10 +2038,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2075,10 +2067,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2100,10 +2090,8 @@ describe('FrigateCamera', () => {
);
const hass = createHASS();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: new EntityRegistryManagerMock(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2126,10 +2114,8 @@ describe('FrigateCamera', () => {
);
await expect(
camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: new EntityRegistryManagerMock(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
}),
@@ -2153,10 +2139,8 @@ describe('FrigateCamera', () => {
);
const hass = createHASS();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2179,10 +2163,8 @@ describe('FrigateCamera', () => {
);
const hass = createHASS();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2204,10 +2186,8 @@ describe('FrigateCamera', () => {
);
const hass = createHASS();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: new EntityRegistryManagerMock(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2238,10 +2218,8 @@ describe('FrigateCamera', () => {
);
const hass = createHASS();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2274,10 +2252,8 @@ describe('FrigateCamera', () => {
);
const hass = createHASS();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2295,10 +2271,8 @@ describe('FrigateCamera', () => {
const hass = createHASS();
await camera.initialize({
hass: hass,
hassManager: createHASSManager({ hass }),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2327,12 +2301,10 @@ describe('FrigateCamera', () => {
);
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: new EntityRegistryManagerMock([
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
]),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2366,12 +2338,10 @@ describe('FrigateCamera', () => {
);
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: new EntityRegistryManagerMock([
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
]),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2420,12 +2390,10 @@ describe('FrigateCamera', () => {
);
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: new EntityRegistryManagerMock([
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
]),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2457,12 +2425,10 @@ describe('FrigateCamera', () => {
);
await camera.initialize({
hass: createHASS(),
hassManager: createHASSManager(),
entityRegistryManager: new EntityRegistryManagerMock([
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
]),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -1,5 +1,4 @@
import { afterEach, assert, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { RecordingSegmentsCache } from '../../../src/camera-manager/cache';
import { Camera } from '../../../src/camera-manager/camera';
import {
@@ -33,8 +32,6 @@ import {
QueryResultsType,
QueryType,
} from '../../../src/camera-manager/types';
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { CameraConfig } from '../../../src/config/schema/cameras';
import { RawAdvancedCameraCardConfig } from '../../../src/config/types';
import { QuerySource } from '../../../src/query-source';
@@ -47,6 +44,7 @@ import {
createFrigateRecording,
createFrigateReview,
createHASS,
createHASSManager,
createStore,
TestViewMedia,
} from '../../test-utils';
@@ -59,8 +57,7 @@ const createEngine = (options?: {
}): FrigateCameraManagerEngine => {
return new FrigateCameraManagerEngine(
new EntityRegistryManagerMock(),
new StateWatcher(),
mock<EventWatcherSubscriptionInterface>(),
createHASSManager(),
options?.cache ?? new RecordingSegmentsCache(),
options?.requestCache ?? new CameraManagerRequestCache(),
);
@@ -223,10 +220,7 @@ describe('FrigateCameraManagerEngine', () => {
const engine = createEngine();
vi.mocked(getPTZInfo).mockResolvedValue({ features: [], presets: [] });
const camera = await engine.createCamera(
createHASS(),
createFrigateCameraConfig(),
);
const camera = await engine.createCamera(createFrigateCameraConfig());
expect(camera).toBeInstanceOf(Camera);
});
+93 -207
View File
@@ -1,4 +1,6 @@
import { afterEach, assert, describe, expect, it, vi } from 'vitest';
import { Connection } from 'home-assistant-js-websocket';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import {
FrigateEventChange,
FrigateReviewChange,
@@ -8,7 +10,7 @@ import {
FrigateReviewWatcher,
} from '../../../src/camera-manager/frigate/watcher.js';
import { HomeAssistant } from '../../../src/ha/types.js';
import { createHASS } from '../../test-utils.js';
import { createHASS, createHASSSource, flushPromises } from '../../test-utils.js';
const createEventChange = (): FrigateEventChange => {
return {
@@ -68,110 +70,91 @@ const createReviewChange = (): FrigateReviewChange => {
},
};
};
const callHASubscribeMessageCallback = (
hass: HomeAssistant,
data: unknown,
n = 0,
): void => {
// Drive the dispatcher registered with `hass.connection.subscribeMessage` to
// simulate a Frigate WS message arriving over the bus.
const fireMessage = (hass: HomeAssistant, data: unknown, n = 0): void => {
const mock = vi.mocked(hass.connection.subscribeMessage).mock;
expect(mock.calls.length).greaterThan(n);
mock.calls[n][0](data);
};
// @vitest-environment jsdom
describe('FrigateEventWatcher', () => {
it('should subscribe to a given topic once', async () => {
const stateWatcher = new FrigateEventWatcher();
const hass = createHASS();
await stateWatcher.subscribe(hass, {
instanceID: 'frigate',
callback: vi.fn(),
});
await stateWatcher.subscribe(hass, {
instanceID: 'frigate',
callback: vi.fn(),
});
expect(hass.connection.subscribeMessage).toBeCalledTimes(1);
afterEach(() => {
vi.restoreAllMocks();
});
it('should only subscribe from a given topic once', async () => {
const stateWatcher = new FrigateEventWatcher();
it('should open a WS subscription with the frigate event type and instance id', async () => {
const hass = createHASS();
const { source } = createHASSSource(hass);
const watcher = new FrigateEventWatcher(source);
const unsubscribeCallback = vi.fn();
vi.mocked(hass.connection.subscribeMessage).mockResolvedValue(unsubscribeCallback);
watcher.subscribe({ instanceID: 'frigate', callback: vi.fn() });
await flushPromises();
const request_1 = {
instanceID: 'frigate',
callback: vi.fn(),
};
const request_2 = { ...request_1 };
await stateWatcher.subscribe(hass, request_1);
await stateWatcher.subscribe(hass, request_2);
await stateWatcher.unsubscribe(request_1);
expect(unsubscribeCallback).not.toBeCalled();
await stateWatcher.unsubscribe(request_2);
expect(unsubscribeCallback).toBeCalledTimes(1);
expect(hass.connection.subscribeMessage).toBeCalledWith(
expect.any(Function),
expect.objectContaining({
type: 'frigate/events/subscribe',
instance_id: 'frigate',
}),
);
});
it('should handle unsubscribe during pending subscription', async () => {
const stateWatcher = new FrigateEventWatcher();
it('should unsubscribe from the WS subscription', async () => {
const hass = createHASS();
const unsub = vi.fn();
vi.mocked(hass.connection.subscribeMessage).mockResolvedValue(unsub);
const { source } = createHASSSource(hass);
const watcher = new FrigateEventWatcher(source);
const request = { instanceID: 'frigate', callback: vi.fn() };
let resolveSubscription: ((callback: () => Promise<void>) => void) | undefined;
const subscriptionPromise = new Promise<() => Promise<void>>((resolve) => {
resolveSubscription = resolve;
});
vi.mocked(hass.connection.subscribeMessage).mockReturnValue(subscriptionPromise);
watcher.subscribe(request);
await flushPromises();
const request = {
instanceID: 'frigate',
callback: vi.fn(),
};
watcher.unsubscribe(request);
await flushPromises();
// Start subscription (doesn't complete yet as not awaited).
const subscribePromise = stateWatcher.subscribe(hass, request);
expect(unsub).toBeCalledTimes(1);
});
// Unsubscribe while subscription is still pending.
const unsubscribePromise = stateWatcher.unsubscribe(request);
it('should drop messages from an old-connection subscription after a swap', async () => {
const oldHass = createHASS();
const { source, push } = createHASSSource(oldHass);
const watcher = new FrigateEventWatcher(source);
const callback = vi.fn();
// Complete the subscription: both subscribe and unsubscribe await the same
// pending promise, and unsubscribe then invokes the resolved unsub.
const unsubscribeCallback = vi.fn();
assert(resolveSubscription);
resolveSubscription(unsubscribeCallback);
await subscribePromise;
await unsubscribePromise;
watcher.subscribe({ instanceID: 'frigate', callback });
await flushPromises();
expect(unsubscribeCallback).toBeCalledTimes(1);
callHASubscribeMessageCallback(hass, JSON.stringify(createEventChange()));
expect(request.callback).not.toBeCalled();
// Capture the dispatcher registered against the OLD connection before the
// swap, so it still points at the now-stale era guard.
const oldDispatcher = vi.mocked(oldHass.connection.subscribeMessage).mock
.calls[0][0];
const newHass = createHASS();
newHass.connection = mock<Connection>();
vi.mocked(newHass.connection.subscribeMessage).mockResolvedValue(vi.fn());
push(newHass);
await flushPromises();
oldDispatcher(JSON.stringify(createEventChange()));
expect(callback).not.toBeCalled();
});
describe('should call handler', () => {
afterEach(() => {
vi.resetAllMocks();
});
it('with invalid JSON', async () => {
const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true);
const stateWatcher = new FrigateEventWatcher();
const hass = createHASS();
const { source } = createHASSSource(hass);
const watcher = new FrigateEventWatcher(source);
const callback = vi.fn();
const request = {
instanceID: 'frigate',
callback: callback,
};
await stateWatcher.subscribe(hass, request);
callHASubscribeMessageCallback(hass, 'NOT_JSON');
watcher.subscribe({ instanceID: 'frigate', callback });
await flushPromises();
fireMessage(hass, 'NOT_JSON');
expect(callback).not.toBeCalled();
expect(spy).toBeCalledWith(
@@ -183,18 +166,15 @@ describe('FrigateEventWatcher', () => {
it('with malformed event', async () => {
const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true);
const stateWatcher = new FrigateEventWatcher();
const hass = createHASS();
const { source } = createHASSSource(hass);
const watcher = new FrigateEventWatcher(source);
const callback = vi.fn();
const request = {
instanceID: 'frigate',
callback: callback,
};
await stateWatcher.subscribe(hass, request);
watcher.subscribe({ instanceID: 'frigate', callback });
await flushPromises();
const data = JSON.stringify({});
callHASubscribeMessageCallback(hass, data);
fireMessage(hass, data);
expect(callback).not.toBeCalled();
expect(spy).toBeCalledWith(
@@ -204,71 +184,40 @@ describe('FrigateEventWatcher', () => {
});
it('without a matcher', async () => {
const stateWatcher = new FrigateEventWatcher();
const hass = createHASS();
const { source } = createHASSSource(hass);
const watcher = new FrigateEventWatcher(source);
const callback = vi.fn();
const request = {
instanceID: 'frigate',
callback: callback,
};
await stateWatcher.subscribe(hass, request);
watcher.subscribe({ instanceID: 'frigate', callback });
await flushPromises();
const eventChange = createEventChange();
callHASubscribeMessageCallback(hass, JSON.stringify(eventChange));
fireMessage(hass, JSON.stringify(eventChange));
expect(callback).toBeCalledWith(eventChange);
});
it('with a non-matching instance_id', async () => {
const stateWatcher = new FrigateEventWatcher();
const hass = createHASS();
const callback_1 = vi.fn();
const request_1 = {
instanceID: 'frigate_1',
callback: callback_1,
};
const callback_2 = vi.fn();
const request_2 = {
instanceID: 'frigate_2',
callback: callback_2,
};
await stateWatcher.subscribe(hass, request_1);
await stateWatcher.subscribe(hass, request_2);
const eventChange = createEventChange();
callHASubscribeMessageCallback(hass, JSON.stringify(eventChange), 1);
expect(callback_1).not.toBeCalledWith(eventChange);
expect(callback_2).toBeCalledWith(eventChange);
});
it('with a matcher', async () => {
const stateWatcher = new FrigateEventWatcher();
const hass = createHASS();
const { source } = createHASSSource(hass);
const watcher = new FrigateEventWatcher(source);
const matching_callback = vi.fn();
const matching_request = {
const non_matching_callback = vi.fn();
watcher.subscribe({
instanceID: 'frigate',
callback: matching_callback,
matcher: (event: FrigateEventChange) => event.after.camera === 'front_door',
};
const non_matching_callback = vi.fn();
const non_matching_request = {
});
watcher.subscribe({
instanceID: 'frigate',
callback: non_matching_callback,
matcher: (event: FrigateEventChange) => event.after.camera === 'back_door',
};
await stateWatcher.subscribe(hass, matching_request);
await stateWatcher.subscribe(hass, non_matching_request);
});
await flushPromises();
const eventChange = createEventChange();
callHASubscribeMessageCallback(hass, JSON.stringify(eventChange));
fireMessage(hass, JSON.stringify(eventChange));
expect(non_matching_callback).not.toBeCalledWith(eventChange);
expect(matching_callback).toBeCalledWith(eventChange);
@@ -276,20 +225,20 @@ describe('FrigateEventWatcher', () => {
});
});
// @vitest-environment jsdom
describe('FrigateReviewWatcher', () => {
it('should subscribe to a given topic once', async () => {
const stateWatcher = new FrigateReviewWatcher();
afterEach(() => {
vi.restoreAllMocks();
});
it('should subscribe to the frigate reviews channel and dispatch review changes', async () => {
const hass = createHASS();
const { source } = createHASSSource(hass);
const watcher = new FrigateReviewWatcher(source);
await stateWatcher.subscribe(hass, {
instanceID: 'frigate',
callback: vi.fn(),
});
await stateWatcher.subscribe(hass, {
instanceID: 'frigate',
callback: vi.fn(),
});
const callback = vi.fn();
watcher.subscribe({ instanceID: 'frigate', callback });
await flushPromises();
expect(hass.connection.subscribeMessage).toBeCalledWith(
expect.any(Function),
@@ -297,73 +246,10 @@ describe('FrigateReviewWatcher', () => {
type: 'frigate/reviews/subscribe',
}),
);
expect(hass.connection.subscribeMessage).toBeCalledTimes(1);
});
describe('should call handler', () => {
afterEach(() => {
vi.resetAllMocks();
});
const reviewChange = createReviewChange();
fireMessage(hass, JSON.stringify(reviewChange));
it('with a review change', async () => {
const stateWatcher = new FrigateReviewWatcher();
const hass = createHASS();
const callback = vi.fn();
const request = {
instanceID: 'frigate',
callback: callback,
};
await stateWatcher.subscribe(hass, request);
const reviewChange = createReviewChange();
callHASubscribeMessageCallback(hass, JSON.stringify(reviewChange));
expect(callback).toBeCalledWith(reviewChange);
});
it('with a genai review change', async () => {
const stateWatcher = new FrigateReviewWatcher();
const hass = createHASS();
const callback = vi.fn();
const request = {
instanceID: 'frigate',
callback: callback,
};
await stateWatcher.subscribe(hass, request);
const reviewChange = createReviewChange();
reviewChange.type = 'genai';
callHASubscribeMessageCallback(hass, JSON.stringify(reviewChange));
expect(callback).toBeCalledWith(reviewChange);
});
it('with invalid JSON', async () => {
const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true);
const stateWatcher = new FrigateReviewWatcher();
const hass = createHASS();
const callback = vi.fn();
const request = {
instanceID: 'frigate',
callback: callback,
};
await stateWatcher.subscribe(hass, request);
callHASubscribeMessageCallback(hass, 'NOT_JSON');
expect(callback).not.toBeCalled();
expect(spy).toBeCalledWith(
'Received non-JSON payload from subscription: frigate/reviews/subscribe',
'NOT_JSON',
);
});
expect(callback).toBeCalledWith(reviewChange);
});
});