Add support for global cameras options
This commit is contained in:
+182
-133
@@ -61,7 +61,7 @@ import {
|
||||
frigateCardHasAction,
|
||||
getActionConfigGivenAction,
|
||||
} from './utils/action.js';
|
||||
import { contentsChanged, errorToConsole } from './utils/basic.js';
|
||||
import { errorToConsole } from './utils/basic.js';
|
||||
import {
|
||||
getEntityIcon,
|
||||
getEntityTitle,
|
||||
@@ -87,6 +87,10 @@ import { EntityRegistryManager } from './utils/ha/entity-registry/index.js';
|
||||
import { EntityCache } from './utils/ha/entity-registry/cache.js';
|
||||
import { Entity, ExtendedEntity } from './utils/ha/entity-registry/types.js';
|
||||
import { getAllDependentCameras } from './utils/camera.js';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import merge from 'lodash-es/merge';
|
||||
import { FrigateCardInitializer } from './utils/initializer.js';
|
||||
|
||||
/** A note on media callbacks:
|
||||
*
|
||||
@@ -135,7 +139,12 @@ console.info(
|
||||
documentationURL: REPO_URL,
|
||||
});
|
||||
|
||||
type InitializedType = 'initialized' | 'initializing';
|
||||
enum InitializationAspect {
|
||||
LANGUAGES = 'languages',
|
||||
SIDE_LOAD_ELEMENTS = 'side-load-elements',
|
||||
MEDIA_PLAYERS = 'media-players',
|
||||
CAMERAS = 'cameras',
|
||||
}
|
||||
|
||||
/**
|
||||
* Main FrigateCard class.
|
||||
@@ -202,9 +211,6 @@ class FrigateCard extends LitElement {
|
||||
// per second for performance reasons.
|
||||
protected _boundMouseHandler = throttle(this._mouseHandler.bind(this), 1 * 1000);
|
||||
|
||||
// Whether the card has been successfully initialized.
|
||||
protected _initialized?: InitializedType;
|
||||
|
||||
protected _triggers: Map<string, Date> = new Map();
|
||||
protected _untriggerTimerID: number | null = null;
|
||||
|
||||
@@ -212,6 +218,8 @@ class FrigateCard extends LitElement {
|
||||
|
||||
protected _mediaPlayers?: string[];
|
||||
|
||||
protected _initializer = new FrigateCardInitializer();
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._entityRegistryManager = new EntityRegistryManager(
|
||||
@@ -315,7 +323,15 @@ class FrigateCard extends LitElement {
|
||||
|
||||
// Save on Lit re-rendering costs by only updating the configuration if it
|
||||
// actually changes.
|
||||
if (contentsChanged(overriddenConfig, this._overriddenConfig)) {
|
||||
if (!isEqual(overriddenConfig, this._overriddenConfig)) {
|
||||
if (
|
||||
!isEqual(overriddenConfig.cameras, this._overriddenConfig?.cameras) ||
|
||||
!isEqual(overriddenConfig.cameras_global, this._overriddenConfig?.cameras_global)
|
||||
) {
|
||||
// Uninitialize the cameras (they will be re-initialized on the render
|
||||
// cycle triggered by updating the overridden config) below.
|
||||
this._initializer.uninitialize(InitializationAspect.CAMERAS);
|
||||
}
|
||||
this._overriddenConfig = overriddenConfig;
|
||||
}
|
||||
}
|
||||
@@ -906,98 +922,6 @@ class FrigateCard extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
protected async _initialize(
|
||||
hass: HomeAssistant,
|
||||
config: FrigateCardConfig,
|
||||
cardWideConfig: CardWideConfig,
|
||||
): Promise<void> {
|
||||
// Above arguments are taken (vs usage of `this`) as they must exist prior
|
||||
// to initialization and this ensures it is the callers responsibility to
|
||||
// verify that.
|
||||
|
||||
await Promise.all([
|
||||
// Side load Home Assistant elements used in the UI.
|
||||
sideLoadHomeAssistantElements(),
|
||||
|
||||
// Load dynamic language imports.
|
||||
loadLanguages(),
|
||||
]);
|
||||
|
||||
await this._initializeCameras(hass, config, cardWideConfig);
|
||||
|
||||
// Don't reset the message which may be set to an error above. This sets the
|
||||
// first view using the newly loaded cameras.
|
||||
this._changeView({ resetMessage: false });
|
||||
}
|
||||
|
||||
protected async _initializeCameras(
|
||||
hass: HomeAssistant,
|
||||
config: FrigateCardConfig,
|
||||
cardWideConfig: CardWideConfig,
|
||||
): Promise<void> {
|
||||
this._cameraManager = new CameraManager(
|
||||
new CameraManagerEngineFactory(this._entityRegistryManager, cardWideConfig),
|
||||
this._cardWideConfig,
|
||||
);
|
||||
|
||||
try {
|
||||
await this._cameraManager.initializeCameras(
|
||||
hass,
|
||||
this._entityRegistryManager,
|
||||
config.cameras,
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorToConsole(e);
|
||||
}
|
||||
if (e instanceof FrigateCardError) {
|
||||
this._setMessageAndUpdate({
|
||||
message: e.message,
|
||||
type: 'error',
|
||||
context: e.context,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async _initializeMediaPlayers(hass: HomeAssistant): Promise<void> {
|
||||
const isValidMediaPlayer = (entityID: string): boolean => {
|
||||
if (entityID.startsWith('media_player.')) {
|
||||
const stateObj = this._hass?.states[entityID];
|
||||
if (
|
||||
stateObj &&
|
||||
stateObj.state !== 'unavailable' &&
|
||||
supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const mediaPlayers = Object.keys(this._hass?.states || {}).filter(
|
||||
isValidMediaPlayer,
|
||||
);
|
||||
let mediaPlayerEntities: Map<string, Entity>;
|
||||
try {
|
||||
mediaPlayerEntities = await this._entityRegistryManager.getEntities(
|
||||
hass,
|
||||
mediaPlayers,
|
||||
);
|
||||
} catch (e) {
|
||||
// Failing to fetch media player information is not considered
|
||||
// sufficiently serious to block card startup.
|
||||
errorToConsole(e as Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out entities that are marked as hidden (this information is not
|
||||
// available in the HA state, only in the registry).
|
||||
this._mediaPlayers = [...mediaPlayerEntities.values()]
|
||||
.filter((entity) => !entity.hidden_by)
|
||||
.map((entity) => entity.entity_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called before each update.
|
||||
*/
|
||||
@@ -1006,25 +930,7 @@ class FrigateCard extends LitElement {
|
||||
setPerformanceCSSStyles(this, this._cardWideConfig?.performance);
|
||||
}
|
||||
|
||||
if (
|
||||
this._hass &&
|
||||
!this._mediaPlayers &&
|
||||
this._getConfig().menu.buttons.media_player.enabled
|
||||
) {
|
||||
// Media players are initialized outside the main initialization code (the
|
||||
// `initialize` method) since they may be required depending on an
|
||||
// overridable configuration value.
|
||||
|
||||
// We also want to initialize media players after since the main camera
|
||||
// initialization since that may have fetched entity information that will
|
||||
// be cached and re-used here (minor performance optimization). Only do
|
||||
// this if the media player button is enabled to further limit the
|
||||
// performance implications.
|
||||
|
||||
// Prevent a double initialization.
|
||||
this._mediaPlayers = [];
|
||||
this._initializeMediaPlayers(this._hass);
|
||||
}
|
||||
this._initializeBackground();
|
||||
|
||||
if (this._view?.is('live')) {
|
||||
import('./components/live/live.js');
|
||||
@@ -1147,30 +1053,173 @@ class FrigateCard extends LitElement {
|
||||
}
|
||||
}
|
||||
|
||||
protected async _initializeCameras(
|
||||
hass: HomeAssistant,
|
||||
config: FrigateCardConfig,
|
||||
cardWideConfig: CardWideConfig,
|
||||
): Promise<void> {
|
||||
this._cameraManager = new CameraManager(
|
||||
new CameraManagerEngineFactory(this._entityRegistryManager, cardWideConfig),
|
||||
this._cardWideConfig,
|
||||
);
|
||||
|
||||
// For each camera merge the config into the camera global config. The
|
||||
// merging must happen in this order, to ensure that the defaults in the
|
||||
// cameras global config do not override the values specified in the
|
||||
// per-camera config.
|
||||
const cameras = config.cameras.map((camera) =>
|
||||
merge(cloneDeep(config.cameras_global), camera),
|
||||
);
|
||||
|
||||
console.info("MERGED CAMERAS", cameras);
|
||||
|
||||
try {
|
||||
await this._cameraManager.initializeCameras(
|
||||
hass,
|
||||
this._entityRegistryManager,
|
||||
cameras,
|
||||
);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof Error) {
|
||||
errorToConsole(e);
|
||||
}
|
||||
if (e instanceof FrigateCardError) {
|
||||
this._setMessageAndUpdate({
|
||||
message: e.message,
|
||||
type: 'error',
|
||||
context: e.context,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If there's no view set yet, set one. This will be the case on initial camera load.
|
||||
if (!this._view) {
|
||||
// Don't reset the message which may be set to an error above. This sets the
|
||||
// first view using the newly loaded cameras.
|
||||
this._changeView({ resetMessage: false });
|
||||
}
|
||||
}
|
||||
|
||||
protected async _initializeMediaPlayers(hass: HomeAssistant): Promise<void> {
|
||||
const isValidMediaPlayer = (entityID: string): boolean => {
|
||||
if (entityID.startsWith('media_player.')) {
|
||||
const stateObj = this._hass?.states[entityID];
|
||||
if (
|
||||
stateObj &&
|
||||
stateObj.state !== 'unavailable' &&
|
||||
supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const mediaPlayers = Object.keys(this._hass?.states || {}).filter(
|
||||
isValidMediaPlayer,
|
||||
);
|
||||
let mediaPlayerEntities: Map<string, Entity>;
|
||||
try {
|
||||
mediaPlayerEntities = await this._entityRegistryManager.getEntities(
|
||||
hass,
|
||||
mediaPlayers,
|
||||
);
|
||||
} catch (e) {
|
||||
// Failing to fetch media player information is not considered
|
||||
// sufficiently serious to block card startup.
|
||||
errorToConsole(e as Error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out entities that are marked as hidden (this information is not
|
||||
// available in the HA state, only in the registry).
|
||||
this._mediaPlayers = [...mediaPlayerEntities.values()]
|
||||
.filter((entity) => !entity.hidden_by)
|
||||
.map((entity) => entity.entity_id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize the hard requirements for rendering anything.
|
||||
* @returns `true` if card rendering can continue.
|
||||
*/
|
||||
protected _initializeMandatory(): boolean {
|
||||
if (
|
||||
this._initializer.isInitializedMultiple([
|
||||
InitializationAspect.LANGUAGES,
|
||||
InitializationAspect.SIDE_LOAD_ELEMENTS,
|
||||
InitializationAspect.CAMERAS,
|
||||
])
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const hass = this._hass;
|
||||
const config = this._getConfig();
|
||||
const cardWideConfig = this._cardWideConfig;
|
||||
if (!hass || !config || !cardWideConfig) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this._initializer
|
||||
.initializeMultipleIfNecessary({
|
||||
// Caution: Ensure nothing in this set of initializers requires
|
||||
// languages since they will not yet have been initialized.
|
||||
[InitializationAspect.LANGUAGES]: async () => loadLanguages,
|
||||
[InitializationAspect.SIDE_LOAD_ELEMENTS]: async () =>
|
||||
sideLoadHomeAssistantElements,
|
||||
})
|
||||
.then(() => {
|
||||
return this._initializer.initializeIfNecessary(
|
||||
InitializationAspect.CAMERAS,
|
||||
async () => this._initializeCameras(hass, config, cardWideConfig),
|
||||
);
|
||||
})
|
||||
.then((initialized) => {
|
||||
if (initialized) {
|
||||
return this.requestUpdate();
|
||||
}
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize aspects of the card that can load in the 'background'.
|
||||
* @returns `true` if card rendering can continue.
|
||||
*/
|
||||
protected _initializeBackground(): void {
|
||||
if (this._initializer.isInitialized(InitializationAspect.MEDIA_PLAYERS)) {
|
||||
return;
|
||||
}
|
||||
const hass = this._hass;
|
||||
const config = this._getConfig();
|
||||
if (!hass || !config) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._initializer
|
||||
.initializeMultipleIfNecessary({
|
||||
...(config.menu.buttons.media_player.enabled && {
|
||||
[InitializationAspect.MEDIA_PLAYERS]: async () =>
|
||||
this._initializeMediaPlayers(hass),
|
||||
}),
|
||||
})
|
||||
.then((initialized) => {
|
||||
if (initialized) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine whether the element should be updated.
|
||||
* @param changedProps The changed properties if any.
|
||||
* @returns `true` if the element should be updated.
|
||||
*/
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
// Load the relevant languages. Cannot do anything until then.
|
||||
if (this._initialized !== 'initialized') {
|
||||
const config = this._getConfig();
|
||||
if (
|
||||
this._initialized !== 'initializing' &&
|
||||
this._hass &&
|
||||
config &&
|
||||
this._cardWideConfig
|
||||
) {
|
||||
this._initialized = 'initializing';
|
||||
this._initialize(this._hass, config, this._cardWideConfig).then(() => {
|
||||
this._initialized = 'initialized';
|
||||
this.requestUpdate();
|
||||
});
|
||||
}
|
||||
if (!this._initializeMandatory()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get('_hass') as HomeAssistant | undefined;
|
||||
let shouldUpdate = !oldHass || changedProps.size != 1;
|
||||
|
||||
|
||||
+6
-99
@@ -4,6 +4,9 @@ import isEqual from 'lodash-es/isEqual';
|
||||
import set from 'lodash-es/set';
|
||||
import {
|
||||
CONF_CAMERAS,
|
||||
CONF_CAMERAS_GLOBAL_IMAGE,
|
||||
CONF_CAMERAS_GLOBAL_JSMPEG,
|
||||
CONF_CAMERAS_GLOBAL_WEBRTC_CARD,
|
||||
CONF_ELEMENTS,
|
||||
CONF_LIVE_AUTO_UNMUTE,
|
||||
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
|
||||
@@ -28,7 +31,6 @@ import {
|
||||
import {
|
||||
BUTTON_SIZE_MIN,
|
||||
RawFrigateCardConfig,
|
||||
RawFrigateCardConfigArray,
|
||||
THUMBNAIL_WIDTH_MAX,
|
||||
THUMBNAIL_WIDTH_MIN,
|
||||
} from './types';
|
||||
@@ -91,7 +93,6 @@ export const upgradeConfig = function (obj: RawFrigateCardConfig): boolean {
|
||||
for (let i = 0; i < UPGRADES.length; i++) {
|
||||
upgraded = UPGRADES[i](obj) || upgraded;
|
||||
}
|
||||
trimConfig(obj);
|
||||
return upgraded;
|
||||
};
|
||||
|
||||
@@ -104,28 +105,6 @@ export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean
|
||||
return upgradeConfig(copyConfig(obj));
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove empty sections from a configuration.
|
||||
* @param obj Configuration object.
|
||||
* @returns `true` if the configuration was modified.
|
||||
*/
|
||||
const trimConfig = function (obj: RawFrigateCardConfig): boolean {
|
||||
const keys = Object.keys(obj);
|
||||
let modified = false;
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
const key = keys[i];
|
||||
if (typeof obj[key] === 'object' && obj[key] != null) {
|
||||
modified = trimConfig(obj[key] as RawFrigateCardConfig) || modified;
|
||||
|
||||
if (!Object.keys(obj[key] as RawFrigateCardConfig).length) {
|
||||
delete obj[key];
|
||||
modified = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return modified;
|
||||
};
|
||||
|
||||
/**
|
||||
* Copy a configuration.
|
||||
* @param obj Configuration to copy.
|
||||
@@ -523,80 +502,6 @@ const transformFrigateUIAction = (data: unknown): boolean => {
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Move live provider options exclusively into camera configs.
|
||||
* @returns An upgrade function.
|
||||
*/
|
||||
const upgradeCameraOptionsFromLiveToMultipleCameras = (): ((
|
||||
obj: RawFrigateCardConfig,
|
||||
) => boolean) => {
|
||||
return function (obj: RawFrigateCardConfig): boolean {
|
||||
const cameras = getConfigValue(obj, CONF_CAMERAS) as
|
||||
| RawFrigateCardConfigArray
|
||||
| undefined;
|
||||
if (cameras === undefined) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const webrtcCardConfig = getConfigValue(obj, 'live.webrtc_card') as
|
||||
| RawFrigateCardConfigArray
|
||||
| undefined;
|
||||
const imageConfig = getConfigValue(obj, 'live.image') as
|
||||
| RawFrigateCardConfigArray
|
||||
| undefined;
|
||||
const jsmpegConfig = getConfigValue(obj, 'live.jsmpeg') as
|
||||
| RawFrigateCardConfigArray
|
||||
| undefined;
|
||||
|
||||
if (!webrtcCardConfig && !imageConfig && !jsmpegConfig) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (webrtcCardConfig) {
|
||||
cameras.forEach((camera) => {
|
||||
if (
|
||||
camera.live_provider === 'webrtc_card' &&
|
||||
(camera.webrtc_card === undefined || typeof camera.webrtc_card === 'object')
|
||||
) {
|
||||
camera.webrtc_card = { ...webrtcCardConfig, ...camera.webrtc_card };
|
||||
}
|
||||
});
|
||||
}
|
||||
if (imageConfig) {
|
||||
cameras.forEach((camera) => {
|
||||
if (
|
||||
camera.live_provider === 'image' &&
|
||||
(camera.image === undefined || typeof camera.image === 'object')
|
||||
) {
|
||||
camera.image = { ...imageConfig, ...camera.image };
|
||||
}
|
||||
});
|
||||
}
|
||||
if (jsmpegConfig) {
|
||||
cameras.forEach((camera) => {
|
||||
if (
|
||||
camera.live_provider === 'jsmpeg' &&
|
||||
(camera.jsmpeg === undefined || typeof camera.jsmpeg === 'object')
|
||||
) {
|
||||
camera.jsmpeg = { ...jsmpegConfig, ...camera.jsmpeg };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setConfigValue(obj, CONF_CAMERAS, cameras);
|
||||
deleteConfigValue(obj, 'live.webrtc_card');
|
||||
deleteConfigValue(obj, 'live.image');
|
||||
deleteConfigValue(obj, 'live.jsmpeg');
|
||||
|
||||
// Note: This upgrade is imperfect. There could be override conditions being
|
||||
// set that this upgrade cannot understand, e.g. if in fullscreen mode then
|
||||
// refresh a live image more frequently. Such functionality is not possible
|
||||
// after this change, since camera configs cannot be overrided.
|
||||
|
||||
return true;
|
||||
};
|
||||
};
|
||||
|
||||
const UPGRADES = [
|
||||
// v3.0.0 -> v4.0.0-rc.1
|
||||
upgradeWithOverrides(
|
||||
@@ -680,5 +585,7 @@ const UPGRADES = [
|
||||
val === 'frigate-jsmpeg' ? 'jsmpeg' : val,
|
||||
),
|
||||
),
|
||||
upgradeCameraOptionsFromLiveToMultipleCameras(),
|
||||
upgradeMoveToWithOverrides('live.image', CONF_CAMERAS_GLOBAL_IMAGE),
|
||||
upgradeMoveToWithOverrides('live.jsmpeg', CONF_CAMERAS_GLOBAL_JSMPEG),
|
||||
upgradeMoveToWithOverrides('live.webrtc_card', CONF_CAMERAS_GLOBAL_WEBRTC_CARD),
|
||||
];
|
||||
|
||||
@@ -39,6 +39,12 @@ export const CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY =
|
||||
export const CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES =
|
||||
`${CONF_CAMERAS}.#.triggers.entities` as const;
|
||||
|
||||
export const CONF_CAMERAS_GLOBAL = 'cameras_global' as const;
|
||||
export const CONF_CAMERAS_GLOBAL_IMAGE = `${CONF_CAMERAS_GLOBAL}.image` as const;
|
||||
export const CONF_CAMERAS_GLOBAL_JSMPEG = `${CONF_CAMERAS_GLOBAL}.jsmpeg` as const;
|
||||
export const CONF_CAMERAS_GLOBAL_WEBRTC_CARD =
|
||||
`${CONF_CAMERAS_GLOBAL}.webrtc_card` as const;
|
||||
|
||||
export const CONF_ELEMENTS = 'elements' as const;
|
||||
|
||||
const CONF_VIEW = 'view' as const;
|
||||
|
||||
+6
-5
@@ -412,10 +412,8 @@ const go2rtcConfigSchema = z.object({
|
||||
modes: z.enum(['webrtc', 'mse', 'mp4', 'mjpeg']).array().optional(),
|
||||
stream: z.string().optional(),
|
||||
});
|
||||
export type Go2rtcConfig = z.infer<typeof go2rtcConfigSchema>;
|
||||
|
||||
const liveImageConfigSchema = imageBaseConfigSchema;
|
||||
export type LiveImageConfig = z.infer<typeof liveImageConfigSchema>;
|
||||
|
||||
const webrtcCardConfigSchema = z
|
||||
.object({
|
||||
@@ -423,7 +421,6 @@ const webrtcCardConfigSchema = z
|
||||
url: z.string().optional(),
|
||||
})
|
||||
.passthrough();
|
||||
export type WebRTCCardConfig = z.infer<typeof webrtcCardConfigSchema>;
|
||||
|
||||
const jsmpegConfigSchema = z.object({
|
||||
options: z
|
||||
@@ -444,7 +441,6 @@ const jsmpegConfigSchema = z.object({
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
export type JSMPEGConfig = z.infer<typeof jsmpegConfigSchema>;
|
||||
|
||||
/**
|
||||
* Camera configuration section
|
||||
@@ -524,7 +520,9 @@ const cameraConfigSchema = z
|
||||
.default(cameraConfigDefault);
|
||||
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
||||
|
||||
const camerasConfigSchema = cameraConfigSchema.array().nonempty();
|
||||
// Avoid using .nonempty() to avoid changing the inferred type
|
||||
// (https://github.com/colinhacks/zod#minmaxlength).
|
||||
const camerasConfigSchema = cameraConfigSchema.array().min(1);
|
||||
export type CamerasConfig = z.infer<typeof camerasConfigSchema>;
|
||||
|
||||
/**
|
||||
@@ -1200,6 +1198,8 @@ export type TimelineConfig = z.infer<typeof timelineConfigSchema>;
|
||||
// Strip all defaults from the override schemas, to ensure values are only what
|
||||
// the user has specified.
|
||||
const overrideConfigurationSchema = z.object({
|
||||
cameras: deepRemoveDefaults(camerasConfigSchema).optional(),
|
||||
cameras_global: deepRemoveDefaults(cameraConfigSchema).optional(),
|
||||
live: deepRemoveDefaults(liveOverridableConfigSchema).optional(),
|
||||
menu: deepRemoveDefaults(menuConfigSchema).optional(),
|
||||
image: deepRemoveDefaults(imageConfigSchema).optional(),
|
||||
@@ -1284,6 +1284,7 @@ export interface CardWideConfig {
|
||||
export const frigateCardConfigSchema = z.object({
|
||||
// Main configuration sections.
|
||||
cameras: camerasConfigSchema,
|
||||
cameras_global: cameraConfigSchema.optional(),
|
||||
view: viewConfigSchema,
|
||||
menu: menuConfigSchema,
|
||||
live: liveConfigSchema,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { allPromises } from './basic';
|
||||
|
||||
enum InitializationState {
|
||||
INITIALIZING = 'initializing',
|
||||
INITIALIZED = 'initialized',
|
||||
}
|
||||
|
||||
type Initializer = () => Promise<unknown>;
|
||||
|
||||
/**
|
||||
* Manages initialization state & calling initializers.
|
||||
*/
|
||||
export class FrigateCardInitializer {
|
||||
protected _state: Map<string, InitializationState>;
|
||||
|
||||
constructor() {
|
||||
this._state = new Map();
|
||||
}
|
||||
|
||||
public async initializeMultipleIfNecessary(
|
||||
aspects: Record<string, Initializer>,
|
||||
): Promise<boolean> {
|
||||
const results = await allPromises(
|
||||
Object.entries(aspects),
|
||||
async ([aspect, options]) => this.initializeIfNecessary(aspect, options),
|
||||
);
|
||||
return results.every(Boolean);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param aspect The aspect to initialize.
|
||||
* @param initializer The initializer to call.
|
||||
* @returns `true` if the state is confirmed as initialized, `false`
|
||||
* otherwise (i.e. initializing).
|
||||
*/
|
||||
public async initializeIfNecessary(
|
||||
aspect: string,
|
||||
initializer?: Initializer,
|
||||
): Promise<boolean> {
|
||||
const state = this._state.get(aspect);
|
||||
if (state !== InitializationState.INITIALIZED) {
|
||||
if (state !== InitializationState.INITIALIZING) {
|
||||
if (initializer) {
|
||||
this._state.set(aspect, InitializationState.INITIALIZING);
|
||||
await initializer();
|
||||
this._state.set(aspect, InitializationState.INITIALIZED);
|
||||
} else {
|
||||
this._state.set(aspect, InitializationState.INITIALIZED);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public uninitialize(aspect: string) {
|
||||
return this._state.delete(aspect);
|
||||
}
|
||||
|
||||
public isInitialized(aspect: string): boolean {
|
||||
return this._state.get(aspect) == InitializationState.INITIALIZED;
|
||||
}
|
||||
|
||||
public isInitializedMultiple(aspects: string[]): boolean {
|
||||
return aspects.every((aspect) => this.isInitialized(aspect));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user