|
|
|
@@ -1,4 +1,5 @@
|
|
|
|
|
import { add } from 'date-fns';
|
|
|
|
|
import { PartialDeep } from 'type-fest';
|
|
|
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
|
import { CardController } from '../../src/card-controller/controller';
|
|
|
|
|
import { TriggersManager } from '../../src/card-controller/triggers-manager';
|
|
|
|
@@ -32,10 +33,8 @@ const baseTriggersConfig: TriggersOptions = {
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Creating and mocking a trigger API is a lot of boilerplate, this convenience
|
|
|
|
|
// function reduces it.
|
|
|
|
|
const createTriggerAPI = (options?: {
|
|
|
|
|
config?: Partial<TriggersOptions>;
|
|
|
|
|
config?: PartialDeep<TriggersOptions>;
|
|
|
|
|
default?: AdvancedCameraCardView;
|
|
|
|
|
interaction?: boolean;
|
|
|
|
|
}): CardController => {
|
|
|
|
@@ -44,7 +43,14 @@ const createTriggerAPI = (options?: {
|
|
|
|
|
createConfig({
|
|
|
|
|
view: {
|
|
|
|
|
triggers: options?.config
|
|
|
|
|
? triggersSchema.parse(options.config)
|
|
|
|
|
? triggersSchema.parse({
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
|
...options.config,
|
|
|
|
|
actions: {
|
|
|
|
|
...baseTriggersConfig.actions,
|
|
|
|
|
...options.config.actions,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
: baseTriggersConfig,
|
|
|
|
|
...(options?.default && { default: options.default }),
|
|
|
|
|
},
|
|
|
|
@@ -90,19 +96,35 @@ describe('TriggersManager', () => {
|
|
|
|
|
expect(manager.isTriggered()).toBeFalsy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not trigger without a triggers config', () => {
|
|
|
|
|
it('should not trigger if triggers config is empty', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
actions: {
|
|
|
|
|
trigger: 'none',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(manager.isTriggered()).toBeTruthy();
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not trigger if there is no config', async () => {
|
|
|
|
|
const api = createTriggerAPI();
|
|
|
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
|
|
|
createView({
|
|
|
|
|
camera: 'camera_1' as const,
|
|
|
|
|
}),
|
|
|
|
|
);
|
|
|
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(null);
|
|
|
|
|
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
manager.handleCameraEvent({
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -110,7 +132,7 @@ describe('TriggersManager', () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('trigger actions', () => {
|
|
|
|
|
it('update', async () => {
|
|
|
|
|
it('should handle trigger action set to update', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
@@ -125,6 +147,7 @@ describe('TriggersManager', () => {
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -134,7 +157,7 @@ describe('TriggersManager', () => {
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('default', async () => {
|
|
|
|
|
it('should handle trigger action set to default', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
@@ -147,7 +170,11 @@ describe('TriggersManager', () => {
|
|
|
|
|
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(manager.isTriggered()).toBeTruthy();
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalledWith({
|
|
|
|
@@ -157,7 +184,7 @@ describe('TriggersManager', () => {
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('live', () => {
|
|
|
|
|
it('should handle trigger action set to live', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
@@ -170,7 +197,11 @@ describe('TriggersManager', () => {
|
|
|
|
|
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(manager.isTriggered()).toBeTruthy();
|
|
|
|
|
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
|
|
|
@@ -209,8 +240,9 @@ describe('TriggersManager', () => {
|
|
|
|
|
});
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
manager.handleCameraEvent({
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
fidelity: 'high',
|
|
|
|
|
review: hasReview,
|
|
|
|
@@ -235,7 +267,7 @@ describe('TriggersManager', () => {
|
|
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('none', () => {
|
|
|
|
|
it('should handle trigger action set to none', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
@@ -248,7 +280,11 @@ describe('TriggersManager', () => {
|
|
|
|
|
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(manager.isTriggered()).toBeTruthy();
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
|
|
|
@@ -257,7 +293,7 @@ describe('TriggersManager', () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('untrigger actions', () => {
|
|
|
|
|
it('none', () => {
|
|
|
|
|
it('should handle untrigger action set to none with no trigger actions', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
@@ -270,11 +306,20 @@ describe('TriggersManager', () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
|
|
|
|
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'end' });
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
vi.setSystemTime(add(start, { seconds: 10 }));
|
|
|
|
|
vi.runOnlyPendingTimers();
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
|
|
expect(manager.isTriggered()).toBeFalsy();
|
|
|
|
|
|
|
|
|
@@ -282,7 +327,7 @@ describe('TriggersManager', () => {
|
|
|
|
|
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('default', async () => {
|
|
|
|
|
it('should handle untrigger action set to default', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
@@ -295,8 +340,16 @@ describe('TriggersManager', () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
await manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
|
|
|
|
await manager.handleCameraEvent({ cameraID: 'camera_1', type: 'end' });
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
vi.setSystemTime(add(start, { seconds: 10 }));
|
|
|
|
|
vi.runOnlyPendingTimers();
|
|
|
|
@@ -306,43 +359,232 @@ describe('TriggersManager', () => {
|
|
|
|
|
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle untrigger call with no state', async () => {
|
|
|
|
|
const api = createTriggerAPI();
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'unknown-id',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not untrigger if other sources are still active', async () => {
|
|
|
|
|
const api = createTriggerAPI();
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'entity_1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'entity_2',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(manager.isTriggered()).toBeTruthy();
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'entity_1',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
vi.setSystemTime(add(start, { seconds: 10 }));
|
|
|
|
|
vi.runOnlyPendingTimers();
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
|
|
// Should still be triggered because entity_2 is active.
|
|
|
|
|
expect(manager.isTriggered()).toBeTruthy();
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'entity_2',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
vi.setSystemTime(add(start, { seconds: 20 }));
|
|
|
|
|
vi.runOnlyPendingTimers();
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
|
|
expect(manager.isTriggered()).toBeFalsy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should cancel untrigger timer if a new trigger starts', async () => {
|
|
|
|
|
const api = createTriggerAPI();
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'entity_1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'entity_1',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Move time forward by 5s (the untrigger delay is 10s).
|
|
|
|
|
vi.setSystemTime(add(start, { seconds: 5 }));
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'entity_1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
vi.setSystemTime(add(start, { seconds: 15 }));
|
|
|
|
|
vi.runOnlyPendingTimers();
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
|
|
// Should still be triggered because the second 'new' event should have cancelled the first timer.
|
|
|
|
|
expect(manager.isTriggered()).toBeTruthy();
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should untrigger each camera independently', async () => {
|
|
|
|
|
const api = createTriggerAPI();
|
|
|
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
|
|
|
createStore([
|
|
|
|
|
{
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
config: createCameraConfig({
|
|
|
|
|
triggers: { entities: ['binary_sensor.motion_1'] },
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
cameraID: 'camera_2',
|
|
|
|
|
config: createCameraConfig({
|
|
|
|
|
triggers: { entities: ['binary_sensor.motion_2'] },
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
id: 'motion_1',
|
|
|
|
|
});
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_2',
|
|
|
|
|
type: 'new',
|
|
|
|
|
id: 'motion_2',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(manager.getTriggeredCameraIDs()).toEqual(new Set(['camera_1', 'camera_2']));
|
|
|
|
|
|
|
|
|
|
// Untrigger camera 1.
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
type: 'end',
|
|
|
|
|
id: 'motion_1',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
vi.setSystemTime(add(start, { seconds: 10 }));
|
|
|
|
|
vi.runOnlyPendingTimers();
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
|
|
// Camera 1 untriggered: setViewDefaultWithNewQuery is called.
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalledTimes(1);
|
|
|
|
|
// Camera 2 is still triggered.
|
|
|
|
|
expect(manager.getTriggeredCameraIDs()).toEqual(new Set(['camera_2']));
|
|
|
|
|
|
|
|
|
|
// Untrigger camera 2.
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_2',
|
|
|
|
|
type: 'end',
|
|
|
|
|
id: 'motion_2',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
vi.setSystemTime(add(start, { seconds: 20 }));
|
|
|
|
|
vi.runOnlyPendingTimers();
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
|
|
// Camera 2 untriggered: setViewDefaultWithNewQuery is called again.
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalledTimes(2);
|
|
|
|
|
expect(manager.getTriggeredCameraIDs()).toEqual(new Set());
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should untrigger immediately when untrigger_seconds is 0', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
untrigger_seconds: 0,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
|
|
expect(manager.isTriggered()).toBeFalsy();
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should manage condition state', () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
|
actions: {
|
|
|
|
|
...baseTriggersConfig.actions,
|
|
|
|
|
trigger: 'none',
|
|
|
|
|
untrigger: 'none',
|
|
|
|
|
describe('condition state management', () => {
|
|
|
|
|
it('should manage condition state', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
|
actions: {
|
|
|
|
|
...baseTriggersConfig.actions,
|
|
|
|
|
trigger: 'none',
|
|
|
|
|
untrigger: 'none',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new' });
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith({
|
|
|
|
|
triggered: new Set(['camera_1']),
|
|
|
|
|
});
|
|
|
|
|
vi.mocked(api.getConditionStateManager().getState).mockReturnValue({
|
|
|
|
|
triggered: new Set(['camera_1']),
|
|
|
|
|
});
|
|
|
|
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith({
|
|
|
|
|
triggered: new Set(['camera_1']),
|
|
|
|
|
});
|
|
|
|
|
vi.mocked(api.getConditionStateManager().getState).mockReturnValue({
|
|
|
|
|
triggered: new Set(['camera_1']),
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'end' });
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
vi.setSystemTime(add(start, { seconds: 10 }));
|
|
|
|
|
vi.runOnlyPendingTimers();
|
|
|
|
|
vi.setSystemTime(add(start, { seconds: 10 }));
|
|
|
|
|
vi.runOnlyPendingTimers();
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
|
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith({
|
|
|
|
|
triggered: undefined,
|
|
|
|
|
expect(api.getConditionStateManager().setState).toHaveBeenLastCalledWith({
|
|
|
|
|
triggered: undefined,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('should take no actions with high-fidelity event', () => {
|
|
|
|
|
it('with non-live action', () => {
|
|
|
|
|
it('should ignore high-fidelity events when trigger action is not live', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
@@ -356,13 +598,18 @@ describe('TriggersManager', () => {
|
|
|
|
|
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new', fidelity: 'high' });
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
fidelity: 'high',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
|
|
|
|
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('with non-live default', () => {
|
|
|
|
|
it('should ignore high-fidelity events when default view is not live', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
@@ -376,22 +623,28 @@ describe('TriggersManager', () => {
|
|
|
|
|
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
manager.handleCameraEvent({ cameraID: 'camera_1', type: 'new', fidelity: 'high' });
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
fidelity: 'high',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
|
|
|
|
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should take no actions with human interactions', () => {
|
|
|
|
|
it('should take no actions with human interactions', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
// Interaction present.
|
|
|
|
|
interaction: true,
|
|
|
|
|
});
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
manager.handleCameraEvent({
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -400,8 +653,9 @@ describe('TriggersManager', () => {
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
|
|
|
|
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
|
|
|
|
|
|
|
|
|
manager.handleCameraEvent({
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -414,7 +668,7 @@ describe('TriggersManager', () => {
|
|
|
|
|
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should take no actions when actions are set to none', () => {
|
|
|
|
|
it('should take no actions when actions are set to none', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
actions: {
|
|
|
|
@@ -425,16 +679,18 @@ describe('TriggersManager', () => {
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
manager.handleCameraEvent({
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
expect(manager.isTriggered()).toBeTruthy();
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
|
|
|
|
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
|
|
|
|
|
|
|
|
|
manager.handleCameraEvent({
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -462,6 +718,7 @@ describe('TriggersManager', () => {
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -475,6 +732,7 @@ describe('TriggersManager', () => {
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -518,10 +776,12 @@ describe('TriggersManager', () => {
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_2',
|
|
|
|
|
id: 'event-2',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -535,6 +795,7 @@ describe('TriggersManager', () => {
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
@@ -548,7 +809,7 @@ describe('TriggersManager', () => {
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('should filter triggers by camera', () => {
|
|
|
|
|
it('no dependencies', () => {
|
|
|
|
|
it('should filter triggers when there are no dependencies', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
@@ -564,8 +825,9 @@ describe('TriggersManager', () => {
|
|
|
|
|
});
|
|
|
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(otherCameraSelected);
|
|
|
|
|
|
|
|
|
|
manager.handleCameraEvent({
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
expect(manager.isTriggered()).toBeFalsy();
|
|
|
|
@@ -575,14 +837,15 @@ describe('TriggersManager', () => {
|
|
|
|
|
});
|
|
|
|
|
vi.mocked(api.getViewManager().getView).mockReturnValue(thisCameraSelected);
|
|
|
|
|
|
|
|
|
|
manager.handleCameraEvent({
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
expect(manager.isTriggered()).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('dependencies', () => {
|
|
|
|
|
it('should not filter triggers when there are dependencies', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
@@ -623,8 +886,9 @@ describe('TriggersManager', () => {
|
|
|
|
|
|
|
|
|
|
// Events for the secondary will still trigger when filter_selected_camera
|
|
|
|
|
// is true.
|
|
|
|
|
manager.handleCameraEvent({
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_secondary',
|
|
|
|
|
id: 'event-secondary',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
expect(manager.isTriggered()).toBeTruthy();
|
|
|
|
@@ -717,5 +981,241 @@ describe('TriggersManager', () => {
|
|
|
|
|
expect(result).toBeTruthy();
|
|
|
|
|
expect(manager.isTriggered()).toBeTruthy();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should only execute a single trigger action at startup for multiple triggered sources', async () => {
|
|
|
|
|
const api = createTriggerAPI();
|
|
|
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
|
|
|
createStore([
|
|
|
|
|
{
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
config: createCameraConfig({
|
|
|
|
|
triggers: {
|
|
|
|
|
entities: ['binary_sensor.motion', 'binary_sensor.occupancy'],
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const hass = createHASS({
|
|
|
|
|
'binary_sensor.motion': createStateEntity({
|
|
|
|
|
state: 'on',
|
|
|
|
|
}),
|
|
|
|
|
'binary_sensor.occupancy': createStateEntity({
|
|
|
|
|
state: 'open',
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
|
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
const result = await manager.handleInitialCameraTriggers();
|
|
|
|
|
|
|
|
|
|
expect(result).toBeTruthy();
|
|
|
|
|
expect(manager.isTriggered()).toBeTruthy();
|
|
|
|
|
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledTimes(1);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should prioritize the first triggered camera action at startup', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
actions: {
|
|
|
|
|
trigger: 'live',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
|
|
|
createStore([
|
|
|
|
|
{
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
config: createCameraConfig({
|
|
|
|
|
triggers: {
|
|
|
|
|
entities: ['binary_sensor.motion_1'],
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
cameraID: 'camera_2',
|
|
|
|
|
config: createCameraConfig({
|
|
|
|
|
triggers: {
|
|
|
|
|
entities: ['binary_sensor.motion_2'],
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const hass = createHASS({
|
|
|
|
|
'binary_sensor.motion_1': createStateEntity({
|
|
|
|
|
state: 'on',
|
|
|
|
|
}),
|
|
|
|
|
'binary_sensor.motion_2': createStateEntity({
|
|
|
|
|
state: 'on',
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
|
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
const result = await manager.handleInitialCameraTriggers();
|
|
|
|
|
|
|
|
|
|
expect(result).toBeTruthy();
|
|
|
|
|
expect(manager.getTriggeredCameraIDs()).toEqual(new Set(['camera_1', 'camera_2']));
|
|
|
|
|
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledTimes(1);
|
|
|
|
|
expect(api.getViewManager().setViewByParametersWithNewQuery).toHaveBeenCalledWith({
|
|
|
|
|
params: {
|
|
|
|
|
view: 'live',
|
|
|
|
|
camera: 'camera_1',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not execute startup action if triggered cameras are filtered out', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
filter_selected_camera: true,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
|
|
|
|
createStore([
|
|
|
|
|
{
|
|
|
|
|
cameraID: 'camera_2',
|
|
|
|
|
config: createCameraConfig({
|
|
|
|
|
triggers: {
|
|
|
|
|
entities: ['binary_sensor.motion_2'],
|
|
|
|
|
},
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const hass = createHASS({
|
|
|
|
|
'binary_sensor.motion_2': createStateEntity({
|
|
|
|
|
state: 'on',
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
|
|
|
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
const result = await manager.handleInitialCameraTriggers();
|
|
|
|
|
|
|
|
|
|
// A trigger entity was active...
|
|
|
|
|
expect(result).toBeTruthy();
|
|
|
|
|
// ...but the camera was filtered out, so no trigger state/action was applied.
|
|
|
|
|
expect(manager.isTriggered()).toBeFalsy();
|
|
|
|
|
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should take actions with human interactions when interaction mode is active', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
// Interaction present.
|
|
|
|
|
interaction: true,
|
|
|
|
|
config: {
|
|
|
|
|
...baseTriggersConfig,
|
|
|
|
|
actions: {
|
|
|
|
|
trigger: 'live' as const,
|
|
|
|
|
untrigger: 'default' as const,
|
|
|
|
|
interaction_mode: 'active',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'new',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(manager.isTriggered()).toBeTruthy();
|
|
|
|
|
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
|
|
|
|
params: {
|
|
|
|
|
view: 'live' as const,
|
|
|
|
|
camera: 'camera_1' as const,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'event-1',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
vi.setSystemTime(add(start, { seconds: 10 }));
|
|
|
|
|
vi.runOnlyPendingTimers();
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
|
|
expect(manager.isTriggered()).toBeFalsy();
|
|
|
|
|
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should ignore untrigger actions during non-allowable interaction but still untrigger camera', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
interaction: true,
|
|
|
|
|
config: {
|
|
|
|
|
actions: {
|
|
|
|
|
interaction_mode: 'inactive',
|
|
|
|
|
untrigger: 'default',
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e1', type: 'new' });
|
|
|
|
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e1', type: 'end' });
|
|
|
|
|
|
|
|
|
|
vi.setSystemTime(add(start, { seconds: 15 }));
|
|
|
|
|
vi.runOnlyPendingTimers();
|
|
|
|
|
await flushPromises();
|
|
|
|
|
|
|
|
|
|
// Camera is untriggered...
|
|
|
|
|
expect(manager.isTriggered()).toBe(false);
|
|
|
|
|
|
|
|
|
|
// ...but the action was skipped.
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toHaveBeenCalled();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should not include cameras with no active sources in triggered IDs even before untrigger action completes', async () => {
|
|
|
|
|
const api = createTriggerAPI({
|
|
|
|
|
config: {
|
|
|
|
|
untrigger_seconds: 0,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
// Trigger then end, but don't await the end event so we can check
|
|
|
|
|
// triggered IDs while the camera is in the map but no longer triggered.
|
|
|
|
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e1', type: 'new' });
|
|
|
|
|
const untriggerPromise = manager.handleCameraEvent({
|
|
|
|
|
cameraID: 'camera_1',
|
|
|
|
|
id: 'e1',
|
|
|
|
|
type: 'end',
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
expect(manager.getTriggeredCameraIDs()).toEqual(new Set());
|
|
|
|
|
|
|
|
|
|
await untriggerPromise;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should handle newly missing configuration', async () => {
|
|
|
|
|
const api = createTriggerAPI();
|
|
|
|
|
const manager = new TriggersManager(api);
|
|
|
|
|
|
|
|
|
|
// 1. Trigger the camera with valid config.
|
|
|
|
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e1', type: 'new' });
|
|
|
|
|
|
|
|
|
|
// 2. Mock getConfig to return null.
|
|
|
|
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(null);
|
|
|
|
|
|
|
|
|
|
// 3. End the trigger. This should now hit the fallback '?? 0' in
|
|
|
|
|
// _startUntrigger because getConfig() is null.
|
|
|
|
|
await manager.handleCameraEvent({ cameraID: 'camera_1', id: 'e1', type: 'end' });
|
|
|
|
|
|
|
|
|
|
// Should NOT have triggered a view reset (since config is missing, no
|
|
|
|
|
// untrigger action is defined).
|
|
|
|
|
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toHaveBeenCalled();
|
|
|
|
|
|
|
|
|
|
// But the camera should be untriggered.
|
|
|
|
|
expect(manager.isTriggered()).toBe(false);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|