Initial version of more advanced scan mode control.

This commit is contained in:
Dermot Duffy
2024-01-17 22:07:30 -08:00
parent ad1c323cb0
commit 278f4f372d
24 changed files with 648 additions and 327 deletions
+238 -56
View File
@@ -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();
});
});