fix: Fix initialization race condition that occurs in certain circumstances (#1545)
* fix: Fix initialization race condition in certain circumstances * Minor improvements and camera iris logo
This commit is contained in:
@@ -111,7 +111,6 @@ export class CameraManager {
|
|||||||
protected _api: CardCameraAPI;
|
protected _api: CardCameraAPI;
|
||||||
protected _engineFactory: CameraManagerEngineFactory;
|
protected _engineFactory: CameraManagerEngineFactory;
|
||||||
protected _store: CameraManagerStore;
|
protected _store: CameraManagerStore;
|
||||||
protected _initializationLimit = new PQueue({ concurrency: 1 });
|
|
||||||
protected _requestLimit = new PQueue();
|
protected _requestLimit = new PQueue();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -147,16 +146,8 @@ export class CameraManager {
|
|||||||
recursivelyMergeObjectsNotArrays({}, cloneDeep(config?.cameras_global), camera),
|
recursivelyMergeObjectsNotArrays({}, cloneDeep(config?.cameras_global), camera),
|
||||||
);
|
);
|
||||||
|
|
||||||
const resetAndInitialize = async () => {
|
|
||||||
await this._reset();
|
|
||||||
await this._initializeCameras(cameras);
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// This concurrency limit prevents multiple rapidly arriving configs from
|
await this._initializeCameras(cameras);
|
||||||
// generating reset-n-initialize race conditions (e.g. changing values
|
|
||||||
// rapidly in the config editor).
|
|
||||||
await this._initializationLimit.add(resetAndInitialize);
|
|
||||||
} catch (e: unknown) {
|
} catch (e: unknown) {
|
||||||
this._api
|
this._api
|
||||||
.getMessageManager()
|
.getMessageManager()
|
||||||
@@ -166,7 +157,7 @@ export class CameraManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _reset(): Promise<void> {
|
public async reset(): Promise<void> {
|
||||||
await this._store.reset();
|
await this._store.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -246,6 +237,8 @@ export class CameraManager {
|
|||||||
async ([cameraConfig, engine]) => await engine.createCamera(hass, cameraConfig),
|
async ([cameraConfig, engine]) => await engine.createCamera(hass, cameraConfig),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const cameraIDs: Set<string> = new Set();
|
||||||
|
|
||||||
// Do the additions based off the result-order, to ensure the map order is
|
// Do the additions based off the result-order, to ensure the map order is
|
||||||
// preserved.
|
// preserved.
|
||||||
cameras.forEach((camera) => {
|
cameras.forEach((camera) => {
|
||||||
@@ -258,7 +251,7 @@ export class CameraManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._store.hasCameraID(cameraID)) {
|
if (cameraIDs.has(cameraID)) {
|
||||||
throw new CameraInitializationError(
|
throw new CameraInitializationError(
|
||||||
localize('error.duplicate_camera_id'),
|
localize('error.duplicate_camera_id'),
|
||||||
camera.getConfig(),
|
camera.getConfig(),
|
||||||
@@ -267,9 +260,11 @@ export class CameraManager {
|
|||||||
|
|
||||||
// Always ensure the actual ID used in the card is in the configuration itself.
|
// Always ensure the actual ID used in the card is in the configuration itself.
|
||||||
camera.setID(cameraID);
|
camera.setID(cameraID);
|
||||||
this._store.addCamera(camera);
|
cameraIDs.add(cameraID);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await this._store.setCameras(cameras);
|
||||||
|
|
||||||
log(
|
log(
|
||||||
this._api.getConfigManager().getCardWideConfig(),
|
this._api.getConfigManager().getCardWideConfig(),
|
||||||
'Frigate Card CameraManager initialized (Cameras: ',
|
'Frigate Card CameraManager initialized (Cameras: ',
|
||||||
|
|||||||
@@ -44,6 +44,30 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
|||||||
this._enginesByType.set(camera.getEngine().getEngineType(), camera.getEngine());
|
this._enginesByType.set(camera.getEngine().getEngineType(), camera.getEngine());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async setCameras(cameras: Camera[]): Promise<void> {
|
||||||
|
// In setting the store cameras, take great care to replace/add first before
|
||||||
|
// remove. Otherwise, there may be race conditions where the card attempts
|
||||||
|
// to render a view with (momentarily) no camera.
|
||||||
|
// See: https://github.com/dermotduffy/frigate-hass-card/issues/1533
|
||||||
|
|
||||||
|
// Replace/Add the new cameras.
|
||||||
|
for (const camera of cameras) {
|
||||||
|
const oldCamera = this._cameras.get(camera.getID());
|
||||||
|
if (oldCamera !== camera) {
|
||||||
|
this.addCamera(camera);
|
||||||
|
await oldCamera?.destroy();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the old cameras.
|
||||||
|
for (const camera of this._cameras.values()) {
|
||||||
|
if (!cameras.includes(camera)) {
|
||||||
|
await camera.destroy();
|
||||||
|
this._cameras.delete(camera.getID());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async reset(): Promise<void> {
|
public async reset(): Promise<void> {
|
||||||
await allPromises(this._cameras.values(), (camera) => camera.destroy());
|
await allPromises(this._cameras.values(), (camera) => camera.destroy());
|
||||||
this._cameras.clear();
|
this._cameras.clear();
|
||||||
|
|||||||
@@ -67,7 +67,12 @@ export class CardElementManager {
|
|||||||
this._api.getMediaLoadedInfoManager().initialize();
|
this._api.getMediaLoadedInfoManager().initialize();
|
||||||
this._api.getMicrophoneManager().initialize();
|
this._api.getMicrophoneManager().initialize();
|
||||||
this._api.getKeyboardStateManager().initialize();
|
this._api.getKeyboardStateManager().initialize();
|
||||||
|
|
||||||
|
// These initializers are called when the config is updated, but on initial
|
||||||
|
// creation of the card hass is not yet available when the config is first
|
||||||
|
// loaded.
|
||||||
this._api.getDefaultManager().initialize();
|
this._api.getDefaultManager().initialize();
|
||||||
|
this._api.getMediaPlayerManager().initialize();
|
||||||
|
|
||||||
this._api
|
this._api
|
||||||
.getHASSManager()
|
.getHASSManager()
|
||||||
@@ -146,6 +151,8 @@ export class CardElementManager {
|
|||||||
// reconnection, to ensure the state subscription/unsubscription works
|
// reconnection, to ensure the state subscription/unsubscription works
|
||||||
// correctly for triggers.
|
// correctly for triggers.
|
||||||
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
|
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
|
||||||
|
this._api.getCameraManager().reset();
|
||||||
|
|
||||||
this._element.removeEventListener(
|
this._element.removeEventListener(
|
||||||
'mousemove',
|
'mousemove',
|
||||||
this._api.getInteractionManager().reportInteraction,
|
this._api.getInteractionManager().reportInteraction,
|
||||||
|
|||||||
@@ -92,16 +92,20 @@ export class ConfigManager {
|
|||||||
camera: undefined,
|
camera: undefined,
|
||||||
});
|
});
|
||||||
this._api.getMediaLoadedInfoManager().clear();
|
this._api.getMediaLoadedInfoManager().clear();
|
||||||
|
|
||||||
|
this._api.getInitializationManager().uninitialize(InitializationAspect.VIEW);
|
||||||
this._api.getViewManager().reset();
|
this._api.getViewManager().reset();
|
||||||
|
|
||||||
this._api.getMessageManager().reset();
|
this._api.getMessageManager().reset();
|
||||||
this._api.getStyleManager().setPerformance();
|
this._api.getStyleManager().setPerformance();
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
this._api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
|
this._api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
|
||||||
|
|
||||||
setKeyboardShortcutsFromConfig(this._api, this);
|
setKeyboardShortcutsFromConfig(this._api, this);
|
||||||
setAutomationsFromConfig(this._api);
|
setAutomationsFromConfig(this._api);
|
||||||
|
|
||||||
this.computeOverrideConfig();
|
this.computeOverrideConfig();
|
||||||
|
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
}
|
}
|
||||||
|
|
||||||
public computeOverrideConfig(): void {
|
public computeOverrideConfig(): void {
|
||||||
@@ -144,18 +148,19 @@ export class ConfigManager {
|
|||||||
.uninitialize(InitializationAspect.MICROPHONE_CONNECT);
|
.uninitialize(InitializationAspect.MICROPHONE_CONNECT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
/* async */ this._initializeBackground(previousConfig);
|
||||||
previousConfig &&
|
|
||||||
!isEqual(
|
|
||||||
previousConfig?.view.default_reset,
|
|
||||||
this._overriddenConfig?.view.default_reset,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
this._api
|
|
||||||
.getInitializationManager()
|
|
||||||
.uninitialize(InitializationAspect.DEFAULT_RESET);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize config dependent items in the background. For items that the
|
||||||
|
* card hard requires, use InitializationManager instead.
|
||||||
|
*/
|
||||||
|
protected async _initializeBackground(
|
||||||
|
previousConfig: FrigateCardConfig | null,
|
||||||
|
): Promise<void> {
|
||||||
|
await this._api.getDefaultManager().initializeIfNecessary(previousConfig);
|
||||||
|
await this._api.getMediaPlayerManager().initializeIfNecessary(previousConfig);
|
||||||
|
|
||||||
this._api.getCardElementManager().update();
|
this._api.getCardElementManager().update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import PQueue from 'p-queue';
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { FrigateCardConfig } from '../config/types';
|
||||||
import { createGeneralAction } from '../utils/action';
|
import { createGeneralAction } from '../utils/action';
|
||||||
import { isActionAllowedBasedOnInteractionState } from '../utils/interaction-mode';
|
import { isActionAllowedBasedOnInteractionState } from '../utils/interaction-mode';
|
||||||
import { Timer } from '../utils/timer';
|
import { Timer } from '../utils/timer';
|
||||||
@@ -10,20 +11,45 @@ import { CardDefaultManagerAPI } from './types';
|
|||||||
export class DefaultManager {
|
export class DefaultManager {
|
||||||
protected _timer = new Timer();
|
protected _timer = new Timer();
|
||||||
protected _api: CardDefaultManagerAPI;
|
protected _api: CardDefaultManagerAPI;
|
||||||
protected _initializationLimit = new PQueue({ concurrency: 1 });
|
|
||||||
|
|
||||||
constructor(api: CardDefaultManagerAPI) {
|
constructor(api: CardDefaultManagerAPI) {
|
||||||
this._api = api;
|
this._api = api;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async initializeIfNecessary(
|
||||||
|
previousConfig: FrigateCardConfig | null,
|
||||||
|
): Promise<void> {
|
||||||
|
if (
|
||||||
|
!isEqual(
|
||||||
|
previousConfig?.view.default_reset,
|
||||||
|
this._api.getConfigManager().getConfig()?.view.default_reset,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the default manager. Requires both hass and configuration to be
|
* This needs to be public since the first initialization requires both hass
|
||||||
* effective (so cannot be called from just the configuration manager, as hass
|
* and the config, so it is not suitable from calling exclusively from the
|
||||||
* will not be available yet)
|
* config manager.
|
||||||
*/
|
*/
|
||||||
public async initialize(): Promise<boolean> {
|
public async initialize(): Promise<boolean> {
|
||||||
const result = await this._initializationLimit.add(() => this._reconfigure());
|
this.uninitialize();
|
||||||
this._startTimer();
|
|
||||||
|
const config = this._api.getConfigManager().getConfig()?.view.default_reset;
|
||||||
|
if (config?.entities.length) {
|
||||||
|
this._api
|
||||||
|
.getHASSManager()
|
||||||
|
.getStateWatcher()
|
||||||
|
.subscribe(this._stateChangeHandler, config.entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
const timerSeconds = this._api.getConfigManager().getConfig()?.view
|
||||||
|
.default_reset.every_seconds;
|
||||||
|
if (timerSeconds) {
|
||||||
|
this._timer.startRepeated(timerSeconds, () => this._setToDefaultIfAllowed());
|
||||||
|
}
|
||||||
|
|
||||||
if (this._api.getConfigManager().getConfig()?.view.default_reset.after_interaction) {
|
if (this._api.getConfigManager().getConfig()?.view.default_reset.after_interaction) {
|
||||||
this._api.getAutomationsManager().addAutomations([
|
this._api.getAutomationsManager().addAutomations([
|
||||||
@@ -40,7 +66,7 @@ export class DefaultManager {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!result;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public uninitialize(): void {
|
public uninitialize(): void {
|
||||||
@@ -49,28 +75,6 @@ export class DefaultManager {
|
|||||||
this._api.getAutomationsManager().deleteAutomations(this);
|
this._api.getAutomationsManager().deleteAutomations(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _reconfigure(): Promise<boolean> {
|
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
|
||||||
const config = this._api.getConfigManager().getConfig()?.view.default_reset;
|
|
||||||
if (!hass || !config) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._api.getHASSManager().getStateWatcher().unsubscribe(this._stateChangeHandler);
|
|
||||||
this._api
|
|
||||||
.getHASSManager()
|
|
||||||
.getStateWatcher()
|
|
||||||
.subscribe(this._stateChangeHandler, config.entities);
|
|
||||||
|
|
||||||
// If the timer is running, restart it with the newly configured timer.
|
|
||||||
if (this._timer.isRunning()) {
|
|
||||||
this._timer.stop();
|
|
||||||
this._startTimer();
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _stateChangeHandler = (): void => {
|
protected _stateChangeHandler = (): void => {
|
||||||
this._setToDefaultIfAllowed();
|
this._setToDefaultIfAllowed();
|
||||||
};
|
};
|
||||||
@@ -92,12 +96,4 @@ export class DefaultManager {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _startTimer(): void {
|
|
||||||
const timerSeconds = this._api.getConfigManager().getConfig()?.view
|
|
||||||
.default_reset.every_seconds;
|
|
||||||
if (timerSeconds) {
|
|
||||||
this._timer.startRepeated(timerSeconds, () => this._setToDefaultIfAllowed());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import PQueue from 'p-queue';
|
||||||
import { loadLanguages } from '../localize/localize';
|
import { loadLanguages } from '../localize/localize';
|
||||||
import { sideLoadHomeAssistantElements } from '../utils/ha';
|
import { sideLoadHomeAssistantElements } from '../utils/ha';
|
||||||
import { Initializer } from '../utils/initializer/initializer';
|
import { Initializer } from '../utils/initializer/initializer';
|
||||||
@@ -6,14 +7,29 @@ import { CardInitializerAPI } from './types';
|
|||||||
export enum InitializationAspect {
|
export enum InitializationAspect {
|
||||||
LANGUAGES = 'languages',
|
LANGUAGES = 'languages',
|
||||||
SIDE_LOAD_ELEMENTS = 'side-load-elements',
|
SIDE_LOAD_ELEMENTS = 'side-load-elements',
|
||||||
MEDIA_PLAYERS = 'media-players',
|
|
||||||
CAMERAS = 'cameras',
|
CAMERAS = 'cameras',
|
||||||
MICROPHONE_CONNECT = 'microphone-connect',
|
MICROPHONE_CONNECT = 'microphone-connect',
|
||||||
DEFAULT_RESET = 'default-reset',
|
VIEW = 'view',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// Rules for initialization. Initializers must be reentrant as these situations
|
||||||
|
// may occur:
|
||||||
|
//
|
||||||
|
// 1. Multiple JS async contexts may execute these functions at the same time.
|
||||||
|
// 2. At any point, something may uninitialize a part of the card (including
|
||||||
|
// while a different async context is in the middle of running the
|
||||||
|
// initialization method).
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
export class InitializationManager {
|
export class InitializationManager {
|
||||||
protected _api: CardInitializerAPI;
|
protected _api: CardInitializerAPI;
|
||||||
|
|
||||||
|
// A concurrency limit is placed to ensure that on card load multiple async
|
||||||
|
// contexts do not attempt to initialize the card at the same time. This is
|
||||||
|
// not strictly necessary, just more efficient, as long as the "Rules for
|
||||||
|
// initialization" (above) are followed.
|
||||||
|
protected _initializationQueue = new PQueue({ concurrency: 1 });
|
||||||
protected _initializer: Initializer;
|
protected _initializer: Initializer;
|
||||||
|
|
||||||
constructor(api: CardInitializerAPI, initializer?: Initializer) {
|
constructor(api: CardInitializerAPI, initializer?: Initializer) {
|
||||||
@@ -27,28 +43,29 @@ export class InitializationManager {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return this._initializer.isInitializedMultiple([
|
||||||
this._initializer.isInitializedMultiple([
|
|
||||||
InitializationAspect.LANGUAGES,
|
InitializationAspect.LANGUAGES,
|
||||||
InitializationAspect.SIDE_LOAD_ELEMENTS,
|
InitializationAspect.SIDE_LOAD_ELEMENTS,
|
||||||
InitializationAspect.CAMERAS,
|
InitializationAspect.CAMERAS,
|
||||||
...(config.live.microphone.always_connected
|
...(config.live.microphone.always_connected
|
||||||
? [InitializationAspect.MICROPHONE_CONNECT]
|
? [InitializationAspect.MICROPHONE_CONNECT]
|
||||||
: []),
|
: []),
|
||||||
]) &&
|
InitializationAspect.VIEW,
|
||||||
// If there's no view, re-initialize (e.g. config changes).
|
]);
|
||||||
this._api.getViewManager().hasView()
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the hard requirements for rendering anything.
|
* Initialize the hard requirements for rendering anything.
|
||||||
* @returns `true` if card rendering can continue.
|
* @returns `true` if card rendering can continue.
|
||||||
*/
|
*/
|
||||||
public async initializeMandatory(): Promise<boolean> {
|
public async initializeMandatory(): Promise<void> {
|
||||||
|
await this._initializationQueue.add(() => this._initializeMandatory());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _initializeMandatory(): Promise<void> {
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
if (!hass) {
|
if (!hass || this.isInitializedMandatory()) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -60,12 +77,12 @@ export class InitializationManager {
|
|||||||
await sideLoadHomeAssistantElements(),
|
await sideLoadHomeAssistantElements(),
|
||||||
}))
|
}))
|
||||||
) {
|
) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
const config = this._api.getConfigManager().getConfig();
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return false;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
@@ -83,76 +100,23 @@ export class InitializationManager {
|
|||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
) {
|
) {
|
||||||
return false;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._api.getMessageManager().hasMessage()) {
|
|
||||||
if (!this._api.getViewManager().hasView()) {
|
|
||||||
// Set a view on initial load. However, if the query string contains a
|
|
||||||
// view related action, we don't set any view here and allow that content
|
|
||||||
// to be triggered by the firstUpdated() call that runs query string
|
|
||||||
// actions. To do otherwise may cause a race condition between the default
|
|
||||||
// view and the querystring view, see:
|
|
||||||
// https://github.com/dermotduffy/frigate-hass-card/issues/1200
|
|
||||||
const hasViewRelatedActions = this._api
|
|
||||||
.getQueryStringManager()
|
|
||||||
.hasViewRelatedActions();
|
|
||||||
if (hasViewRelatedActions) {
|
|
||||||
this._api.getQueryStringManager().executeViewRelated();
|
|
||||||
} else {
|
|
||||||
this._api.getViewManager().setViewDefaultWithNewQuery({ failSafe: true });
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// If we already have a view, something (e.g. cameras) may have been
|
|
||||||
// reinitialized, be sure to ask for an update.
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize aspects of the card that can load in the 'background'.
|
|
||||||
* @returns `true` if card rendering can continue.
|
|
||||||
*/
|
|
||||||
public async initializeBackgroundIfNecessary(): Promise<boolean> {
|
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
|
||||||
|
|
||||||
if (!hass || !config) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this._initializer.isInitializedMultiple([
|
this._api.getMessageManager().hasMessage() ||
|
||||||
InitializationAspect.DEFAULT_RESET,
|
!(await this._initializer.initializeIfNecessary(
|
||||||
...(config.menu.buttons.media_player.enabled
|
InitializationAspect.VIEW,
|
||||||
? [InitializationAspect.MEDIA_PLAYERS]
|
this._api.getViewManager().initialize,
|
||||||
: []),
|
))
|
||||||
])
|
|
||||||
) {
|
) {
|
||||||
return true;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(await this._initializer.initializeMultipleIfNecessary({
|
|
||||||
[InitializationAspect.DEFAULT_RESET]: async () =>
|
|
||||||
await this._api.getDefaultManager().initialize(),
|
|
||||||
...(config.menu.buttons.media_player.enabled && {
|
|
||||||
[InitializationAspect.MEDIA_PLAYERS]: async () =>
|
|
||||||
await this._api.getMediaPlayerManager().initialize(),
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this._api.getCardElementManager().update();
|
this._api.getCardElementManager().update();
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public uninitialize(aspect: InitializationAspect) {
|
public uninitialize(aspect: InitializationAspect): void {
|
||||||
return this._initializer.uninitialize(aspect);
|
this._initializer.uninitialize(aspect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CameraConfig } from '../config/types';
|
import { CameraConfig, FrigateCardConfig } from '../config/types';
|
||||||
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../const';
|
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../const';
|
||||||
import { localize } from '../localize/localize';
|
import { localize } from '../localize/localize';
|
||||||
import { errorToConsole } from '../utils/basic';
|
import { errorToConsole } from '../utils/basic';
|
||||||
@@ -25,9 +25,23 @@ export class MediaPlayerManager {
|
|||||||
return this._mediaPlayers.length > 0;
|
return this._mediaPlayers.length > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async initializeIfNecessary(
|
||||||
|
previousConfig: FrigateCardConfig | null,
|
||||||
|
): Promise<void> {
|
||||||
|
if (
|
||||||
|
previousConfig?.menu.buttons.media_player.enabled !==
|
||||||
|
this._api.getConfigManager().getConfig()?.menu.buttons.media_player.enabled
|
||||||
|
) {
|
||||||
|
await this.initialize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async initialize(): Promise<boolean> {
|
public async initialize(): Promise<boolean> {
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
if (!hass) {
|
if (
|
||||||
|
!hass ||
|
||||||
|
!this._api.getConfigManager().getConfig()?.menu.buttons.media_player.enabled
|
||||||
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,18 +23,18 @@ export class QueryStringManager {
|
|||||||
return !!this._calculateIntent().view;
|
return !!this._calculateIntent().view;
|
||||||
}
|
}
|
||||||
|
|
||||||
public executeNonViewRelated = (): void => {
|
public executeNonViewRelated = async (): Promise<void> => {
|
||||||
this._executeNonViewRelated(this._calculateIntent());
|
await this._executeNonViewRelated(this._calculateIntent());
|
||||||
};
|
};
|
||||||
|
|
||||||
public executeViewRelated = (): void => {
|
public executeViewRelated = async (): Promise<void> => {
|
||||||
this._executeViewRelated(this._calculateIntent());
|
await this._executeViewRelated(this._calculateIntent());
|
||||||
};
|
};
|
||||||
|
|
||||||
public executeAll = (): void => {
|
public executeAll = async (): Promise<void> => {
|
||||||
const intent = this._calculateIntent();
|
const intent = this._calculateIntent();
|
||||||
this._executeViewRelated(intent);
|
await this._executeViewRelated(intent);
|
||||||
this._executeNonViewRelated(intent);
|
await this._executeNonViewRelated(intent);
|
||||||
};
|
};
|
||||||
|
|
||||||
protected async _executeViewRelated(intent: QueryStringViewIntent): Promise<void> {
|
protected async _executeViewRelated(intent: QueryStringViewIntent): Promise<void> {
|
||||||
@@ -62,7 +62,7 @@ export class QueryStringManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _executeNonViewRelated(intent: QueryStringViewIntent): void {
|
protected async _executeNonViewRelated(intent: QueryStringViewIntent): Promise<void> {
|
||||||
if (
|
if (
|
||||||
// Only execute non-view actions when the card has rendered at least once.
|
// Only execute non-view actions when the card has rendered at least once.
|
||||||
!this._api.getCardElementManager().hasUpdated() ||
|
!this._api.getCardElementManager().hasUpdated() ||
|
||||||
@@ -71,7 +71,7 @@ export class QueryStringManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this._api.getActionsManager().executeActions(intent.other);
|
await this._api.getActionsManager().executeActions(intent.other);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _calculateIntent(): QueryStringViewIntent {
|
protected _calculateIntent(): QueryStringViewIntent {
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export interface CardConfigAPI {
|
|||||||
getDefaultManager(): DefaultManager;
|
getDefaultManager(): DefaultManager;
|
||||||
getInitializationManager(): InitializationManager;
|
getInitializationManager(): InitializationManager;
|
||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
|
getMediaPlayerManager(): MediaPlayerManager;
|
||||||
getMessageManager(): MessageManager;
|
getMessageManager(): MessageManager;
|
||||||
getStatusBarItemManager(): StatusBarItemManager;
|
getStatusBarItemManager(): StatusBarItemManager;
|
||||||
getStyleManager(): StyleManager;
|
getStyleManager(): StyleManager;
|
||||||
@@ -201,6 +202,7 @@ export interface CardMediaLoadedAPI {
|
|||||||
|
|
||||||
export interface CardMediaPlayerAPI {
|
export interface CardMediaPlayerAPI {
|
||||||
getCameraManager(): CameraManager;
|
getCameraManager(): CameraManager;
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
getEntityRegistryManager(): EntityRegistryManager;
|
getEntityRegistryManager(): EntityRegistryManager;
|
||||||
getHASSManager(): HASSManager;
|
getHASSManager(): HASSManager;
|
||||||
getMessageManager(): MessageManager;
|
getMessageManager(): MessageManager;
|
||||||
@@ -257,6 +259,7 @@ export interface CardViewAPI {
|
|||||||
getHASSManager(): HASSManager;
|
getHASSManager(): HASSManager;
|
||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
getMessageManager(): MessageManager;
|
getMessageManager(): MessageManager;
|
||||||
|
getQueryStringManager(): QueryStringManager;
|
||||||
getStyleManager(): StyleManager;
|
getStyleManager(): StyleManager;
|
||||||
getTriggersManager(): TriggersManager;
|
getTriggersManager(): TriggersManager;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -133,6 +133,24 @@ export class ViewManager implements ViewManagerInterface {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public initialize = async (): Promise<boolean> => {
|
||||||
|
// Set a view on initial load. However, if the query string contains a view
|
||||||
|
// related action, we don't set any view here and allow that content to be
|
||||||
|
// triggered by the firstUpdated() call that runs query string actions. To
|
||||||
|
// do otherwise may cause a race condition between the default view and the
|
||||||
|
// querystring view, see:
|
||||||
|
// https://github.com/dermotduffy/frigate-hass-card/issues/1200
|
||||||
|
const hasViewRelatedActions = this._api
|
||||||
|
.getQueryStringManager()
|
||||||
|
.hasViewRelatedActions();
|
||||||
|
if (hasViewRelatedActions) {
|
||||||
|
await this._api.getQueryStringManager().executeViewRelated();
|
||||||
|
} else {
|
||||||
|
await this.setViewDefaultWithNewQuery({ failSafe: true });
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
protected _setView(view: View | null): void {
|
protected _setView(view: View | null): void {
|
||||||
const oldView = this._view;
|
const oldView = this._view;
|
||||||
|
|
||||||
|
|||||||
+6
-15
@@ -15,7 +15,7 @@ import { FrigateCardElements } from './components/elements.js';
|
|||||||
import './components/menu.js';
|
import './components/menu.js';
|
||||||
import { FrigateCardMenu } from './components/menu.js';
|
import { FrigateCardMenu } from './components/menu.js';
|
||||||
import './components/message.js';
|
import './components/message.js';
|
||||||
import { renderMessage, renderProgressIndicator } from './components/message.js';
|
import { renderMessage } from './components/message.js';
|
||||||
import './components/overlay.js';
|
import './components/overlay.js';
|
||||||
import { FrigateCardOverlay } from './components/overlay.js';
|
import { FrigateCardOverlay } from './components/overlay.js';
|
||||||
import './components/status-bar';
|
import './components/status-bar';
|
||||||
@@ -188,10 +188,6 @@ class FrigateCard extends LitElement {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected willUpdate(): void {
|
|
||||||
this._controller.getInitializationManager().initializeBackgroundIfNecessary();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _renderMenuStatusContainer(
|
protected _renderMenuStatusContainer(
|
||||||
position: 'top' | 'bottom' | 'overlay',
|
position: 'top' | 'bottom' | 'overlay',
|
||||||
): TemplateResult | void {
|
): TemplateResult | void {
|
||||||
@@ -373,12 +369,8 @@ class FrigateCard extends LitElement {
|
|||||||
${this._renderMenuStatusContainer('top')}
|
${this._renderMenuStatusContainer('top')}
|
||||||
<div ${ref(this._refMain)} class="${classMap(mainClasses)}">
|
<div ${ref(this._refMain)} class="${classMap(mainClasses)}">
|
||||||
${this._renderMenuStatusContainer('overlay')}
|
${this._renderMenuStatusContainer('overlay')}
|
||||||
${!cameraManager.isInitialized() &&
|
${
|
||||||
!this._controller.getMessageManager().hasMessage()
|
// Always want to render <frigate-card-views> even if there's a message, to
|
||||||
? renderProgressIndicator({
|
|
||||||
cardWideConfig: this._controller.getConfigManager().getCardWideConfig(),
|
|
||||||
})
|
|
||||||
: // Always want to render <frigate-card-views> even if there's a message, to
|
|
||||||
// ensure live preload is always present (even if not displayed).
|
// ensure live preload is always present (even if not displayed).
|
||||||
html`<frigate-card-views
|
html`<frigate-card-views
|
||||||
${ref(this._refViews)}
|
${ref(this._refViews)}
|
||||||
@@ -390,9 +382,7 @@ class FrigateCard extends LitElement {
|
|||||||
.getConfigManager()
|
.getConfigManager()
|
||||||
.getNonOverriddenConfig()}
|
.getNonOverriddenConfig()}
|
||||||
.overriddenConfig=${this._controller.getConfigManager().getConfig()}
|
.overriddenConfig=${this._controller.getConfigManager().getConfig()}
|
||||||
.cardWideConfig=${this._controller
|
.cardWideConfig=${this._controller.getConfigManager().getCardWideConfig()}
|
||||||
.getConfigManager()
|
|
||||||
.getCardWideConfig()}
|
|
||||||
.rawConfig=${this._controller.getConfigManager().getRawConfig()}
|
.rawConfig=${this._controller.getConfigManager().getRawConfig()}
|
||||||
.configManager=${this._controller.getConfigManager()}
|
.configManager=${this._controller.getConfigManager()}
|
||||||
.conditionsManagerEpoch=${this._controller
|
.conditionsManagerEpoch=${this._controller
|
||||||
@@ -403,7 +393,8 @@ class FrigateCard extends LitElement {
|
|||||||
.triggeredCameraIDs=${this._config?.view.triggers.show_trigger_status
|
.triggeredCameraIDs=${this._config?.view.triggers.show_trigger_status
|
||||||
? this._controller.getTriggersManager().getTriggeredCameraIDs()
|
? this._controller.getTriggersManager().getTriggeredCameraIDs()
|
||||||
: undefined}
|
: undefined}
|
||||||
></frigate-card-views>`}
|
></frigate-card-views>`
|
||||||
|
}
|
||||||
${
|
${
|
||||||
// Keep message rendering to last to show messages that may have been
|
// Keep message rendering to last to show messages that may have been
|
||||||
// generated during the render.
|
// generated during the render.
|
||||||
|
|||||||
@@ -21,3 +21,9 @@
|
|||||||
$ convert -extent 2048x1152 -flop -gravity center 47543120431_b285c45ac8_k.jpg frigate-bird-in-sky.jpg
|
$ convert -extent 2048x1152 -flop -gravity center 47543120431_b285c45ac8_k.jpg frigate-bird-in-sky.jpg
|
||||||
$ mogrify -strip -interlace Plane -quality 85% -scale 492x277 frigate-bird-in-sky.jpg
|
$ mogrify -strip -interlace Plane -quality 85% -scale 492x277 frigate-bird-in-sky.jpg
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## camera-iris.svg
|
||||||
|
|
||||||
|
**Link**: https://pictogrammers.com/library/mdi/icon/camera-iris/
|
||||||
|
|
||||||
|
**License**: https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13.73,15L9.83,21.76C10.53,21.91 11.25,22 12,22C14.4,22 16.6,21.15 18.32,19.75L14.66,13.4M2.46,15C3.38,17.92 5.61,20.26 8.45,21.34L12.12,15M8.54,12L4.64,5.25C3,7 2,9.39 2,12C2,12.68 2.07,13.35 2.2,14H9.69M21.8,10H14.31L14.6,10.5L19.36,18.75C21,16.97 22,14.6 22,12C22,11.31 21.93,10.64 21.8,10M21.54,9C20.62,6.07 18.39,3.74 15.55,2.66L11.88,9M9.4,10.5L14.17,2.24C13.47,2.09 12.75,2 12,2C9.6,2 7.4,2.84 5.68,4.25L9.34,10.6L9.4,10.5Z" /></svg>
|
||||||
|
After Width: | Height: | Size: 509 B |
+8
-4
@@ -1,6 +1,8 @@
|
|||||||
import {
|
import {
|
||||||
HomeAssistant,
|
HomeAssistant,
|
||||||
|
LovelaceCard,
|
||||||
LovelaceCardConfig,
|
LovelaceCardConfig,
|
||||||
|
LovelaceCardEditor,
|
||||||
Themes,
|
Themes,
|
||||||
} from '@dermotduffy/custom-card-helpers';
|
} from '@dermotduffy/custom-card-helpers';
|
||||||
import { StyleInfo } from 'lit/directives/style-map.js';
|
import { StyleInfo } from 'lit/directives/style-map.js';
|
||||||
@@ -86,12 +88,14 @@ export interface FrigateCardMediaPlayer {
|
|||||||
isPaused(): boolean;
|
isPaused(): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CardHelpers {
|
export type LovelaceCardWithEditor = LovelaceCard & {
|
||||||
createCardElement(config: LovelaceCardConfig): Promise<{
|
|
||||||
constructor: {
|
constructor: {
|
||||||
getConfigElement(): HTMLElement;
|
getConfigElement(): Promise<LovelaceCardEditor>;
|
||||||
};
|
};
|
||||||
}>;
|
};
|
||||||
|
|
||||||
|
export interface CardHelpers {
|
||||||
|
createCardElement(config: LovelaceCardConfig): Promise<LovelaceCardWithEditor>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type PTZMovementType = 'relative' | 'continuous';
|
export type PTZMovementType = 'relative' | 'continuous';
|
||||||
|
|||||||
+11
-6
@@ -11,6 +11,7 @@ import {
|
|||||||
CardHelpers,
|
CardHelpers,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FrigateCardError,
|
FrigateCardError,
|
||||||
|
LovelaceCardWithEditor,
|
||||||
SignedPath,
|
SignedPath,
|
||||||
signedPathSchema,
|
signedPathSchema,
|
||||||
StateParameters,
|
StateParameters,
|
||||||
@@ -316,17 +317,21 @@ export const sideLoadHomeAssistantElements = async (): Promise<boolean> => {
|
|||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
const helpers: CardHelpers = await (window as any).loadCardHelpers();
|
const helpers: CardHelpers = await (window as any).loadCardHelpers();
|
||||||
|
|
||||||
// The picture-glance editor loads everything this card needs.
|
// This bizarre combination of hacks creates a dummy picture glance card, then
|
||||||
const pictureGlance = await helpers.createCardElement({
|
// waits for it to be fully loaded/upgraded as a custom element, so it will
|
||||||
|
// have the getConfigElement() method which is necessary to load all the
|
||||||
|
// elements this card requires.
|
||||||
|
await helpers.createCardElement({
|
||||||
type: 'picture-glance',
|
type: 'picture-glance',
|
||||||
entities: [],
|
entities: [],
|
||||||
camera_image: 'dummy-to-load-editor-components',
|
camera_image: 'dummy-to-load-editor-components',
|
||||||
});
|
});
|
||||||
if (pictureGlance.constructor.getConfigElement) {
|
|
||||||
await pictureGlance.constructor.getConfigElement();
|
const pgcConstructor = await customElements.whenDefined('hui-picture-glance-card');
|
||||||
|
const pgc = new pgcConstructor() as LovelaceCardWithEditor;
|
||||||
|
|
||||||
|
await pgc.constructor.getConfigElement();
|
||||||
return true;
|
return true;
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,21 +1,14 @@
|
|||||||
import { allPromises } from '../basic';
|
import { allPromises } from '../basic';
|
||||||
|
|
||||||
enum InitializationState {
|
|
||||||
INITIALIZING = 'initializing',
|
|
||||||
INITIALIZED = 'initialized',
|
|
||||||
}
|
|
||||||
|
|
||||||
type InitializationCallback = () => Promise<boolean>;
|
type InitializationCallback = () => Promise<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages initialization state & calling initializers.
|
* Manages initialization state & calling initializers. There is no guarantee
|
||||||
|
* something will not be initialized twice unless there are concurrency controls
|
||||||
|
* applied to the usage of this class.
|
||||||
*/
|
*/
|
||||||
export class Initializer {
|
export class Initializer {
|
||||||
protected _state: Map<string, InitializationState>;
|
protected _initialized: Set<string> = new Set();
|
||||||
|
|
||||||
constructor() {
|
|
||||||
this._state = new Map();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async initializeMultipleIfNecessary(
|
public async initializeMultipleIfNecessary(
|
||||||
aspects: Record<string, InitializationCallback>,
|
aspects: Record<string, InitializationCallback>,
|
||||||
@@ -27,43 +20,30 @@ export class Initializer {
|
|||||||
return results.every(Boolean);
|
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(
|
public async initializeIfNecessary(
|
||||||
aspect: string,
|
aspect: string,
|
||||||
initializer?: InitializationCallback,
|
initializer?: InitializationCallback,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const state = this._state.get(aspect);
|
if (this._initialized.has(aspect)) {
|
||||||
if (state !== InitializationState.INITIALIZED) {
|
return true;
|
||||||
if (state !== InitializationState.INITIALIZING) {
|
}
|
||||||
if (initializer) {
|
if (!initializer) {
|
||||||
this._state.set(aspect, InitializationState.INITIALIZING);
|
this._initialized.add(aspect);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (await initializer()) {
|
if (await initializer()) {
|
||||||
this._state.set(aspect, InitializationState.INITIALIZED);
|
this._initialized.add(aspect);
|
||||||
} else {
|
|
||||||
this.uninitialize(aspect);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
this._state.set(aspect, InitializationState.INITIALIZED);
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public uninitialize(aspect: string) {
|
public uninitialize(aspect: string): void {
|
||||||
return this._state.delete(aspect);
|
this._initialized.delete(aspect);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isInitialized(aspect: string): boolean {
|
public isInitialized(aspect: string): boolean {
|
||||||
return this._state.get(aspect) == InitializationState.INITIALIZED;
|
return this._initialized.has(aspect);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isInitializedMultiple(aspects: string[]): boolean {
|
public isInitializedMultiple(aspects: string[]): boolean {
|
||||||
|
|||||||
@@ -1289,4 +1289,19 @@ describe('CameraManager', async () => {
|
|||||||
expect(await manager.getMediaSeekTime(media, middleTime)).toBeNull();
|
expect(await manager.getMediaSeekTime(media, middleTime)).toBeNull();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reset', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const engine = mock<CameraManagerEngine>();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
const manager = createCameraManager(api, engine);
|
||||||
|
|
||||||
|
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
|
||||||
|
|
||||||
|
expect(manager.getStore().getCameraCount()).toBe(1);
|
||||||
|
|
||||||
|
await manager.reset();
|
||||||
|
|
||||||
|
expect(manager.getStore().getCameraCount()).toBe(0);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -308,4 +308,39 @@ describe('CameraManagerStore', async () => {
|
|||||||
);
|
);
|
||||||
expect(store.getCameraIDsWithCapability('clips')).toEqual(new Set(['one']));
|
expect(store.getCameraIDsWithCapability('clips')).toEqual(new Set(['one']));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('setCameras', async () => {
|
||||||
|
const store = new CameraManagerStore();
|
||||||
|
const camera_1 = new Camera(createCameraConfig({ id: 'camera-1' }), engineGeneric);
|
||||||
|
camera_1.destroy = vi.fn();
|
||||||
|
|
||||||
|
const camera_2 = new Camera(createCameraConfig({ id: 'camera-2' }), engineGeneric);
|
||||||
|
camera_2.destroy = vi.fn();
|
||||||
|
|
||||||
|
const camera_3 = new Camera(createCameraConfig({ id: 'camera-3' }), engineGeneric);
|
||||||
|
camera_3.destroy = vi.fn();
|
||||||
|
|
||||||
|
const camera_3_new = new Camera(
|
||||||
|
createCameraConfig({ id: 'camera-3' }),
|
||||||
|
engineGeneric,
|
||||||
|
);
|
||||||
|
camera_3_new.destroy = vi.fn();
|
||||||
|
|
||||||
|
const camera_4 = new Camera(createCameraConfig({ id: 'camera-4' }), engineGeneric);
|
||||||
|
camera_4.destroy = vi.fn();
|
||||||
|
|
||||||
|
await store.setCameras([camera_1, camera_2, camera_3]);
|
||||||
|
await store.setCameras([camera_2, camera_3_new, camera_4]);
|
||||||
|
|
||||||
|
expect(store.getCamera('camera-1')).toBeNull();
|
||||||
|
expect(store.getCamera('camera-2')).toBe(camera_2);
|
||||||
|
expect(store.getCamera('camera-3')).toBe(camera_3_new);
|
||||||
|
expect(store.getCamera('camera-4')).toBe(camera_4);
|
||||||
|
|
||||||
|
expect(camera_1.destroy).toBeCalled();
|
||||||
|
expect(camera_2.destroy).not.toBeCalled();
|
||||||
|
expect(camera_3.destroy).toBeCalled();
|
||||||
|
expect(camera_3_new.destroy).not.toBeCalled();
|
||||||
|
expect(camera_4.destroy).not.toBeCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { ZodError } from 'zod';
|
import { ZodError } from 'zod';
|
||||||
import { frigateCardConfigSchema } from '../../../src/config/types';
|
|
||||||
import { getOverriddenConfig } from '../../../src/card-controller/conditions-manager';
|
import { getOverriddenConfig } from '../../../src/card-controller/conditions-manager';
|
||||||
import { ConfigManager } from '../../../src/card-controller/config/config-manager';
|
import { ConfigManager } from '../../../src/card-controller/config/config-manager';
|
||||||
import { InitializationAspect } from '../../../src/card-controller/initialization-manager';
|
import { InitializationAspect } from '../../../src/card-controller/initialization-manager';
|
||||||
import { createCardAPI, createConfig } from '../../test-utils';
|
import { frigateCardConfigSchema } from '../../../src/config/types';
|
||||||
|
import { createCardAPI, createConfig, flushPromises } from '../../test-utils';
|
||||||
|
|
||||||
vi.mock('../../../src/card-controller/conditions-manager.js');
|
vi.mock('../../../src/card-controller/conditions-manager.js');
|
||||||
|
|
||||||
@@ -229,7 +229,9 @@ describe('ConfigManager', () => {
|
|||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
|
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
|
||||||
|
|
||||||
manager.setConfig(config_1);
|
manager.setConfig(config_1);
|
||||||
expect(api.getInitializationManager().uninitialize).not.toBeCalled();
|
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
|
||||||
|
InitializationAspect.VIEW,
|
||||||
|
);
|
||||||
|
|
||||||
const config_2 = {
|
const config_2 = {
|
||||||
type: 'custom:frigate-card',
|
type: 'custom:frigate-card',
|
||||||
@@ -238,7 +240,7 @@ describe('ConfigManager', () => {
|
|||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
|
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
|
||||||
manager.computeOverrideConfig();
|
manager.computeOverrideConfig();
|
||||||
|
|
||||||
expect(api.getInitializationManager().uninitialize).toBeCalledWith(
|
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
|
||||||
InitializationAspect.CAMERAS,
|
InitializationAspect.CAMERAS,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -253,7 +255,9 @@ describe('ConfigManager', () => {
|
|||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
|
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
|
||||||
|
|
||||||
manager.setConfig(config_1);
|
manager.setConfig(config_1);
|
||||||
expect(api.getInitializationManager().uninitialize).not.toBeCalled();
|
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
|
||||||
|
InitializationAspect.VIEW,
|
||||||
|
);
|
||||||
|
|
||||||
const config_2 = {
|
const config_2 = {
|
||||||
...config_1,
|
...config_1,
|
||||||
@@ -264,7 +268,7 @@ describe('ConfigManager', () => {
|
|||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
|
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
|
||||||
manager.computeOverrideConfig();
|
manager.computeOverrideConfig();
|
||||||
|
|
||||||
expect(api.getInitializationManager().uninitialize).toBeCalledWith(
|
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
|
||||||
InitializationAspect.CAMERAS,
|
InitializationAspect.CAMERAS,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -284,7 +288,9 @@ describe('ConfigManager', () => {
|
|||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
|
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
|
||||||
|
|
||||||
manager.setConfig(config_1);
|
manager.setConfig(config_1);
|
||||||
expect(api.getInitializationManager().uninitialize).not.toBeCalled();
|
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
|
||||||
|
InitializationAspect.VIEW,
|
||||||
|
);
|
||||||
|
|
||||||
const config_2 = {
|
const config_2 = {
|
||||||
...config_1,
|
...config_1,
|
||||||
@@ -297,42 +303,26 @@ describe('ConfigManager', () => {
|
|||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
|
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
|
||||||
manager.computeOverrideConfig();
|
manager.computeOverrideConfig();
|
||||||
|
|
||||||
expect(api.getInitializationManager().uninitialize).toBeCalledWith(
|
expect(api.getInitializationManager().uninitialize).toHaveBeenLastCalledWith(
|
||||||
InitializationAspect.MICROPHONE_CONNECT,
|
InitializationAspect.MICROPHONE_CONNECT,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('view.default_reset', () => {
|
it('should initialize background items', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const manager = new ConfigManager(api);
|
const manager = new ConfigManager(api);
|
||||||
const config_1 = {
|
const config = {
|
||||||
type: 'custom:frigate-card',
|
type: 'custom:frigate-card',
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
cameras: [{ camera_entity: 'camera.office' }],
|
||||||
view: {
|
|
||||||
default_reset: {
|
|
||||||
every_seconds: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
|
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config));
|
||||||
|
|
||||||
manager.setConfig(config_1);
|
manager.setConfig(config);
|
||||||
expect(api.getInitializationManager().uninitialize).not.toBeCalled();
|
|
||||||
|
|
||||||
const config_2 = {
|
await flushPromises();
|
||||||
...config_1,
|
|
||||||
view: {
|
|
||||||
default_reset: {
|
|
||||||
every_seconds: 2,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
|
|
||||||
manager.computeOverrideConfig();
|
|
||||||
|
|
||||||
expect(api.getInitializationManager().uninitialize).toBeCalledWith(
|
expect(api.getDefaultManager().initializeIfNecessary).toBeCalledWith(null);
|
||||||
InitializationAspect.DEFAULT_RESET,
|
expect(api.getMediaPlayerManager().initializeIfNecessary).toBeCalledWith(null);
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ describe('DefaultManager', () => {
|
|||||||
describe('time based', () => {
|
describe('time based', () => {
|
||||||
it('should set default view when allowed', async () => {
|
it('should set default view when allowed', async () => {
|
||||||
const api = createCardAPIWithStateWatcher();
|
const api = createCardAPIWithStateWatcher();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
createConfig({
|
createConfig({
|
||||||
view: {
|
view: {
|
||||||
@@ -62,7 +63,8 @@ describe('DefaultManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not set default view when not configured', () => {
|
it('should not set default view when not configured', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPIWithStateWatcher();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
createConfig({
|
createConfig({
|
||||||
view: {
|
view: {
|
||||||
@@ -88,6 +90,7 @@ describe('DefaultManager', () => {
|
|||||||
|
|
||||||
it('should restart timer when reconfigured', async () => {
|
it('should restart timer when reconfigured', async () => {
|
||||||
const api = createCardAPIWithStateWatcher();
|
const api = createCardAPIWithStateWatcher();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
createConfig({
|
createConfig({
|
||||||
view: {
|
view: {
|
||||||
@@ -99,9 +102,6 @@ describe('DefaultManager', () => {
|
|||||||
);
|
);
|
||||||
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
|
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
|
||||||
|
|
||||||
const hass = createHASS();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
||||||
|
|
||||||
vi.useFakeTimers();
|
vi.useFakeTimers();
|
||||||
|
|
||||||
const manager = new DefaultManager(api);
|
const manager = new DefaultManager(api);
|
||||||
@@ -120,6 +120,7 @@ describe('DefaultManager', () => {
|
|||||||
|
|
||||||
it('should set default view when state changed', async () => {
|
it('should set default view when state changed', async () => {
|
||||||
const api = createCardAPIWithStateWatcher();
|
const api = createCardAPIWithStateWatcher();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
createConfig({
|
createConfig({
|
||||||
view: {
|
view: {
|
||||||
@@ -210,4 +211,47 @@ describe('DefaultManager', () => {
|
|||||||
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith(manager);
|
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith(manager);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reinitialize when there is a config change', async () => {
|
||||||
|
const configOn = createConfig({
|
||||||
|
view: {
|
||||||
|
default_reset: {
|
||||||
|
every_seconds: 10,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const configOff = createConfig({
|
||||||
|
view: {
|
||||||
|
default_reset: {
|
||||||
|
every_seconds: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = createCardAPIWithStateWatcher();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
|
||||||
|
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const manager = new DefaultManager(api);
|
||||||
|
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(configOn);
|
||||||
|
await manager.initializeIfNecessary(null);
|
||||||
|
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
expect(api.getViewManager().setViewDefault).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(configOff);
|
||||||
|
await manager.initializeIfNecessary(configOn);
|
||||||
|
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
expect(api.getViewManager().setViewDefault).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(configOff);
|
||||||
|
await manager.initializeIfNecessary(configOff);
|
||||||
|
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
expect(api.getViewManager().setViewDefault).toBeCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,50 +21,23 @@ describe('InitializationManager', () => {
|
|||||||
describe('should correctly determine when mandatory initialization is required', () => {
|
describe('should correctly determine when mandatory initialization is required', () => {
|
||||||
it('without config', () => {
|
it('without config', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const initializer = mock<Initializer>();
|
const manager = new InitializationManager(api);
|
||||||
const manager = new InitializationManager(api, initializer);
|
|
||||||
|
|
||||||
expect(manager.isInitializedMandatory()).toBeFalsy();
|
expect(manager.isInitializedMandatory()).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('without aspects', () => {
|
it('without aspects', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const initializer = mock<Initializer>();
|
const manager = new InitializationManager(api);
|
||||||
const manager = new InitializationManager(api, initializer);
|
|
||||||
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
||||||
initializer.isInitializedMultiple.mockReturnValue(false);
|
|
||||||
|
|
||||||
expect(manager.isInitializedMandatory()).toBeFalsy();
|
expect(manager.isInitializedMandatory()).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('without view', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const initializer = mock<Initializer>();
|
|
||||||
const manager = new InitializationManager(api, initializer);
|
|
||||||
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
initializer.isInitializedMultiple.mockReturnValue(true);
|
|
||||||
|
|
||||||
expect(manager.isInitializedMandatory()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with aspects and view', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const initializer = mock<Initializer>();
|
|
||||||
const manager = new InitializationManager(api, initializer);
|
|
||||||
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
initializer.isInitializedMultiple.mockReturnValue(true);
|
|
||||||
vi.mocked(api.getViewManager().hasView).mockReturnValue(true);
|
|
||||||
|
|
||||||
expect(manager.isInitializedMandatory()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with microphone if configured', () => {
|
it('with microphone if configured', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const initializer = mock<Initializer>();
|
const manager = new InitializationManager(api);
|
||||||
const manager = new InitializationManager(api, initializer);
|
|
||||||
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
createConfig({
|
createConfig({
|
||||||
@@ -75,24 +48,25 @@ describe('InitializationManager', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
initializer.isInitializedMultiple.mockReturnValue(true);
|
|
||||||
vi.mocked(api.getViewManager().hasView).mockReturnValue(true);
|
|
||||||
|
|
||||||
expect(manager.isInitializedMandatory()).toBeTruthy();
|
expect(manager.isInitializedMandatory()).toBeFalsy();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('should initialize mandatory', () => {
|
describe('should initialize mandatory', () => {
|
||||||
it('without hass', async () => {
|
it('without hass', async () => {
|
||||||
const manager = new InitializationManager(createCardAPI());
|
const manager = new InitializationManager(createCardAPI());
|
||||||
expect(await manager.initializeMandatory()).toBeFalsy();
|
await manager.initializeMandatory();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('without config', async () => {
|
it('without config', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const manager = new InitializationManager(api);
|
const manager = new InitializationManager(api);
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
expect(await manager.initializeMandatory()).toBeFalsy();
|
vi.mocked(loadLanguages).mockResolvedValue(true);
|
||||||
|
vi.mocked(sideLoadHomeAssistantElements).mockResolvedValue(true);
|
||||||
|
|
||||||
|
await manager.initializeMandatory();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('successfully', async () => {
|
it('successfully', async () => {
|
||||||
@@ -103,15 +77,23 @@ describe('InitializationManager', () => {
|
|||||||
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(
|
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
vi.mocked(loadLanguages).mockResolvedValue(true);
|
||||||
|
vi.mocked(sideLoadHomeAssistantElements).mockResolvedValue(true);
|
||||||
|
vi.mocked(api.getCameraManager().initializeCamerasFromConfig).mockResolvedValue(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
vi.mocked(api.getViewManager().initialize).mockResolvedValue(true);
|
||||||
|
|
||||||
const manager = new InitializationManager(api);
|
const manager = new InitializationManager(api);
|
||||||
|
|
||||||
expect(await manager.initializeMandatory()).toBeTruthy();
|
await manager.initializeMandatory();
|
||||||
|
|
||||||
expect(loadLanguages).toBeCalled();
|
expect(loadLanguages).toBeCalled();
|
||||||
expect(sideLoadHomeAssistantElements).toBeCalled();
|
expect(sideLoadHomeAssistantElements).toBeCalled();
|
||||||
expect(api.getCameraManager().initializeCamerasFromConfig).toBeCalled();
|
expect(api.getCameraManager().initializeCamerasFromConfig).toBeCalled();
|
||||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
expect(api.getViewManager().initialize).toBeCalled();
|
||||||
expect(api.getMicrophoneManager().connect).not.toBeCalled();
|
expect(api.getMicrophoneManager().connect).not.toBeCalled();
|
||||||
|
expect(api.getCardElementManager().update).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('successfully with microphone if configured', async () => {
|
it('successfully with microphone if configured', async () => {
|
||||||
@@ -126,25 +108,19 @@ describe('InitializationManager', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
vi.mocked(loadLanguages).mockResolvedValue(true);
|
||||||
|
vi.mocked(sideLoadHomeAssistantElements).mockResolvedValue(true);
|
||||||
|
vi.mocked(api.getCameraManager().initializeCamerasFromConfig).mockResolvedValue(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const manager = new InitializationManager(api);
|
const manager = new InitializationManager(api);
|
||||||
|
|
||||||
expect(await manager.initializeMandatory()).toBeTruthy();
|
await manager.initializeMandatory();
|
||||||
|
|
||||||
expect(api.getMicrophoneManager().connect).toBeCalled();
|
expect(api.getMicrophoneManager().connect).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('successfully with querystring view', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(false);
|
|
||||||
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(true);
|
|
||||||
const manager = new InitializationManager(api);
|
|
||||||
|
|
||||||
expect(await manager.initializeMandatory()).toBeTruthy();
|
|
||||||
|
|
||||||
expect(api.getQueryStringManager().executeViewRelated).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with message set during initialization', async () => {
|
it('with message set during initialization', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
@@ -153,12 +129,17 @@ describe('InitializationManager', () => {
|
|||||||
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(
|
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
|
vi.mocked(loadLanguages).mockResolvedValue(true);
|
||||||
|
vi.mocked(sideLoadHomeAssistantElements).mockResolvedValue(true);
|
||||||
|
vi.mocked(api.getCameraManager().initializeCamerasFromConfig).mockResolvedValue(
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
const manager = new InitializationManager(api);
|
const manager = new InitializationManager(api);
|
||||||
|
|
||||||
expect(await manager.initializeMandatory()).toBeTruthy();
|
await manager.initializeMandatory();
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
expect(api.getViewManager().initialize).not.toBeCalled();
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with languages and side load elements in progress', async () => {
|
it('with languages and side load elements in progress', async () => {
|
||||||
@@ -168,7 +149,7 @@ describe('InitializationManager', () => {
|
|||||||
const manager = new InitializationManager(api, initializer);
|
const manager = new InitializationManager(api, initializer);
|
||||||
initializer.initializeMultipleIfNecessary.mockResolvedValue(false);
|
initializer.initializeMultipleIfNecessary.mockResolvedValue(false);
|
||||||
|
|
||||||
expect(await manager.initializeMandatory()).toBeFalsy();
|
await manager.initializeMandatory();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('with cameras in progress', async () => {
|
it('with cameras in progress', async () => {
|
||||||
@@ -182,100 +163,7 @@ describe('InitializationManager', () => {
|
|||||||
.mockResolvedValueOnce(true)
|
.mockResolvedValueOnce(true)
|
||||||
.mockResolvedValueOnce(false);
|
.mockResolvedValueOnce(false);
|
||||||
|
|
||||||
expect(await manager.initializeMandatory()).toBeFalsy();
|
await manager.initializeMandatory();
|
||||||
});
|
|
||||||
|
|
||||||
it('with existing view', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getViewManager().hasView).mockReturnValue(true);
|
|
||||||
|
|
||||||
const initializer = mock<Initializer>();
|
|
||||||
const manager = new InitializationManager(api, initializer);
|
|
||||||
initializer.initializeMultipleIfNecessary
|
|
||||||
.mockResolvedValueOnce(true)
|
|
||||||
.mockResolvedValueOnce(true);
|
|
||||||
|
|
||||||
expect(await manager.initializeMandatory()).toBeTruthy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should initialize background', () => {
|
|
||||||
it('without hass and config', async () => {
|
|
||||||
const manager = new InitializationManager(createCardAPI());
|
|
||||||
expect(await manager.initializeBackgroundIfNecessary()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('successfully when already initialized', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
|
|
||||||
const initializer = mock<Initializer>();
|
|
||||||
initializer.isInitializedMultiple.mockReturnValue(true);
|
|
||||||
|
|
||||||
const manager = new InitializationManager(api, initializer);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
menu: {
|
|
||||||
buttons: {
|
|
||||||
media_player: {
|
|
||||||
enabled: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(await manager.initializeBackgroundIfNecessary()).toBeTruthy();
|
|
||||||
expect(api.getMediaPlayerManager().initialize).not.toBeCalled();
|
|
||||||
expect(api.getDefaultManager().initialize).not.toBeCalled();
|
|
||||||
expect(api.getCardElementManager().update).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('successfully with all inititalizers', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new InitializationManager(api);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
menu: {
|
|
||||||
buttons: {
|
|
||||||
media_player: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(await manager.initializeBackgroundIfNecessary()).toBeTruthy();
|
|
||||||
expect(api.getMediaPlayerManager().initialize).toBeCalled();
|
|
||||||
expect(api.getDefaultManager().initialize).toBeCalled();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with initializers in progress', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
menu: {
|
|
||||||
buttons: {
|
|
||||||
media_player: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const initializer = mock<Initializer>();
|
|
||||||
|
|
||||||
const manager = new InitializationManager(api, initializer);
|
|
||||||
initializer.initializeMultipleIfNecessary.mockResolvedValue(false);
|
|
||||||
|
|
||||||
expect(await manager.initializeBackgroundIfNecessary()).toBeFalsy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
createCameraManager,
|
createCameraManager,
|
||||||
createCardAPI,
|
createCardAPI,
|
||||||
|
createConfig,
|
||||||
createHASS,
|
createHASS,
|
||||||
createRegistryEntity,
|
createRegistryEntity,
|
||||||
createStateEntity,
|
createStateEntity,
|
||||||
@@ -75,6 +76,17 @@ describe('MediaPlayerManager', () => {
|
|||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
||||||
createHASSWithMediaPlayers(),
|
createHASSWithMediaPlayers(),
|
||||||
);
|
);
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
menu: {
|
||||||
|
buttons: {
|
||||||
|
media_player: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
vi.mocked(api.getEntityRegistryManager).mockReturnValue(entityRegistryManager);
|
vi.mocked(api.getEntityRegistryManager).mockReturnValue(entityRegistryManager);
|
||||||
const manager = new MediaPlayerManager(api);
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
@@ -108,6 +120,17 @@ describe('MediaPlayerManager', () => {
|
|||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
||||||
createHASSWithMediaPlayers(),
|
createHASSWithMediaPlayers(),
|
||||||
);
|
);
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
menu: {
|
||||||
|
buttons: {
|
||||||
|
media_player: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
vi.mocked(api.getEntityRegistryManager).mockReturnValue(entityRegistryManager);
|
vi.mocked(api.getEntityRegistryManager).mockReturnValue(entityRegistryManager);
|
||||||
const manager = new MediaPlayerManager(api);
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
@@ -121,6 +144,39 @@ describe('MediaPlayerManager', () => {
|
|||||||
expect(manager.hasMediaPlayers()).toBeTruthy();
|
expect(manager.hasMediaPlayers()).toBeTruthy();
|
||||||
expect(spy).toBeCalled();
|
expect(spy).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should reinitialize when there is a config change', async () => {
|
||||||
|
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||||
|
entityRegistryManager.getEntities.mockResolvedValue(
|
||||||
|
new Map([
|
||||||
|
['media_player.ok1', createRegistryEntity({ hidden_by: '' })],
|
||||||
|
['media_player.ok2', createRegistryEntity({ hidden_by: 'user' })],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
||||||
|
createHASSWithMediaPlayers(),
|
||||||
|
);
|
||||||
|
const config = createConfig({
|
||||||
|
menu: {
|
||||||
|
buttons: {
|
||||||
|
media_player: {
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(config);
|
||||||
|
vi.mocked(api.getEntityRegistryManager).mockReturnValue(entityRegistryManager);
|
||||||
|
const manager = new MediaPlayerManager(api);
|
||||||
|
|
||||||
|
await manager.initializeIfNecessary(null);
|
||||||
|
expect(manager.hasMediaPlayers()).toBeTruthy();
|
||||||
|
|
||||||
|
await manager.initializeIfNecessary(config);
|
||||||
|
expect(manager.hasMediaPlayers()).toBeTruthy();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should stop', async () => {
|
it('should stop', async () => {
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ describe('QueryStringManager', () => {
|
|||||||
global.window.location = mock<Location>();
|
global.window.location = mock<Location>();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject malformed query string', () => {
|
it('should reject malformed query string', async () => {
|
||||||
setQueryString('BOGUS_KEY=BOGUS_VALUE');
|
setQueryString('BOGUS_KEY=BOGUS_VALUE');
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true);
|
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true);
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeAll();
|
await manager.executeAll();
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeFalsy();
|
expect(manager.hasViewRelatedActions()).toBeFalsy();
|
||||||
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
||||||
@@ -41,7 +41,7 @@ describe('QueryStringManager', () => {
|
|||||||
['snapshot' as const],
|
['snapshot' as const],
|
||||||
['snapshots' as const],
|
['snapshots' as const],
|
||||||
['timeline' as const],
|
['timeline' as const],
|
||||||
])('%s', (viewName: string) => {
|
])('%s', async (viewName: string) => {
|
||||||
setQueryString(`?frigate-card-action.id.${viewName}=`);
|
setQueryString(`?frigate-card-action.id.${viewName}=`);
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ describe('QueryStringManager', () => {
|
|||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
|
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeAll();
|
await manager.executeAll();
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
||||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||||
@@ -66,13 +66,13 @@ describe('QueryStringManager', () => {
|
|||||||
['download' as const],
|
['download' as const],
|
||||||
['expand' as const],
|
['expand' as const],
|
||||||
['menu_toggle' as const],
|
['menu_toggle' as const],
|
||||||
])('%s', (action: string) => {
|
])('%s', async (action: string) => {
|
||||||
setQueryString(`?frigate-card-action.id.${action}=`);
|
setQueryString(`?frigate-card-action.id.${action}=`);
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeAll();
|
await manager.executeAll();
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeFalsy();
|
expect(manager.hasViewRelatedActions()).toBeFalsy();
|
||||||
expect(api.getActionsManager().executeActions).toBeCalledWith([
|
expect(api.getActionsManager().executeActions).toBeCalledWith([
|
||||||
@@ -85,7 +85,7 @@ describe('QueryStringManager', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute view default action', () => {
|
it('should execute view default action', async () => {
|
||||||
setQueryString('?frigate-card-action.id.default=');
|
setQueryString('?frigate-card-action.id.default=');
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
// View actions do not need the card to have been updated.
|
// View actions do not need the card to have been updated.
|
||||||
@@ -93,7 +93,7 @@ describe('QueryStringManager', () => {
|
|||||||
|
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeAll();
|
await manager.executeAll();
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
|
||||||
|
|
||||||
@@ -102,13 +102,13 @@ describe('QueryStringManager', () => {
|
|||||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute camera_select action', () => {
|
it('should execute camera_select action', async () => {
|
||||||
setQueryString('?frigate-card-action.id.camera_select=camera.office');
|
setQueryString('?frigate-card-action.id.camera_select=camera.office');
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeAll();
|
await manager.executeAll();
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||||
params: {
|
params: {
|
||||||
@@ -121,13 +121,13 @@ describe('QueryStringManager', () => {
|
|||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute live_substream_select action', () => {
|
it('should execute live_substream_select action', async () => {
|
||||||
setQueryString('?frigate-card-action.id.live_substream_select=camera.office_hd');
|
setQueryString('?frigate-card-action.id.live_substream_select=camera.office_hd');
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeAll();
|
await manager.executeAll();
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||||
modifiers: [expect.any(SubstreamSelectViewModifier)],
|
modifiers: [expect.any(SubstreamSelectViewModifier)],
|
||||||
@@ -142,13 +142,13 @@ describe('QueryStringManager', () => {
|
|||||||
describe('should ignore action without value', () => {
|
describe('should ignore action without value', () => {
|
||||||
it.each([['camera_select' as const], ['live_substream_select' as const]])(
|
it.each([['camera_select' as const], ['live_substream_select' as const]])(
|
||||||
'%s',
|
'%s',
|
||||||
(action: string) => {
|
async (action: string) => {
|
||||||
setQueryString(`?frigate-card-action.id.${action}=`);
|
setQueryString(`?frigate-card-action.id.${action}=`);
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeAll();
|
await manager.executeAll();
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeFalsy();
|
expect(manager.hasViewRelatedActions()).toBeFalsy();
|
||||||
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
||||||
@@ -158,7 +158,7 @@ describe('QueryStringManager', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle unknown action', () => {
|
it('should handle unknown action', async () => {
|
||||||
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
||||||
|
|
||||||
setQueryString('?frigate-card-action.id.not_an_action=value');
|
setQueryString('?frigate-card-action.id.not_an_action=value');
|
||||||
@@ -166,7 +166,7 @@ describe('QueryStringManager', () => {
|
|||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeAll();
|
await manager.executeAll();
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeFalsy();
|
expect(manager.hasViewRelatedActions()).toBeFalsy();
|
||||||
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
||||||
@@ -187,13 +187,13 @@ describe('QueryStringManager', () => {
|
|||||||
['snapshot' as const],
|
['snapshot' as const],
|
||||||
['snapshots' as const],
|
['snapshots' as const],
|
||||||
['timeline' as const],
|
['timeline' as const],
|
||||||
])('%s', (viewName: string) => {
|
])('%s', async (viewName: string) => {
|
||||||
setQueryString(`?frigate-card-action.id.${viewName}=`);
|
setQueryString(`?frigate-card-action.id.${viewName}=`);
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeAll();
|
await manager.executeAll();
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
||||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||||
@@ -210,13 +210,13 @@ describe('QueryStringManager', () => {
|
|||||||
['download' as const],
|
['download' as const],
|
||||||
['expand' as const],
|
['expand' as const],
|
||||||
['menu_toggle' as const],
|
['menu_toggle' as const],
|
||||||
])('%s', (action: string) => {
|
])('%s', async (action: string) => {
|
||||||
setQueryString(`?frigate-card-action.id.${action}=value`);
|
setQueryString(`?frigate-card-action.id.${action}=value`);
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
|
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeAll();
|
await manager.executeAll();
|
||||||
|
|
||||||
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||||
@@ -225,7 +225,7 @@ describe('QueryStringManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('should handle conflicting but valid actions', () => {
|
describe('should handle conflicting but valid actions', () => {
|
||||||
it('view and default with camera and substream specified', () => {
|
it('view and default with camera and substream specified', async () => {
|
||||||
setQueryString(
|
setQueryString(
|
||||||
'?frigate-card-action.id.clips=' +
|
'?frigate-card-action.id.clips=' +
|
||||||
'&frigate-card-action.id.live_substream_select=camera.kitchen_hd' +
|
'&frigate-card-action.id.live_substream_select=camera.kitchen_hd' +
|
||||||
@@ -236,7 +236,7 @@ describe('QueryStringManager', () => {
|
|||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeAll();
|
await manager.executeAll();
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalledWith({
|
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalledWith({
|
||||||
params: {
|
params: {
|
||||||
@@ -247,7 +247,7 @@ describe('QueryStringManager', () => {
|
|||||||
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('multiple cameras specified', () => {
|
it('multiple cameras specified', async () => {
|
||||||
setQueryString(
|
setQueryString(
|
||||||
'?frigate-card-action.id.camera_select=camera.kitchen' +
|
'?frigate-card-action.id.camera_select=camera.kitchen' +
|
||||||
'&frigate-card-action.id.camera_select=camera.office',
|
'&frigate-card-action.id.camera_select=camera.office',
|
||||||
@@ -256,7 +256,7 @@ describe('QueryStringManager', () => {
|
|||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeAll();
|
await manager.executeAll();
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
expect(api.getViewManager().setViewByParametersWithNewQuery).toBeCalledWith({
|
||||||
params: {
|
params: {
|
||||||
@@ -279,13 +279,13 @@ describe('QueryStringManager', () => {
|
|||||||
['snapshot' as const],
|
['snapshot' as const],
|
||||||
['snapshots' as const],
|
['snapshots' as const],
|
||||||
['timeline' as const],
|
['timeline' as const],
|
||||||
])('%s', (viewName: string) => {
|
])('%s', async (viewName: string) => {
|
||||||
setQueryString(`?frigate-card-action.id.${viewName}=`);
|
setQueryString(`?frigate-card-action.id.${viewName}=`);
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeNonViewRelated();
|
await manager.executeNonViewRelated();
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||||
@@ -298,13 +298,13 @@ describe('QueryStringManager', () => {
|
|||||||
['download' as const],
|
['download' as const],
|
||||||
['expand' as const],
|
['expand' as const],
|
||||||
['menu_toggle' as const],
|
['menu_toggle' as const],
|
||||||
])('%s', (viewName: string) => {
|
])('%s', async (viewName: string) => {
|
||||||
setQueryString(`?frigate-card-action.id.${viewName}=`);
|
setQueryString(`?frigate-card-action.id.${viewName}=`);
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
||||||
const manager = new QueryStringManager(api);
|
const manager = new QueryStringManager(api);
|
||||||
|
|
||||||
manager.executeViewRelated();
|
await manager.executeViewRelated();
|
||||||
|
|
||||||
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
expect(api.getActionsManager().executeActions).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { mock } from 'vitest-mock-extended';
|
|||||||
import { ViewFactory } from '../../../src/card-controller/view/factory';
|
import { ViewFactory } from '../../../src/card-controller/view/factory';
|
||||||
import { ViewManager } from '../../../src/card-controller/view/view-manager';
|
import { ViewManager } from '../../../src/card-controller/view/view-manager';
|
||||||
import { FrigateCardView } from '../../../src/config/types';
|
import { FrigateCardView } from '../../../src/config/types';
|
||||||
|
import { ViewMedia } from '../../../src/view/media';
|
||||||
|
import { MediaQueriesResults } from '../../../src/view/media-queries-results';
|
||||||
import {
|
import {
|
||||||
createCameraManager,
|
createCameraManager,
|
||||||
createCapabilities,
|
createCapabilities,
|
||||||
@@ -11,8 +13,6 @@ import {
|
|||||||
createStore,
|
createStore,
|
||||||
createView,
|
createView,
|
||||||
} from '../../test-utils';
|
} from '../../test-utils';
|
||||||
import { ViewMedia } from '../../../src/view/media';
|
|
||||||
import { MediaQueriesResults } from '../../../src/view/media-queries-results';
|
|
||||||
|
|
||||||
describe('should act correctly when view is set', () => {
|
describe('should act correctly when view is set', () => {
|
||||||
it('basic view', () => {
|
it('basic view', () => {
|
||||||
@@ -360,4 +360,33 @@ describe('hasMajorMediaChange', () => {
|
|||||||
manager.hasMajorMediaChange(createView({ queryResults: queryResults_2 })),
|
manager.hasMajorMediaChange(createView({ queryResults: queryResults_2 })),
|
||||||
).toBeFalsy();
|
).toBeFalsy();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('should initialize', () => {
|
||||||
|
it('without querystring', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const factory = mock<ViewFactory>();
|
||||||
|
const manager = new ViewManager(api, factory);
|
||||||
|
|
||||||
|
const view = createView({
|
||||||
|
view: 'live',
|
||||||
|
camera: 'camera',
|
||||||
|
});
|
||||||
|
factory.getViewDefaultWithNewQuery.mockResolvedValue(view);
|
||||||
|
|
||||||
|
expect(await manager.initialize()).toBeTruthy();
|
||||||
|
|
||||||
|
expect(manager.getView()).toBe(view);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with querystring', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const factory = mock<ViewFactory>();
|
||||||
|
const manager = new ViewManager(api, factory);
|
||||||
|
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(true);
|
||||||
|
|
||||||
|
expect(await manager.initialize()).toBeTruthy();
|
||||||
|
|
||||||
|
expect(api.getQueryStringManager().executeViewRelated).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { Initializer } from '../../src/utils/initializer/initializer';
|
||||||
|
|
||||||
|
describe('Initializer', () => {
|
||||||
|
it('should initialize with initializer', async () => {
|
||||||
|
const initializer = new Initializer();
|
||||||
|
|
||||||
|
expect(initializer.isInitialized('foo')).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
await initializer.initializeIfNecessary('foo', async () => true),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(initializer.isInitialized('foo')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize without initializer', async () => {
|
||||||
|
const initializer = new Initializer();
|
||||||
|
|
||||||
|
expect(initializer.isInitialized('foo')).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
await initializer.initializeIfNecessary('foo', async () => true),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(initializer.isInitialized('foo')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize when already initialized', async () => {
|
||||||
|
const initializer = new Initializer();
|
||||||
|
|
||||||
|
expect(initializer.isInitialized('foo')).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
await initializer.initializeIfNecessary('foo', async () => true),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(
|
||||||
|
await initializer.initializeIfNecessary('foo', async () => true),
|
||||||
|
).toBeTruthy();
|
||||||
|
expect(initializer.isInitialized('foo')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not initialize with failed initializer', async () => {
|
||||||
|
const initializer = new Initializer();
|
||||||
|
|
||||||
|
expect(initializer.isInitialized('foo')).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
await initializer.initializeIfNecessary('foo', async () => false),
|
||||||
|
).toBeFalsy();
|
||||||
|
expect(initializer.isInitialized('foo')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize multiple', async () => {
|
||||||
|
const initializer = new Initializer();
|
||||||
|
|
||||||
|
expect(initializer.isInitializedMultiple(['foo', 'bar'])).toBeFalsy();
|
||||||
|
expect(
|
||||||
|
await initializer.initializeMultipleIfNecessary({
|
||||||
|
foo: async () => true,
|
||||||
|
bar: async () => false,
|
||||||
|
}),
|
||||||
|
).toBeFalsy();
|
||||||
|
|
||||||
|
expect(initializer.isInitializedMultiple(['foo', 'bar'])).toBeFalsy();
|
||||||
|
|
||||||
|
expect(
|
||||||
|
await initializer.initializeMultipleIfNecessary({
|
||||||
|
bar: async () => true,
|
||||||
|
}),
|
||||||
|
).toBeTruthy();
|
||||||
|
|
||||||
|
expect(initializer.isInitializedMultiple(['foo', 'bar'])).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should uninitialize', async () => {
|
||||||
|
const initializer = new Initializer();
|
||||||
|
await initializer.initializeIfNecessary('foo');
|
||||||
|
expect(initializer.isInitialized('foo')).toBeTruthy();
|
||||||
|
|
||||||
|
initializer.uninitialize('foo');
|
||||||
|
|
||||||
|
expect(initializer.isInitialized('foo')).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -27,6 +27,7 @@ const FULL_COVERAGE_FILES_RELATIVE = [
|
|||||||
'utils/endpoint.ts',
|
'utils/endpoint.ts',
|
||||||
'utils/ha/entity-registry/types.ts',
|
'utils/ha/entity-registry/types.ts',
|
||||||
'utils/ha/types.ts',
|
'utils/ha/types.ts',
|
||||||
|
'utils/initializer.ts',
|
||||||
'utils/interaction-mode.ts',
|
'utils/interaction-mode.ts',
|
||||||
'utils/media-info.ts',
|
'utils/media-info.ts',
|
||||||
'utils/media-layout.ts',
|
'utils/media-layout.ts',
|
||||||
|
|||||||
Reference in New Issue
Block a user