fix: Don't incorrectly change camera when a dependency supports media (#2171)
- Closes: #2122
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
|||||||
capabilityKeys,
|
capabilityKeys,
|
||||||
PTZCapabilities,
|
PTZCapabilities,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { CapabilitySearchOptions } from './types';
|
import { CapabilitySearchKeys } from './types';
|
||||||
|
|
||||||
export class Capabilities {
|
export class Capabilities {
|
||||||
private _capabilities: CapabilitiesRaw;
|
private _capabilities: CapabilitiesRaw;
|
||||||
@@ -32,7 +32,7 @@ export class Capabilities {
|
|||||||
delete this._capabilities[capability];
|
delete this._capabilities[capability];
|
||||||
}
|
}
|
||||||
|
|
||||||
public matches(capability: CapabilitySearchOptions): boolean {
|
public matches(capability: CapabilitySearchKeys): boolean {
|
||||||
let result = true;
|
let result = true;
|
||||||
if (typeof capability === 'string') {
|
if (typeof capability === 'string') {
|
||||||
result &&= this.has(capability);
|
result &&= this.has(capability);
|
||||||
|
|||||||
+54
-25
@@ -4,7 +4,7 @@ import { allPromises } from '../utils/basic';
|
|||||||
import { ViewMedia } from '../view/item';
|
import { ViewMedia } from '../view/item';
|
||||||
import { Camera } from './camera';
|
import { Camera } from './camera';
|
||||||
import { CameraManagerEngine } from './engine';
|
import { CameraManagerEngine } from './engine';
|
||||||
import { CapabilitySearchOptions, Engine } from './types';
|
import { CapabilitySearchKeys, CapabilitySearchOptions, Engine } from './types';
|
||||||
|
|
||||||
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
|
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
|
||||||
|
|
||||||
@@ -27,11 +27,13 @@ export interface CameraManagerReadOnlyConfigStore {
|
|||||||
getDefaultCameraID(): string | null;
|
getDefaultCameraID(): string | null;
|
||||||
|
|
||||||
getCameraIDsWithCapability(
|
getCameraIDsWithCapability(
|
||||||
capability: CapabilityKey | CapabilitySearchOptions,
|
capability: CapabilitySearchKeys,
|
||||||
|
options?: CapabilitySearchOptions,
|
||||||
): Set<string>;
|
): Set<string>;
|
||||||
getAllDependentCameras(
|
getAllDependentCameras(
|
||||||
cameraID: string,
|
cameraID: string,
|
||||||
capability?: CapabilityKey | CapabilitySearchOptions,
|
capability?: CapabilitySearchKeys,
|
||||||
|
options?: CapabilitySearchOptions,
|
||||||
): Set<string>;
|
): Set<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,14 +123,23 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getCameraIDsWithCapability(
|
public getCameraIDsWithCapability(
|
||||||
capability: CapabilityKey | CapabilitySearchOptions,
|
capability: CapabilityKey | CapabilitySearchKeys,
|
||||||
|
options?: CapabilitySearchOptions,
|
||||||
): Set<string> {
|
): Set<string> {
|
||||||
const output: Set<string> = new Set();
|
const output: Set<string> = new Set();
|
||||||
|
|
||||||
for (const camera of this._cameras.values()) {
|
for (const camera of this._cameras.values()) {
|
||||||
if (camera.getCapabilities()?.matches(capability)) {
|
// Must use getAllDependentCameras() to recursively get all relevant
|
||||||
output.add(camera.getID());
|
// cameras respecting the capabilitiy.
|
||||||
}
|
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2122
|
||||||
|
|
||||||
|
this.getAllDependentCameras(camera.getID(), capability, options).forEach(
|
||||||
|
(cameraID) => {
|
||||||
|
output.add(cameraID);
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return output;
|
return output;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,33 +188,51 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
|||||||
*/
|
*/
|
||||||
public getAllDependentCameras(
|
public getAllDependentCameras(
|
||||||
cameraID: string,
|
cameraID: string,
|
||||||
capability?: CapabilitySearchOptions,
|
capabilitySearchKeys?: CapabilitySearchKeys,
|
||||||
|
options?: CapabilitySearchOptions,
|
||||||
): Set<string> {
|
): Set<string> {
|
||||||
const visitedCameraIDs = new Set<string>();
|
const visitedCameraIDs = new Set<string>();
|
||||||
const matchingCameraIDs: Set<string> = new Set();
|
|
||||||
const getDependentCameras = (cameraID: string): void => {
|
const getDependentCameras = (cameraID: string): Set<string> => {
|
||||||
visitedCameraIDs.add(cameraID);
|
visitedCameraIDs.add(cameraID);
|
||||||
|
|
||||||
|
const matchingCameraIDs: Set<string> = new Set();
|
||||||
|
|
||||||
const camera = this.getCamera(cameraID);
|
const camera = this.getCamera(cameraID);
|
||||||
const cameraConfig = camera?.getConfig();
|
const cameraConfig = camera?.getConfig();
|
||||||
|
|
||||||
if (camera && cameraConfig) {
|
if (!camera || !cameraConfig) {
|
||||||
if (!capability || camera.getCapabilities()?.matches(capability)) {
|
return matchingCameraIDs;
|
||||||
matchingCameraIDs.add(cameraID);
|
}
|
||||||
}
|
|
||||||
const dependentCameras: Set<string> = new Set();
|
// Gather all dependent cameras...
|
||||||
cameraConfig.dependencies.cameras.forEach((item) => dependentCameras.add(item));
|
const dependentCameras: Set<string> = new Set();
|
||||||
if (cameraConfig.dependencies.all_cameras) {
|
cameraConfig.dependencies.cameras.forEach((item) => dependentCameras.add(item));
|
||||||
this.getCameraIDs().forEach((cameraID) => dependentCameras.add(cameraID));
|
if (cameraConfig.dependencies.all_cameras) {
|
||||||
}
|
this.getCameraIDs().forEach((cameraID) => dependentCameras.add(cameraID));
|
||||||
for (const dependentCameraID of dependentCameras) {
|
}
|
||||||
if (!visitedCameraIDs.has(dependentCameraID)) {
|
|
||||||
getDependentCameras(dependentCameraID);
|
const matchingChildCameraIDs: Set<string> = new Set();
|
||||||
}
|
|
||||||
|
// ...now recurse through them.
|
||||||
|
for (const dependentCameraID of dependentCameras) {
|
||||||
|
if (!visitedCameraIDs.has(dependentCameraID)) {
|
||||||
|
getDependentCameras(dependentCameraID).forEach((dependentCameraID) =>
|
||||||
|
matchingChildCameraIDs.add(dependentCameraID),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return new Set([
|
||||||
|
...(!capabilitySearchKeys ||
|
||||||
|
camera.getCapabilities()?.matches(capabilitySearchKeys) ||
|
||||||
|
(options?.inclusive && matchingChildCameraIDs.size)
|
||||||
|
? [cameraID]
|
||||||
|
: []),
|
||||||
|
...matchingChildCameraIDs,
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
getDependentCameras(cameraID);
|
|
||||||
return matchingCameraIDs;
|
return getDependentCameras(cameraID);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -99,7 +99,10 @@ interface CapabilitySearchAllAny {
|
|||||||
allCapabilities?: CapabilityKey[];
|
allCapabilities?: CapabilityKey[];
|
||||||
anyCapabilities?: CapabilityKey[];
|
anyCapabilities?: CapabilityKey[];
|
||||||
}
|
}
|
||||||
export type CapabilitySearchOptions = CapabilityKey | CapabilitySearchAllAny;
|
export type CapabilitySearchKeys = CapabilityKey | CapabilitySearchAllAny;
|
||||||
|
export interface CapabilitySearchOptions {
|
||||||
|
inclusive?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CameraManagerCameraMetadata {
|
export interface CameraManagerCameraMetadata {
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CapabilitySearchOptions, MediaQuery } from '../../camera-manager/types';
|
import { CapabilitySearchKeys, MediaQuery } from '../../camera-manager/types';
|
||||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../../const';
|
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../../const';
|
||||||
import { ClipsOrSnapshotsOrAll } from '../../types';
|
import { ClipsOrSnapshotsOrAll } from '../../types';
|
||||||
import { findBestMediaTimeIndex } from '../../utils/find-best-media-time-index';
|
import { findBestMediaTimeIndex } from '../../utils/find-best-media-time-index';
|
||||||
@@ -27,7 +27,7 @@ export class QueryExecutor {
|
|||||||
eventsMediaType?: ClipsOrSnapshotsOrAll;
|
eventsMediaType?: ClipsOrSnapshotsOrAll;
|
||||||
executorOptions?: QueryExecutorOptions;
|
executorOptions?: QueryExecutorOptions;
|
||||||
}): Promise<QueryExecutorResult | null> {
|
}): Promise<QueryExecutorResult | null> {
|
||||||
const capabilitySearch: CapabilitySearchOptions =
|
const capabilitySearch: CapabilitySearchKeys =
|
||||||
!options?.eventsMediaType || options?.eventsMediaType === 'all'
|
!options?.eventsMediaType || options?.eventsMediaType === 'all'
|
||||||
? {
|
? {
|
||||||
anyCapabilities: ['clips', 'snapshots'],
|
anyCapabilities: ['clips', 'snapshots'],
|
||||||
|
|||||||
@@ -11,10 +11,6 @@ export const getCameraIDsForViewName = (
|
|||||||
viewName: AdvancedCameraCardView,
|
viewName: AdvancedCameraCardView,
|
||||||
cameraID?: string,
|
cameraID?: string,
|
||||||
): Set<string> => {
|
): Set<string> => {
|
||||||
const capabilityMatchAnyMedia: CapabilitySearchOptions = {
|
|
||||||
anyCapabilities: ['clips', 'snapshots', 'recordings'],
|
|
||||||
};
|
|
||||||
|
|
||||||
switch (viewName) {
|
switch (viewName) {
|
||||||
case 'diagnostics':
|
case 'diagnostics':
|
||||||
case 'image':
|
case 'image':
|
||||||
@@ -30,6 +26,9 @@ export const getCameraIDsForViewName = (
|
|||||||
case 'snapshots':
|
case 'snapshots':
|
||||||
case 'recording':
|
case 'recording':
|
||||||
case 'recordings':
|
case 'recordings':
|
||||||
|
const options: CapabilitySearchOptions = {
|
||||||
|
inclusive: viewName !== 'live',
|
||||||
|
};
|
||||||
const capability =
|
const capability =
|
||||||
viewName === 'clip'
|
viewName === 'clip'
|
||||||
? 'clips'
|
? 'clips'
|
||||||
@@ -39,12 +38,12 @@ export const getCameraIDsForViewName = (
|
|||||||
? 'recordings'
|
? 'recordings'
|
||||||
: viewName;
|
: viewName;
|
||||||
return cameraID
|
return cameraID
|
||||||
? cameraManager.getStore().getAllDependentCameras(cameraID, capability)
|
? cameraManager.getStore().getAllDependentCameras(cameraID, capability, options)
|
||||||
: cameraManager.getStore().getCameraIDsWithCapability(capability);
|
: cameraManager.getStore().getCameraIDsWithCapability(capability, options);
|
||||||
|
|
||||||
case 'timeline':
|
case 'timeline':
|
||||||
return cameraManager
|
return cameraManager.getStore().getCameraIDsWithCapability({
|
||||||
.getStore()
|
anyCapabilities: ['clips', 'snapshots', 'recordings'],
|
||||||
.getCameraIDsWithCapability(capabilityMatchAnyMedia);
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -297,6 +297,37 @@ describe('CameraManagerStore', async () => {
|
|||||||
);
|
);
|
||||||
expect(store.getAllDependentCameras('one', 'clips')).toEqual(new Set(['two']));
|
expect(store.getAllDependentCameras('one', 'clips')).toEqual(new Set(['two']));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should return cameras with specific capabilities inclusive of parent', () => {
|
||||||
|
const store = new CameraManagerStore();
|
||||||
|
store.addCamera(
|
||||||
|
new Camera(
|
||||||
|
createCameraConfig({
|
||||||
|
id: 'one',
|
||||||
|
dependencies: {
|
||||||
|
all_cameras: true,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
engineGeneric,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
store.addCamera(
|
||||||
|
new Camera(
|
||||||
|
createCameraConfig({
|
||||||
|
id: 'two',
|
||||||
|
}),
|
||||||
|
engineGeneric,
|
||||||
|
{
|
||||||
|
capabilities: new Capabilities({
|
||||||
|
clips: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
expect(store.getAllDependentCameras('one', 'clips', { inclusive: true })).toEqual(
|
||||||
|
new Set(['one', 'two']),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getCameraIDsWithCapability', () => {
|
it('getCameraIDsWithCapability', () => {
|
||||||
|
|||||||
@@ -44,15 +44,9 @@ describe('getCameraIDsForViewName', () => {
|
|||||||
describe('views that respect dependencies and need a capability', () => {
|
describe('views that respect dependencies and need a capability', () => {
|
||||||
it.each([
|
it.each([
|
||||||
['live' as const, 'live' as const],
|
['live' as const, 'live' as const],
|
||||||
['clip' as const, 'clips' as const],
|
|
||||||
['clips' as const, 'clips' as const],
|
|
||||||
['snapshot' as const, 'snapshots' as const],
|
|
||||||
['snapshots' as const, 'snapshots' as const],
|
|
||||||
['recording' as const, 'recordings' as const],
|
|
||||||
['recordings' as const, 'recordings' as const],
|
|
||||||
['timeline' as const, 'clips' as const],
|
['timeline' as const, 'clips' as const],
|
||||||
['timeline' as const, 'snapshots' as const],
|
|
||||||
['timeline' as const, 'recordings' as const],
|
['timeline' as const, 'recordings' as const],
|
||||||
|
['timeline' as const, 'snapshots' as const],
|
||||||
])('%s', (viewName: AdvancedCameraCardView, capabilityKey: CapabilityKey) => {
|
])('%s', (viewName: AdvancedCameraCardView, capabilityKey: CapabilityKey) => {
|
||||||
const cameraManager = createCameraManager();
|
const cameraManager = createCameraManager();
|
||||||
vi.mocked(cameraManager.getStore).mockReturnValue(
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
@@ -71,8 +65,35 @@ describe('getCameraIDsForViewName', () => {
|
|||||||
expect(getCameraIDsForViewName(cameraManager, viewName)).toEqual(
|
expect(getCameraIDsForViewName(cameraManager, viewName)).toEqual(
|
||||||
new Set(['camera-2']),
|
new Set(['camera-2']),
|
||||||
);
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each([
|
||||||
|
['clip' as const, 'clips' as const],
|
||||||
|
['clips' as const, 'clips' as const],
|
||||||
|
['snapshot' as const, 'snapshots' as const],
|
||||||
|
['snapshots' as const, 'snapshots' as const],
|
||||||
|
['recording' as const, 'recordings' as const],
|
||||||
|
['recordings' as const, 'recordings' as const],
|
||||||
|
])('%s', (viewName: AdvancedCameraCardView, capabilityKey: CapabilityKey) => {
|
||||||
|
const cameraManager = createCameraManager();
|
||||||
|
vi.mocked(cameraManager.getStore).mockReturnValue(
|
||||||
|
createStore([
|
||||||
|
{
|
||||||
|
cameraID: 'camera-1',
|
||||||
|
config: createCameraConfig({ dependencies: { cameras: ['camera-2'] } }),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
cameraID: 'camera-2',
|
||||||
|
capabilities: createCapabilities({ [capabilityKey]: true }),
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(getCameraIDsForViewName(cameraManager, viewName)).toEqual(
|
||||||
|
new Set(['camera-1', 'camera-2']),
|
||||||
|
);
|
||||||
expect(getCameraIDsForViewName(cameraManager, viewName, 'camera-1')).toEqual(
|
expect(getCameraIDsForViewName(cameraManager, viewName, 'camera-1')).toEqual(
|
||||||
new Set(['camera-2']),
|
new Set(['camera-1', 'camera-2']),
|
||||||
);
|
);
|
||||||
expect(getCameraIDsForViewName(cameraManager, viewName, 'camera-2')).toEqual(
|
expect(getCameraIDsForViewName(cameraManager, viewName, 'camera-2')).toEqual(
|
||||||
new Set(['camera-2']),
|
new Set(['camera-2']),
|
||||||
|
|||||||
Reference in New Issue
Block a user