feat: Make cameras optional (#2343)
This commit is contained in:
+198
-118
@@ -1,15 +1,22 @@
|
||||
import { AdvancedCameraCardView, VIEW_DEFAULT } from '../../config/schema/common/const';
|
||||
import { AdvancedCameraCardView } from '../../config/schema/common/const';
|
||||
import { ViewDisplayMode } from '../../config/schema/common/display';
|
||||
import { AdvancedCameraCardConfig } from '../../config/schema/types';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { resolveViewName } from '../../view/utils/resolve-default';
|
||||
import { View, ViewParameters } from '../../view/view';
|
||||
import {
|
||||
getCameraIDsForViewName,
|
||||
doesViewRequireCamera,
|
||||
getCameraIDsWithCapabilityForView,
|
||||
isViewSupportedByCamera,
|
||||
} from '../../view/view-support';
|
||||
import { CardViewAPI } from '../types';
|
||||
import { applyViewModifiers } from './modifiers';
|
||||
import { ViewFactoryOptions, ViewIncompatible, ViewNoCameraError } from './types';
|
||||
import { ViewFactoryOptions, ViewIncompatible } from './types';
|
||||
|
||||
interface ResolvedViewTarget {
|
||||
viewName: AdvancedCameraCardView;
|
||||
cameraID: string | null;
|
||||
}
|
||||
|
||||
export class ViewFactory {
|
||||
protected _api: CardViewAPI;
|
||||
@@ -24,130 +31,72 @@ export class ViewFactory {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Neither options.baseView.camera nor options.baseView.view are respected
|
||||
// here, since this is the default view / camera.
|
||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1564
|
||||
|
||||
let cameraID: string | null = null;
|
||||
const viewName = options?.params?.view ?? config.view.default;
|
||||
|
||||
if (options?.params?.camera) {
|
||||
cameraID = options.params.camera;
|
||||
} else {
|
||||
const cameraIDs = [
|
||||
...getCameraIDsForViewName(
|
||||
viewName,
|
||||
this._api.getCameraManager(),
|
||||
this._api.getFoldersManager(),
|
||||
),
|
||||
];
|
||||
|
||||
if (
|
||||
cameraIDs.length &&
|
||||
options?.baseView?.camera &&
|
||||
config.view.default_cycle_camera
|
||||
) {
|
||||
const currentIndex = cameraIDs.indexOf(options.baseView.camera);
|
||||
const targetIndex = currentIndex + 1 >= cameraIDs.length ? 0 : currentIndex + 1;
|
||||
cameraID = cameraIDs[targetIndex];
|
||||
} else {
|
||||
cameraID = cameraIDs[0] ?? null;
|
||||
}
|
||||
}
|
||||
const viewName = this._getDefaultViewName(config);
|
||||
|
||||
return this.getViewByParameters({
|
||||
params: {
|
||||
...options?.params,
|
||||
view: viewName,
|
||||
camera: cameraID,
|
||||
camera: this._getDefaultCameraID(config, viewName, options),
|
||||
},
|
||||
baseView: options?.baseView,
|
||||
});
|
||||
}
|
||||
|
||||
protected _getDefaultViewName = (
|
||||
config: AdvancedCameraCardConfig,
|
||||
): AdvancedCameraCardView =>
|
||||
resolveViewName(
|
||||
config.view.default,
|
||||
this._api.getCameraManager(),
|
||||
this._api.getFoldersManager(),
|
||||
);
|
||||
|
||||
protected _getDefaultCameraID(
|
||||
config: AdvancedCameraCardConfig,
|
||||
viewName: AdvancedCameraCardView,
|
||||
options?: ViewFactoryOptions,
|
||||
): string | null {
|
||||
if (options?.params?.camera) {
|
||||
return options.params.camera;
|
||||
}
|
||||
|
||||
const cameraIDs = [
|
||||
...getCameraIDsWithCapabilityForView(
|
||||
viewName,
|
||||
this._api.getCameraManager(),
|
||||
this._api.getFoldersManager(),
|
||||
),
|
||||
];
|
||||
|
||||
if (
|
||||
cameraIDs.length &&
|
||||
options?.baseView?.camera &&
|
||||
config.view.default_cycle_camera
|
||||
) {
|
||||
const currentIndex = cameraIDs.indexOf(options.baseView.camera);
|
||||
const targetIndex = currentIndex + 1 >= cameraIDs.length ? 0 : currentIndex + 1;
|
||||
return cameraIDs[targetIndex];
|
||||
}
|
||||
|
||||
return cameraIDs[0] ?? null;
|
||||
}
|
||||
|
||||
public getViewByParameters(options?: ViewFactoryOptions): View | null {
|
||||
const config = this._api.getConfigManager().getConfig();
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let cameraID: string | null =
|
||||
options?.params?.camera ?? options?.baseView?.camera ?? null;
|
||||
let viewName =
|
||||
options?.params?.view ?? options?.baseView?.view ?? config.view.default;
|
||||
|
||||
const allCameraIDs = this._api.getCameraManager().getStore().getCameraIDs();
|
||||
|
||||
if (!cameraID || !allCameraIDs.has(cameraID)) {
|
||||
const viewCameraIDs = getCameraIDsForViewName(
|
||||
viewName,
|
||||
this._api.getCameraManager(),
|
||||
this._api.getFoldersManager(),
|
||||
);
|
||||
|
||||
// Reset to the default camera.
|
||||
cameraID = viewCameraIDs?.keys().next().value ?? null;
|
||||
}
|
||||
|
||||
if (!cameraID) {
|
||||
const camerasToCapabilities = [
|
||||
...this._api.getCameraManager().getStore().getCameras(),
|
||||
].reduce((acc, [cameraID, camera]) => {
|
||||
const capabilities = camera.getCapabilities()?.getRawCapabilities();
|
||||
if (capabilities) {
|
||||
acc[cameraID] = capabilities;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
throw new ViewNoCameraError(localize('error.no_supported_cameras'), {
|
||||
view: viewName,
|
||||
cameras_capabilities: camerasToCapabilities,
|
||||
});
|
||||
}
|
||||
|
||||
if (
|
||||
!isViewSupportedByCamera(
|
||||
viewName,
|
||||
this._api.getCameraManager(),
|
||||
this._api.getFoldersManager(),
|
||||
cameraID,
|
||||
)
|
||||
) {
|
||||
if (
|
||||
options?.failSafe &&
|
||||
isViewSupportedByCamera(
|
||||
VIEW_DEFAULT,
|
||||
this._api.getCameraManager(),
|
||||
this._api.getFoldersManager(),
|
||||
cameraID,
|
||||
)
|
||||
) {
|
||||
viewName = VIEW_DEFAULT;
|
||||
} else {
|
||||
const capabilities = this._api
|
||||
.getCameraManager()
|
||||
.getStore()
|
||||
.getCamera(cameraID)
|
||||
?.getCapabilities()
|
||||
?.getRawCapabilities();
|
||||
|
||||
throw new ViewIncompatible(localize('error.no_supported_camera'), {
|
||||
view: viewName,
|
||||
camera: cameraID,
|
||||
...(capabilities && { camera_capabilities: capabilities }),
|
||||
});
|
||||
}
|
||||
}
|
||||
const configuredDisplayMode = this._getDefaultDisplayModeForView(viewName, config);
|
||||
const displayMode =
|
||||
// Prioritize the configured display mode (if present).
|
||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1812
|
||||
(viewName !== options?.baseView?.view ? configuredDisplayMode : null) ??
|
||||
options?.params?.displayMode ??
|
||||
options?.baseView?.displayMode ??
|
||||
configuredDisplayMode ??
|
||||
'single';
|
||||
let viewName = this._resolveViewName(config, options);
|
||||
let cameraID = this._resolveCameraID(viewName, options);
|
||||
({ viewName, cameraID } = this._ensureViewCompatibility(
|
||||
viewName,
|
||||
cameraID,
|
||||
config,
|
||||
options,
|
||||
));
|
||||
const displayMode = this._resolveDisplayMode(viewName, config, options);
|
||||
|
||||
const viewParameters: ViewParameters = {
|
||||
...options?.params,
|
||||
@@ -165,22 +114,153 @@ export class ViewFactory {
|
||||
return view;
|
||||
}
|
||||
|
||||
protected _getDefaultDisplayModeForView(
|
||||
protected _resolveViewName(
|
||||
config: AdvancedCameraCardConfig,
|
||||
options?: ViewFactoryOptions,
|
||||
): AdvancedCameraCardView {
|
||||
if (options?.params?.view !== undefined) {
|
||||
return resolveViewName(
|
||||
options.params.view,
|
||||
this._api.getCameraManager(),
|
||||
this._api.getFoldersManager(),
|
||||
);
|
||||
}
|
||||
return options?.baseView?.view ?? this._getDefaultViewName(config);
|
||||
}
|
||||
|
||||
protected _resolveCameraID(
|
||||
viewName: AdvancedCameraCardView,
|
||||
options?: ViewFactoryOptions,
|
||||
): string | null {
|
||||
const cameraID = options?.params?.camera ?? options?.baseView?.camera ?? null;
|
||||
const allCameraIDs = this._api.getCameraManager().getStore().getCameraIDs();
|
||||
|
||||
if (cameraID && allCameraIDs.has(cameraID)) {
|
||||
return cameraID;
|
||||
}
|
||||
|
||||
const viewCameraIDs = getCameraIDsWithCapabilityForView(
|
||||
viewName,
|
||||
this._api.getCameraManager(),
|
||||
this._api.getFoldersManager(),
|
||||
);
|
||||
|
||||
return viewCameraIDs?.keys().next().value ?? null;
|
||||
}
|
||||
|
||||
protected _ensureViewCompatibility(
|
||||
viewName: AdvancedCameraCardView,
|
||||
cameraID: string | null,
|
||||
config: AdvancedCameraCardConfig,
|
||||
options?: ViewFactoryOptions,
|
||||
): ResolvedViewTarget {
|
||||
if (!cameraID && doesViewRequireCamera(viewName)) {
|
||||
return this._handleNoCameraForView(viewName, config, options);
|
||||
}
|
||||
|
||||
if (
|
||||
cameraID &&
|
||||
!isViewSupportedByCamera(
|
||||
viewName,
|
||||
this._api.getCameraManager(),
|
||||
this._api.getFoldersManager(),
|
||||
cameraID,
|
||||
)
|
||||
) {
|
||||
return this._handleUnsupportedView(viewName, cameraID, config, options);
|
||||
}
|
||||
|
||||
return { viewName, cameraID };
|
||||
}
|
||||
|
||||
protected _handleNoCameraForView(
|
||||
viewName: AdvancedCameraCardView,
|
||||
config: AdvancedCameraCardConfig,
|
||||
options?: ViewFactoryOptions,
|
||||
): ResolvedViewTarget {
|
||||
const defaultViewName = this._getDefaultViewName(config);
|
||||
if (options?.failSafe && !doesViewRequireCamera(defaultViewName)) {
|
||||
return { viewName: defaultViewName, cameraID: null };
|
||||
}
|
||||
if (options?.failSafe) {
|
||||
return {
|
||||
viewName: defaultViewName,
|
||||
cameraID: this._api.getCameraManager().getStore().getDefaultCameraID(),
|
||||
};
|
||||
}
|
||||
throw new ViewIncompatible(localize('error.no_supported_cameras'), {
|
||||
view: viewName,
|
||||
camera: null,
|
||||
default_view: defaultViewName,
|
||||
});
|
||||
}
|
||||
|
||||
protected _handleUnsupportedView(
|
||||
viewName: AdvancedCameraCardView,
|
||||
cameraID: string,
|
||||
config: AdvancedCameraCardConfig,
|
||||
options?: ViewFactoryOptions,
|
||||
): ResolvedViewTarget {
|
||||
const defaultViewName = this._getDefaultViewName(config);
|
||||
if (
|
||||
options?.failSafe &&
|
||||
isViewSupportedByCamera(
|
||||
defaultViewName,
|
||||
this._api.getCameraManager(),
|
||||
this._api.getFoldersManager(),
|
||||
cameraID,
|
||||
)
|
||||
) {
|
||||
return { viewName: defaultViewName, cameraID };
|
||||
}
|
||||
|
||||
const capabilities = this._api
|
||||
.getCameraManager()
|
||||
.getStore()
|
||||
.getCamera(cameraID)
|
||||
?.getCapabilities()
|
||||
?.getRawCapabilities();
|
||||
|
||||
throw new ViewIncompatible(localize('error.no_supported_camera'), {
|
||||
view: viewName,
|
||||
camera: cameraID,
|
||||
default_view: defaultViewName,
|
||||
...(capabilities && { camera_capabilities: capabilities }),
|
||||
});
|
||||
}
|
||||
|
||||
protected _resolveDisplayMode(
|
||||
viewName: AdvancedCameraCardView,
|
||||
config: AdvancedCameraCardConfig,
|
||||
options?: ViewFactoryOptions,
|
||||
): ViewDisplayMode {
|
||||
const configuredDisplayMode = this._getConfiguredDisplayMode(viewName, config);
|
||||
|
||||
// Prioritize the configured display mode (if present).
|
||||
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1812
|
||||
return (
|
||||
(viewName !== options?.baseView?.view ? configuredDisplayMode : null) ??
|
||||
options?.params?.displayMode ??
|
||||
options?.baseView?.displayMode ??
|
||||
configuredDisplayMode ??
|
||||
'single'
|
||||
);
|
||||
}
|
||||
|
||||
protected _getConfiguredDisplayMode(
|
||||
viewName: AdvancedCameraCardView,
|
||||
config: AdvancedCameraCardConfig,
|
||||
): ViewDisplayMode | null {
|
||||
let mode: ViewDisplayMode | null = null;
|
||||
switch (viewName) {
|
||||
case 'media':
|
||||
case 'clip':
|
||||
case 'recording':
|
||||
case 'snapshot':
|
||||
mode = config.media_viewer.display?.mode ?? null;
|
||||
break;
|
||||
return config.media_viewer.display?.mode ?? null;
|
||||
case 'live':
|
||||
mode = config.live.display?.mode ?? null;
|
||||
break;
|
||||
return config.live.display?.mode ?? null;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
return mode;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,10 @@ export class SubstreamOnViewModifier implements ViewModifier {
|
||||
}
|
||||
|
||||
public modify(view: View): void {
|
||||
if (!view.camera) {
|
||||
return;
|
||||
}
|
||||
|
||||
const dependencies = [
|
||||
...this._api
|
||||
.getCameraManager()
|
||||
@@ -27,6 +31,13 @@ export class SubstreamOnViewModifier implements ViewModifier {
|
||||
}
|
||||
|
||||
const currentOverride = getStreamCameraID(view);
|
||||
|
||||
/* istanbul ignore if: the if path cannot be reached, as there is a
|
||||
view.camera guard at the start of this method and getStreamCameraID will
|
||||
always return non-null as long as camera is present -- @preserve */
|
||||
if (!currentOverride) {
|
||||
return;
|
||||
}
|
||||
const currentIndex = dependencies.indexOf(currentOverride);
|
||||
const newIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % dependencies.length;
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { AdvancedCameraCardError } from '../../types.js';
|
||||
import { ViewItem } from '../../view/item.js';
|
||||
import { QueryResults } from '../../view/query-results.js';
|
||||
|
||||
import { AdvancedCameraCardUserSpecifiedView } from '../../config/schema/common/const.js';
|
||||
import { View, ViewParameters } from '../../view/view.js';
|
||||
|
||||
export interface ViewModifier {
|
||||
@@ -26,12 +27,16 @@ export interface QueryExecutorOptions {
|
||||
useCache?: boolean;
|
||||
}
|
||||
|
||||
export type ViewParametersUserSpecified = Partial<Omit<ViewParameters, 'view'>> & {
|
||||
view?: AdvancedCameraCardUserSpecifiedView;
|
||||
};
|
||||
|
||||
export interface ViewFactoryOptions {
|
||||
// An existing view to evolve from.
|
||||
baseView?: View | null;
|
||||
|
||||
// View parameters to set/evolve.
|
||||
params?: Partial<ViewParameters>;
|
||||
params?: ViewParametersUserSpecified;
|
||||
|
||||
// Modifiers to the view once created.
|
||||
modifiers?: ViewModifier[];
|
||||
@@ -70,5 +75,4 @@ export interface ViewManagerInterface {
|
||||
hasMajorMediaChange(oldView?: View | null, newView?: View | null): boolean;
|
||||
}
|
||||
|
||||
export class ViewNoCameraError extends AdvancedCameraCardError {}
|
||||
export class ViewIncompatible extends AdvancedCameraCardError {}
|
||||
|
||||
@@ -317,7 +317,7 @@ export class ViewManager implements ViewManagerInterface {
|
||||
|
||||
this._api.getConditionStateManager()?.setState({
|
||||
view: view?.view,
|
||||
camera: view?.camera,
|
||||
camera: view?.camera ?? undefined,
|
||||
displayMode: view?.displayMode ?? undefined,
|
||||
});
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import { UnifiedQuery } from '../../view/unified-query';
|
||||
import { UnifiedQueryBuilder } from '../../view/unified-query-builder';
|
||||
import { UnifiedQueryRunner } from '../../view/unified-query-runner';
|
||||
import { View } from '../../view/view';
|
||||
import { doesViewRequireCamera, isViewSupported } from '../../view/view-support';
|
||||
import { CardViewAPI } from '../types';
|
||||
import { MergeContextViewModifier } from './modifiers/merge-context';
|
||||
import { RemoveContextPropertyViewModifier } from './modifiers/remove-context-property';
|
||||
@@ -103,7 +104,26 @@ export class ViewQueryExecutor {
|
||||
];
|
||||
};
|
||||
|
||||
const cameraForQuery = view.isGrid() ? undefined : view.camera;
|
||||
// Don't query if the view is not supported (e.g. a camera-based view with
|
||||
// no cameras).
|
||||
if (
|
||||
!isViewSupported(
|
||||
view.view,
|
||||
this._api.getCameraManager(),
|
||||
this._api.getFoldersManager(),
|
||||
view.camera,
|
||||
)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Don't query if the view requires a camera, if it's not in grid mode, but
|
||||
// no camera is provided.
|
||||
if (!view.isGrid() && !view.camera && doesViewRequireCamera(view.view)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cameraForQuery = view.isGrid() ? undefined : view.camera ?? undefined;
|
||||
|
||||
const getDefaultQueryModifiers = async () => {
|
||||
const query = builder.buildDefaultCameraQuery(cameraForQuery, {
|
||||
|
||||
Reference in New Issue
Block a user