Initial version of more advanced scan mode control.
This commit is contained in:
@@ -15,6 +15,28 @@ import {
|
||||
createView,
|
||||
} from '../test-utils';
|
||||
|
||||
const baseScanConfig: Partial<ScanOptions> = {
|
||||
enabled: true,
|
||||
untrigger_seconds: 10,
|
||||
interaction_mode: 'inactive' as const,
|
||||
trigger_filter_camera: 'all' as const,
|
||||
trigger_action: [
|
||||
{
|
||||
action: 'fire-dom-event' as const,
|
||||
frigate_card_action: 'camera_select' as const,
|
||||
triggered: true,
|
||||
},
|
||||
{
|
||||
action: 'fire-dom-event' as const,
|
||||
frigate_card_action: 'live' as const,
|
||||
},
|
||||
],
|
||||
untrigger_action: {
|
||||
action: 'fire-dom-event' as const,
|
||||
frigate_card_action: 'default' as const,
|
||||
},
|
||||
};
|
||||
|
||||
// Creating and mocking a trigger API is a lot of boilerplate, this convenience
|
||||
// function reduces it.
|
||||
const createTriggerAPI = (options?: {
|
||||
@@ -26,11 +48,7 @@ const createTriggerAPI = (options?: {
|
||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||
createConfig({
|
||||
view: {
|
||||
scan: options?.config ?? {
|
||||
enabled: true,
|
||||
untrigger_reset: true,
|
||||
untrigger_seconds: 10,
|
||||
},
|
||||
scan: options?.config ?? baseScanConfig,
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -83,11 +101,28 @@ describe('TriggersManager', () => {
|
||||
});
|
||||
const manager = new TriggersManager(api);
|
||||
|
||||
manager.updateTriggeredCameras(null);
|
||||
manager.updateTriggerHAState(null);
|
||||
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should not trigger without a scan config', () => {
|
||||
const api = createTriggerAPI({
|
||||
hassStates: hassActiveState,
|
||||
});
|
||||
|
||||
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.updateView(null);
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should trigger and untrigger based on entity state', () => {
|
||||
const start = new Date('2023-10-01T17:14');
|
||||
vi.setSystemTime(start);
|
||||
@@ -96,22 +131,29 @@ describe('TriggersManager', () => {
|
||||
});
|
||||
const manager = new TriggersManager(api);
|
||||
|
||||
manager.updateTriggeredCameras(createHASS(hassInactiveState));
|
||||
manager.updateTriggerHAState(createHASS(hassInactiveState));
|
||||
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
||||
viewName: 'live',
|
||||
cameraID: 'camera_1',
|
||||
});
|
||||
expect(api.getActionsManager().executeActions).toBeCalledWith([
|
||||
{
|
||||
action: 'fire-dom-event' as const,
|
||||
frigate_card_action: 'camera_select' as const,
|
||||
triggered: true,
|
||||
},
|
||||
{
|
||||
action: 'fire-dom-event' as const,
|
||||
frigate_card_action: 'live' as const,
|
||||
},
|
||||
]);
|
||||
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
||||
createHASS(hassInactiveState),
|
||||
);
|
||||
|
||||
manager.updateTriggeredCameras(createHASS(hassActiveState));
|
||||
manager.updateTriggerHAState(createHASS(hassActiveState));
|
||||
|
||||
// Intentional state update with no change.
|
||||
manager.updateTriggeredCameras(createHASS(hassActiveState));
|
||||
manager.updateTriggerHAState(createHASS(hassActiveState));
|
||||
|
||||
// Will still be triggered, but untrigger timer will be running.
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
@@ -120,26 +162,10 @@ describe('TriggersManager', () => {
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
||||
});
|
||||
|
||||
it('should trigger and set view if current view is wrong', () => {
|
||||
const api = createTriggerAPI({
|
||||
hassStates: hassActiveState,
|
||||
});
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||
createView({
|
||||
// Correct camera, but wrong view.
|
||||
view: 'clips',
|
||||
camera: 'camera_1',
|
||||
}),
|
||||
);
|
||||
const manager = new TriggersManager(api);
|
||||
manager.updateTriggeredCameras(null);
|
||||
|
||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
||||
viewName: 'live',
|
||||
cameraID: 'camera_1',
|
||||
expect(api.getActionsManager().executeActions).toBeCalledWith({
|
||||
action: 'fire-dom-event' as const,
|
||||
frigate_card_action: 'default' as const,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -150,40 +176,196 @@ describe('TriggersManager', () => {
|
||||
const manager = new TriggersManager(api);
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
|
||||
manager.updateTriggeredCameras(null);
|
||||
manager.updateTriggerHAState(null);
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should untrigger manually', () => {
|
||||
const api = createTriggerAPI({
|
||||
hassStates: hassActiveState,
|
||||
});
|
||||
const manager = new TriggersManager(api);
|
||||
|
||||
// Untriggering when not triggered.
|
||||
manager.untrigger();
|
||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||
|
||||
manager.updateTriggeredCameras(null);
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
|
||||
manager.untrigger();
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
||||
});
|
||||
|
||||
it('should take no actions when automated actions are not allowed', () => {
|
||||
it('should take no actions with human interactions', () => {
|
||||
const start = new Date('2023-10-01T17:14');
|
||||
const api = createTriggerAPI({
|
||||
hassStates: hassActiveState,
|
||||
// Interaction present.
|
||||
interaction: true,
|
||||
});
|
||||
const manager = new TriggersManager(api);
|
||||
manager.updateTriggeredCameras(null);
|
||||
manager.updateTriggerHAState(createHASS(hassInactiveState));
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||
|
||||
manager.untrigger();
|
||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
||||
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
||||
createHASS(hassInactiveState),
|
||||
);
|
||||
manager.updateTriggerHAState(createHASS(hassActiveState));
|
||||
|
||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
|
||||
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should take actions with human interactions when interaction mode is active', () => {
|
||||
const start = new Date('2023-10-01T17:14');
|
||||
const api = createTriggerAPI({
|
||||
hassStates: hassActiveState,
|
||||
// Interaction present.
|
||||
interaction: true,
|
||||
config: {
|
||||
...baseScanConfig,
|
||||
interaction_mode: 'active',
|
||||
},
|
||||
});
|
||||
const manager = new TriggersManager(api);
|
||||
manager.updateTriggerHAState(createHASS(hassInactiveState));
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
|
||||
expect(api.getActionsManager().executeActions).toBeCalledTimes(1);
|
||||
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
||||
createHASS(hassInactiveState),
|
||||
);
|
||||
manager.updateTriggerHAState(createHASS(hassActiveState));
|
||||
|
||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
|
||||
expect(api.getActionsManager().executeActions).toBeCalledTimes(2);
|
||||
});
|
||||
|
||||
it('should report multiple triggered cameras', () => {
|
||||
const start = new Date('2023-10-01T17:14');
|
||||
const bothOnState = {
|
||||
'binary_sensor.one': createStateEntity({ state: 'on' }),
|
||||
'binary_sensor.two': createStateEntity({ state: 'on' }),
|
||||
};
|
||||
const api = createTriggerAPI({
|
||||
hassStates: bothOnState,
|
||||
});
|
||||
vi.mocked(api.getCameraManager().getStore).mockReturnValue(
|
||||
createStore([
|
||||
{
|
||||
cameraID: 'camera_1',
|
||||
config: createCameraConfig({
|
||||
triggers: {
|
||||
entities: ['binary_sensor.one'],
|
||||
},
|
||||
}),
|
||||
},
|
||||
{
|
||||
cameraID: 'camera_2',
|
||||
config: createCameraConfig({
|
||||
triggers: {
|
||||
entities: ['binary_sensor.two'],
|
||||
},
|
||||
}),
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const manager = new TriggersManager(api);
|
||||
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
expect(manager.getMostRecentlyTriggeredCameraID()).toBeNull();
|
||||
expect(manager.getTriggeredCameraIDs()).toEqual(new Set());
|
||||
|
||||
manager.updateTriggerHAState(null);
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
expect(manager.getTriggeredCameraIDs()).toEqual(new Set(['camera_1', 'camera_2']));
|
||||
|
||||
// Either is the most recently triggered.
|
||||
expect(['camera_1', 'camera_2']).toContain(
|
||||
manager.getMostRecentlyTriggeredCameraID(),
|
||||
);
|
||||
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
||||
createHASS({
|
||||
'binary_sensor.one': createStateEntity({ state: 'off' }),
|
||||
'binary_sensor.two': createStateEntity({ state: 'on' }),
|
||||
}),
|
||||
);
|
||||
|
||||
manager.updateTriggerHAState(createHASS(bothOnState));
|
||||
|
||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||
vi.runOnlyPendingTimers();
|
||||
|
||||
expect(manager.getTriggeredCameraIDs()).toEqual(new Set(['camera_2']));
|
||||
expect(manager.getMostRecentlyTriggeredCameraID()).toBe('camera_2');
|
||||
});
|
||||
|
||||
it('should filter triggers by camera', () => {
|
||||
const start = new Date('2023-10-01T17:14');
|
||||
const api = createTriggerAPI({
|
||||
config: {
|
||||
...baseScanConfig,
|
||||
// Filter triggers to selected camera only.
|
||||
trigger_filter_camera: 'selected' as const,
|
||||
},
|
||||
hassStates: hassActiveState,
|
||||
});
|
||||
const manager = new TriggersManager(api);
|
||||
manager.updateTriggerHAState(createHASS(hassInactiveState));
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
|
||||
const otherCameraSelected = createView({
|
||||
camera: 'camera_SOME_OTHER_CAMERA' as const,
|
||||
});
|
||||
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(otherCameraSelected);
|
||||
manager.updateView(null);
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
|
||||
const thisCameraSelected = createView({
|
||||
camera: 'camera_1' as const,
|
||||
});
|
||||
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(thisCameraSelected);
|
||||
manager.updateView(otherCameraSelected);
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
|
||||
// Ensure a view change to the same camera stays triggered.
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(thisCameraSelected);
|
||||
manager.updateView(thisCameraSelected);
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should not untrigger triggers by camera', () => {
|
||||
const start = new Date('2023-10-01T17:14');
|
||||
const api = createTriggerAPI({
|
||||
config: {
|
||||
...baseScanConfig,
|
||||
// Filter triggers to selected camera only.
|
||||
trigger_filter_camera: 'selected' as const,
|
||||
},
|
||||
hassStates: hassActiveState,
|
||||
});
|
||||
const manager = new TriggersManager(api);
|
||||
manager.updateTriggerHAState(createHASS(hassInactiveState));
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
|
||||
const otherCameraSelected = createView({
|
||||
camera: 'camera_SOME_OTHER_CAMERA' as const,
|
||||
});
|
||||
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(otherCameraSelected);
|
||||
manager.updateView(null);
|
||||
expect(manager.isTriggered()).toBeFalsy();
|
||||
|
||||
const thisCameraSelected = createView({
|
||||
camera: 'camera_1' as const,
|
||||
});
|
||||
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(thisCameraSelected);
|
||||
manager.updateView(otherCameraSelected);
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
|
||||
// Ensure a view change to the same camera stays triggered.
|
||||
vi.mocked(api.getViewManager().getView).mockReturnValue(thisCameraSelected);
|
||||
manager.updateView(thisCameraSelected);
|
||||
expect(manager.isTriggered()).toBeTruthy();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user