Add mixxing part of prior commit.
This commit is contained in:
@@ -8,7 +8,7 @@ import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
|||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import { allPromises, arrayify, setify } from '../utils/basic.js';
|
import { allPromises, arrayify, setify } from '../utils/basic.js';
|
||||||
import { getCameraID } from '../utils/camera.js';
|
import { getCameraID } from '../utils/camera.js';
|
||||||
import { CardCameraAPI } from '../utils/card-controller/types.js';
|
import { CardCameraAPI } from '../card-controller/types.js';
|
||||||
import { log } from '../utils/debug.js';
|
import { log } from '../utils/debug.js';
|
||||||
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
||||||
import { ViewMedia } from '../view/media.js';
|
import { ViewMedia } from '../view/media.js';
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class ConfigManager {
|
|||||||
protected _rawConfig: RawFrigateCardConfig | null = null;
|
protected _rawConfig: RawFrigateCardConfig | null = null;
|
||||||
protected _cardWideConfig: CardWideConfig | null = null;
|
protected _cardWideConfig: CardWideConfig | null = null;
|
||||||
|
|
||||||
constructor(api) {
|
constructor(api: CardConfigAPI) {
|
||||||
this._api = api;
|
this._api = api;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -27,8 +27,8 @@ import {
|
|||||||
Message,
|
Message,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { frigateCardHasAction } from './utils/action.js';
|
import { frigateCardHasAction } from './utils/action.js';
|
||||||
import { ConditionEvaluateRequestEvent } from './utils/card-controller/conditions-manager.js';
|
import { ConditionEvaluateRequestEvent } from './card-controller/conditions-manager.js';
|
||||||
import { CardController } from './utils/card-controller/controller';
|
import { CardController } from './card-controller/controller';
|
||||||
import { MenuButtonController } from './utils/menu-controller';
|
import { MenuButtonController } from './utils/menu-controller';
|
||||||
import { View } from './view/view.js';
|
import { View } from './view/view.js';
|
||||||
|
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
|||||||
import {
|
import {
|
||||||
ConditionsManagerEpoch,
|
ConditionsManagerEpoch,
|
||||||
evaluateConditionViaEvent,
|
evaluateConditionViaEvent,
|
||||||
} from '../utils/card-controller/conditions-manager.js';
|
} from '../card-controller/conditions-manager.js';
|
||||||
import { dispatchFrigateCardErrorEvent } from './message.js';
|
import { dispatchFrigateCardErrorEvent } from './message.js';
|
||||||
|
|
||||||
/* A note on picture element rendering:
|
/* A note on picture element rendering:
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ import { contentsChanged } from '../../utils/basic.js';
|
|||||||
import {
|
import {
|
||||||
ConditionsManagerEpoch,
|
ConditionsManagerEpoch,
|
||||||
getOverriddenConfig,
|
getOverriddenConfig,
|
||||||
} from '../../utils/card-controller/conditions-manager.js';
|
} from '../../card-controller/conditions-manager.js';
|
||||||
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
|
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
|
||||||
import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy-load.js';
|
import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy-load.js';
|
||||||
import { AutoMediaActions } from '../../utils/embla/plugins/auto-media-actions/auto-media-actions.js';
|
import { AutoMediaActions } from '../../utils/embla/plugins/auto-media-actions/auto-media-actions.js';
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import {
|
|||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { CameraManager } from '../camera-manager/manager.js';
|
import { CameraManager } from '../camera-manager/manager.js';
|
||||||
import { ConditionsManagerEpoch, getOverridesByKey } from '../utils/card-controller/conditions-manager';
|
import { ConditionsManagerEpoch, getOverridesByKey } from '../card-controller/conditions-manager.js';
|
||||||
import viewsStyle from '../scss/views.scss';
|
import viewsStyle from '../scss/views.scss';
|
||||||
import { ExtendedHomeAssistant } from '../types.js';
|
import { ExtendedHomeAssistant } from '../types.js';
|
||||||
import { ConfigManager } from '../utils/card-controller/config-manager.js';
|
import { ConfigManager } from '../card-controller/config-manager.js';
|
||||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media.js';
|
import { ResolvedMediaCache } from '../utils/ha/resolved-media.js';
|
||||||
import { View } from '../view/view.js';
|
import { View } from '../view/view.js';
|
||||||
import './surround.js';
|
import './surround.js';
|
||||||
|
|||||||
@@ -1,223 +0,0 @@
|
|||||||
import {
|
|
||||||
Actions,
|
|
||||||
ActionsConfig,
|
|
||||||
FrigateCardCustomAction,
|
|
||||||
FRIGATE_CARD_VIEW_DEFAULT,
|
|
||||||
} from '../../config/types.js';
|
|
||||||
import {
|
|
||||||
convertActionToFrigateCardCustomAction,
|
|
||||||
frigateCardHandleActionConfig,
|
|
||||||
getActionConfigGivenAction,
|
|
||||||
} from '../action.js';
|
|
||||||
import { getStreamCameraID } from '../substream.js';
|
|
||||||
import { CardActionsManagerAPI } from './types.js';
|
|
||||||
|
|
||||||
export class ActionsManager {
|
|
||||||
protected _api: CardActionsManagerAPI;
|
|
||||||
|
|
||||||
constructor(api: CardActionsManagerAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Merge card-wide and view-specific actions.
|
|
||||||
* @returns A combined set of action.
|
|
||||||
*/
|
|
||||||
public getMergedActions(): ActionsConfig {
|
|
||||||
const view = this._api.getViewManager().getView();
|
|
||||||
if (this._api.getMessageManager().hasMessage()) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
|
||||||
let specificActions: Actions | undefined = undefined;
|
|
||||||
if (view?.is('live')) {
|
|
||||||
specificActions = config?.live.actions;
|
|
||||||
} else if (view?.isGalleryView()) {
|
|
||||||
specificActions = config?.media_gallery?.actions;
|
|
||||||
} else if (view?.isViewerView()) {
|
|
||||||
specificActions = config?.media_viewer.actions;
|
|
||||||
} else if (view?.is('image')) {
|
|
||||||
specificActions = config?.image?.actions;
|
|
||||||
} else {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return { ...config?.view.actions, ...specificActions };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle an human interaction called on an element (e.g. 'tap').
|
|
||||||
*/
|
|
||||||
public handleInteraction(interaction: string): void {
|
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
|
||||||
const config = this.getMergedActions();
|
|
||||||
const actionConfig = getActionConfigGivenAction(interaction, config);
|
|
||||||
if (
|
|
||||||
hass &&
|
|
||||||
config &&
|
|
||||||
interaction &&
|
|
||||||
// Don't call frigateCardHandleActionConfig() unless there is explicitly an
|
|
||||||
// action defined (as it uses a default that is unhelpful for views that
|
|
||||||
// have default tap/click actions).
|
|
||||||
actionConfig
|
|
||||||
) {
|
|
||||||
frigateCardHandleActionConfig(
|
|
||||||
this._api.getCardElementManager().getElement(),
|
|
||||||
hass,
|
|
||||||
config,
|
|
||||||
interaction,
|
|
||||||
actionConfig,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public handleActionEvent = (ev: Event): void => {
|
|
||||||
if (!('detail' in ev)) {
|
|
||||||
// The event may not actually be a CustomEvent object, but may still have a
|
|
||||||
// detail field. See:
|
|
||||||
// https://github.com/custom-cards/custom-card-helpers/blob/master/src/fire-event.ts#L70
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail);
|
|
||||||
if (frigateCardAction) {
|
|
||||||
this.executeAction(frigateCardAction);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Execute a card action.
|
|
||||||
* @param frigateCardAction
|
|
||||||
* @returns `true` if an action is executed.
|
|
||||||
*/
|
|
||||||
public async executeAction(frigateCardAction: FrigateCardCustomAction): Promise<void> {
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
|
||||||
const mediaLoadedInfoManager = this._api.getMediaLoadedInfoManager();
|
|
||||||
|
|
||||||
if (
|
|
||||||
// Command not intended for this card (e.g. query string command).
|
|
||||||
frigateCardAction.card_id &&
|
|
||||||
config?.card_id !== frigateCardAction.card_id
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: This function needs to process (view-related) commands even when
|
|
||||||
// _view has not yet been initialized (since it may be used to set a view
|
|
||||||
// via the querystring).
|
|
||||||
const view = this._api.getViewManager().getView();
|
|
||||||
|
|
||||||
const action = frigateCardAction.frigate_card_action;
|
|
||||||
|
|
||||||
switch (action) {
|
|
||||||
case 'default':
|
|
||||||
this._api.getViewManager().setViewDefault();
|
|
||||||
break;
|
|
||||||
case 'clip':
|
|
||||||
case 'clips':
|
|
||||||
case 'image':
|
|
||||||
case 'live':
|
|
||||||
case 'recording':
|
|
||||||
case 'recordings':
|
|
||||||
case 'snapshot':
|
|
||||||
case 'snapshots':
|
|
||||||
case 'timeline':
|
|
||||||
this._api.getViewManager().setViewByParameters({
|
|
||||||
viewName: action,
|
|
||||||
cameraID: view?.camera,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case 'download':
|
|
||||||
await this._api.getDownloadManager().downloadViewerMedia();
|
|
||||||
break;
|
|
||||||
case 'camera_ui':
|
|
||||||
this._api.getCameraURLManager().openURL();
|
|
||||||
break;
|
|
||||||
case 'expand':
|
|
||||||
this._api.getExpandManager().toggleExpanded();
|
|
||||||
break;
|
|
||||||
case 'fullscreen':
|
|
||||||
this._api.getFullscreenManager().toggleFullscreen();
|
|
||||||
break;
|
|
||||||
case 'menu_toggle':
|
|
||||||
// This is a rare code path: this would only be used if someone has a
|
|
||||||
// menu toggle action configured outside of the menu itself (e.g.
|
|
||||||
// picture elements).
|
|
||||||
this._api.getCardElementManager().toggleMenu();
|
|
||||||
break;
|
|
||||||
case 'camera_select':
|
|
||||||
const selectCameraID = frigateCardAction.camera;
|
|
||||||
if (view) {
|
|
||||||
const viewOnCameraSelect = config?.view.camera_select ?? 'current';
|
|
||||||
const targetViewName =
|
|
||||||
viewOnCameraSelect === 'current' ? view.view : viewOnCameraSelect;
|
|
||||||
const verifiedViewName = this._api
|
|
||||||
.getViewManager()
|
|
||||||
.isViewSupportedByCamera(selectCameraID, targetViewName)
|
|
||||||
? targetViewName
|
|
||||||
: FRIGATE_CARD_VIEW_DEFAULT;
|
|
||||||
this._api.getViewManager().setViewByParameters({
|
|
||||||
viewName: verifiedViewName,
|
|
||||||
cameraID: selectCameraID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'live_substream_select': {
|
|
||||||
this._api.getViewManager().setViewWithSubstream(frigateCardAction.camera);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'live_substream_off': {
|
|
||||||
this._api.getViewManager().setViewWithoutSubstream();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'live_substream_on': {
|
|
||||||
this._api.getViewManager().setViewWithSubstream();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case 'media_player':
|
|
||||||
const mediaPlayer = frigateCardAction.media_player;
|
|
||||||
const mediaPlayerController = this._api.getMediaPlayerManager();
|
|
||||||
const media = view?.queryResults?.getSelectedResult() ?? null;
|
|
||||||
|
|
||||||
if (frigateCardAction.media_player_action === 'stop') {
|
|
||||||
await mediaPlayerController.stop(mediaPlayer);
|
|
||||||
} else if (view?.is('live')) {
|
|
||||||
await mediaPlayerController.playLive(mediaPlayer, getStreamCameraID(view));
|
|
||||||
} else if (view?.isViewerView() && media) {
|
|
||||||
await mediaPlayerController.playMedia(mediaPlayer, media);
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'diagnostics':
|
|
||||||
this._api.getViewManager().setViewByParameters({ viewName: 'diagnostics' });
|
|
||||||
break;
|
|
||||||
case 'microphone_mute':
|
|
||||||
this._api.getMicrophoneManager().mute();
|
|
||||||
break;
|
|
||||||
case 'microphone_unmute':
|
|
||||||
await this._api.getMicrophoneManager().unmute();
|
|
||||||
break;
|
|
||||||
case 'mute':
|
|
||||||
await mediaLoadedInfoManager.get()?.player?.mute();
|
|
||||||
break;
|
|
||||||
case 'unmute':
|
|
||||||
await mediaLoadedInfoManager.get()?.player?.unmute();
|
|
||||||
break;
|
|
||||||
case 'play':
|
|
||||||
await mediaLoadedInfoManager.get()?.player?.play();
|
|
||||||
break;
|
|
||||||
case 'pause':
|
|
||||||
await mediaLoadedInfoManager.get()?.player?.pause();
|
|
||||||
break;
|
|
||||||
case 'screenshot':
|
|
||||||
await this._api.getDownloadManager().downloadScreenshot();
|
|
||||||
break;
|
|
||||||
case 'display_mode_select':
|
|
||||||
this._api
|
|
||||||
.getViewManager()
|
|
||||||
.setViewWithNewDisplayMode(frigateCardAction.display_mode);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn(`Frigate card received unknown card action: ${action}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import { Timer } from '../timer';
|
|
||||||
import { CardAutoRefreshAPI } from './types';
|
|
||||||
|
|
||||||
export class AutoUpdateManager {
|
|
||||||
protected _timer = new Timer();
|
|
||||||
protected _api: CardAutoRefreshAPI;
|
|
||||||
|
|
||||||
constructor(api: CardAutoRefreshAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the update timer to trigger an update refresh every
|
|
||||||
* `view.update_seconds`.
|
|
||||||
*/
|
|
||||||
public startDefaultViewTimer(): void {
|
|
||||||
this._timer.stop();
|
|
||||||
const updateSeconds = this._api.getConfigManager().getConfig()
|
|
||||||
?.view.update_seconds;
|
|
||||||
if (updateSeconds) {
|
|
||||||
this._timer.start(updateSeconds, () => {
|
|
||||||
if (this._isAutomatedUpdateAllowed()) {
|
|
||||||
this._api.getViewManager().setViewDefault();
|
|
||||||
} else {
|
|
||||||
// Not allowed to update this time around, but try again at the next
|
|
||||||
// interval.
|
|
||||||
this.startDefaultViewTimer();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _isAutomatedUpdateAllowed(): boolean {
|
|
||||||
const triggers = this._api.getTriggersManager();
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
|
||||||
const interactionManager = this._api.getInteractionManager();
|
|
||||||
|
|
||||||
return (
|
|
||||||
!triggers.isTriggered() &&
|
|
||||||
(config?.view.update_force || !interactionManager.hasInteraction())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { Automation, AutomationActions, Automations } from '../../config/types.js';
|
|
||||||
import { localize } from '../../localize/localize.js';
|
|
||||||
import { frigateCardHandleAction } from '../action.js';
|
|
||||||
import { CardAutomationsAPI } from './types.js';
|
|
||||||
|
|
||||||
const MAX_NESTED_AUTOMATION_EXECUTIONS = 10;
|
|
||||||
|
|
||||||
export class AutomationsManager {
|
|
||||||
protected _api: CardAutomationsAPI;
|
|
||||||
|
|
||||||
protected _automations: Automations;
|
|
||||||
protected _priorEvaluations: Map<Automation, boolean> = new Map();
|
|
||||||
|
|
||||||
// A counter to avoid infinite loops, increases every time actions are run,
|
|
||||||
// decreases every time actions are complete.
|
|
||||||
protected _nestedAutomationExecutions = 0;
|
|
||||||
|
|
||||||
constructor(api: CardAutomationsAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setAutomationsFromConfig() {
|
|
||||||
this._automations = this._api
|
|
||||||
.getConfigManager()
|
|
||||||
.getNonOverriddenConfig()?.automations;
|
|
||||||
}
|
|
||||||
|
|
||||||
public execute(): void {
|
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
|
||||||
|
|
||||||
// Never execute automations if there's an error (as our automation loop
|
|
||||||
// avoidance -- which shows as an error -- would not work!).
|
|
||||||
if (!hass || this._api.getMessageManager().hasErrorMessage()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const actionsToRun: AutomationActions[] = [];
|
|
||||||
for (const automation of this._automations ?? []) {
|
|
||||||
const shouldExecute = this._api
|
|
||||||
.getConditionsManager()
|
|
||||||
.evaluateCondition(automation.conditions);
|
|
||||||
const actions = shouldExecute ? automation.actions : automation.actions_not;
|
|
||||||
const priorEvaluation = this._priorEvaluations.get(automation);
|
|
||||||
this._priorEvaluations.set(automation, shouldExecute);
|
|
||||||
if (shouldExecute !== priorEvaluation && actions) {
|
|
||||||
actionsToRun.push(actions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
++this._nestedAutomationExecutions;
|
|
||||||
if (this._nestedAutomationExecutions > MAX_NESTED_AUTOMATION_EXECUTIONS) {
|
|
||||||
this._api.getMessageManager().setMessageIfHigherPriority({
|
|
||||||
type: 'error',
|
|
||||||
message: localize('error.too_many_automations'),
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
actionsToRun.forEach((actions) => {
|
|
||||||
frigateCardHandleAction(
|
|
||||||
this._api.getCardElementManager().getElement(),
|
|
||||||
hass,
|
|
||||||
{},
|
|
||||||
actions,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
--this._nestedAutomationExecutions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
import { CardCameraURLAPI } from './types';
|
|
||||||
|
|
||||||
export class CameraURLManager {
|
|
||||||
protected _api: CardCameraURLAPI;
|
|
||||||
|
|
||||||
constructor(api: CardCameraURLAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public openURL(): void {
|
|
||||||
const url = this.getCameraURL();
|
|
||||||
if (url) {
|
|
||||||
window.open(url);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasCameraURL(): boolean {
|
|
||||||
return !!this.getCameraURL();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCameraURL(): string | null {
|
|
||||||
const view = this._api.getViewManager().getView();
|
|
||||||
const media = view?.queryResults?.getSelectedResult() ?? null;
|
|
||||||
const endpoints = view?.camera
|
|
||||||
? this._api.getCameraManager().getCameraEndpoints(view.camera, {
|
|
||||||
view: view.view,
|
|
||||||
...(media && { media: media }),
|
|
||||||
}) ?? null
|
|
||||||
: null;
|
|
||||||
return endpoints?.ui?.endpoint ?? null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,117 +0,0 @@
|
|||||||
import { LitElement, ReactiveControllerHost } from 'lit';
|
|
||||||
import { ActionEventTarget } from '../../action-handler-directive';
|
|
||||||
import { setOrRemoveAttribute } from '../basic';
|
|
||||||
import { isCardInPanel } from '../ha';
|
|
||||||
import { CardElementAPI } from './types';
|
|
||||||
|
|
||||||
export type ScrollCallback = () => void;
|
|
||||||
export type MenuToggleCallback = () => void;
|
|
||||||
|
|
||||||
export type CardHTMLElement = LitElement & ReactiveControllerHost & ActionEventTarget;
|
|
||||||
|
|
||||||
export class CardElementManager {
|
|
||||||
protected _api: CardElementAPI;
|
|
||||||
|
|
||||||
protected _element: CardHTMLElement;
|
|
||||||
protected _scrollCallback: ScrollCallback;
|
|
||||||
protected _menuToggleCallback: MenuToggleCallback;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
api: CardElementAPI,
|
|
||||||
element: CardHTMLElement,
|
|
||||||
scrollCallback: ScrollCallback,
|
|
||||||
menuToggleCallback: MenuToggleCallback,
|
|
||||||
) {
|
|
||||||
this._api = api;
|
|
||||||
|
|
||||||
this._element = element;
|
|
||||||
this._scrollCallback = scrollCallback;
|
|
||||||
this._menuToggleCallback = menuToggleCallback;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getElement(): HTMLElement {
|
|
||||||
return this._element;
|
|
||||||
}
|
|
||||||
|
|
||||||
public scrollReset(): void {
|
|
||||||
this._scrollCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
public toggleMenu(): void {
|
|
||||||
this._menuToggleCallback();
|
|
||||||
}
|
|
||||||
|
|
||||||
public update(): void {
|
|
||||||
this._element.requestUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasUpdated(): boolean {
|
|
||||||
return this._element.hasUpdated;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCardHeight(): number {
|
|
||||||
return this._element.getBoundingClientRect().height;
|
|
||||||
}
|
|
||||||
|
|
||||||
public elementConnected(): void {
|
|
||||||
// Whether or not the card is in panel mode on the dashboard.
|
|
||||||
setOrRemoveAttribute(this._element, isCardInPanel(this._element), 'panel');
|
|
||||||
|
|
||||||
this._api.getFullscreenManager().connect();
|
|
||||||
|
|
||||||
this._element.addEventListener(
|
|
||||||
'mousemove',
|
|
||||||
this._api.getInteractionManager().reportInteraction,
|
|
||||||
);
|
|
||||||
this._element.addEventListener(
|
|
||||||
'll-custom',
|
|
||||||
this._api.getActionsManager().handleActionEvent,
|
|
||||||
);
|
|
||||||
this._element.addEventListener(
|
|
||||||
'@action',
|
|
||||||
this._api.getInteractionManager().reportInteraction,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Listen for HA `navigate` actions.
|
|
||||||
// See: https://github.com/home-assistant/frontend/blob/273992c8e9c3062c6e49481b6d7d688a07067232/src/common/navigate.ts#L43
|
|
||||||
window.addEventListener(
|
|
||||||
'location-changed',
|
|
||||||
this._api.getQueryStringManager().executeAll,
|
|
||||||
);
|
|
||||||
|
|
||||||
// Listen for history state changes (i.e. user using the browser
|
|
||||||
// back/forward controls).
|
|
||||||
window.addEventListener('popstate', this._api.getQueryStringManager().executeAll);
|
|
||||||
|
|
||||||
// Manually call the location change handler as the card will be
|
|
||||||
// disconnected/reconnected when dashboard 'tab' changes happen within HA.
|
|
||||||
this._api.getQueryStringManager().executeAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
public elementDisconnected(): void {
|
|
||||||
setOrRemoveAttribute(this._element, false, 'panel');
|
|
||||||
|
|
||||||
// When the dashboard 'tab' is changed, the media is effectively unloaded.
|
|
||||||
this._api.getMediaLoadedInfoManager().clear();
|
|
||||||
this._api.getFullscreenManager().disconnect();
|
|
||||||
|
|
||||||
this._element.removeEventListener(
|
|
||||||
'mousemove',
|
|
||||||
this._api.getInteractionManager().reportInteraction,
|
|
||||||
);
|
|
||||||
this._element.removeEventListener(
|
|
||||||
'll-custom',
|
|
||||||
this._api.getActionsManager().handleActionEvent,
|
|
||||||
);
|
|
||||||
this._element.removeEventListener(
|
|
||||||
'@action',
|
|
||||||
this._api.getInteractionManager().reportInteraction,
|
|
||||||
);
|
|
||||||
|
|
||||||
window.removeEventListener(
|
|
||||||
'location-changed',
|
|
||||||
this._api.getQueryStringManager().executeAll,
|
|
||||||
);
|
|
||||||
window.removeEventListener('popstate', this._api.getQueryStringManager().executeAll);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,257 +0,0 @@
|
|||||||
import { HassEntities } from 'home-assistant-js-websocket';
|
|
||||||
import merge from 'lodash-es/merge';
|
|
||||||
import { copyConfig } from '../../config-mgmt';
|
|
||||||
import {
|
|
||||||
FrigateCardCondition,
|
|
||||||
frigateConditionalSchema,
|
|
||||||
OverrideConfigurationKey,
|
|
||||||
RawFrigateCardConfig,
|
|
||||||
ViewDisplayMode
|
|
||||||
} from '../../config/types';
|
|
||||||
import { CardConditionAPI } from './types';
|
|
||||||
|
|
||||||
interface ConditionState {
|
|
||||||
view?: string;
|
|
||||||
fullscreen?: boolean;
|
|
||||||
expand?: boolean;
|
|
||||||
camera?: string;
|
|
||||||
state?: HassEntities;
|
|
||||||
media_loaded?: boolean;
|
|
||||||
displayMode?: ViewDisplayMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ConditionEvaluateRequestEvent extends Event {
|
|
||||||
public condition: FrigateCardCondition;
|
|
||||||
public evaluation?: boolean;
|
|
||||||
|
|
||||||
constructor(condition: FrigateCardCondition, eventInitDict?: EventInit) {
|
|
||||||
super('frigate-card:condition:evaluate', eventInitDict);
|
|
||||||
this.condition = condition;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Evaluate whether a frigateCardCondition is met using an event to evaluate.
|
|
||||||
* @returns A boolean indicating whether the condition is met.
|
|
||||||
*/
|
|
||||||
export function evaluateConditionViaEvent(
|
|
||||||
element: HTMLElement,
|
|
||||||
condition?: FrigateCardCondition,
|
|
||||||
): boolean {
|
|
||||||
if (!condition) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const evaluateEvent = new ConditionEvaluateRequestEvent(condition, {
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Special note on what's going on here:
|
|
||||||
*
|
|
||||||
* Some parts of the card (e.g. <frigate-card-elements>) may have arbitrary
|
|
||||||
* complexity and layers (that this card doesn't control) between that master
|
|
||||||
* element and the element that needs to evaluate the condition. In these
|
|
||||||
* cases there's no clean way to pass state from the rest of card down through
|
|
||||||
* these layers. Instead, an event is dispatched as a "request for evaluation"
|
|
||||||
* (ConditionEvaluateRequestEvent) upwards which is caught by the outer card
|
|
||||||
* and the evaluation result is added to the event object. Because event
|
|
||||||
* propagation is handled synchronously, the result will be added to the event
|
|
||||||
* before the flow proceeds.
|
|
||||||
*/
|
|
||||||
element.dispatchEvent(evaluateEvent);
|
|
||||||
return evaluateEvent.evaluation ?? false;
|
|
||||||
}
|
|
||||||
|
|
||||||
type RawOverrides = {
|
|
||||||
conditions: FrigateCardCondition;
|
|
||||||
overrides: RawFrigateCardConfig;
|
|
||||||
}[];
|
|
||||||
|
|
||||||
export function getOverriddenConfig(
|
|
||||||
manager: Readonly<ConditionsManager>,
|
|
||||||
config: Readonly<RawFrigateCardConfig>,
|
|
||||||
configOverrides?: Readonly<RawOverrides>,
|
|
||||||
stateOverrides?: Partial<ConditionState>,
|
|
||||||
): RawFrigateCardConfig {
|
|
||||||
const output = copyConfig(config);
|
|
||||||
let overridden = false;
|
|
||||||
if (configOverrides) {
|
|
||||||
for (const override of configOverrides) {
|
|
||||||
if (manager.evaluateCondition(override.conditions, stateOverrides)) {
|
|
||||||
merge(output, override.overrides);
|
|
||||||
overridden = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Attempt to return the same configuration object if it has not been
|
|
||||||
// overridden (to reduce re-renders for a configuration that has not changed).
|
|
||||||
return overridden ? output : config;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getOverridesByKey(
|
|
||||||
key: OverrideConfigurationKey,
|
|
||||||
overrides?: Readonly<RawOverrides>,
|
|
||||||
): RawOverrides {
|
|
||||||
return (
|
|
||||||
overrides
|
|
||||||
?.filter((o) => key in o.overrides)
|
|
||||||
.map((o) => ({
|
|
||||||
conditions: o.conditions,
|
|
||||||
overrides: o.overrides[key] as RawFrigateCardConfig,
|
|
||||||
})) ?? []
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A tiny wrapper interface to allow the same manager to be passed around
|
|
||||||
// immutably within objects that will not be equal (===). Every state change
|
|
||||||
// generates a new epoch. This is used for Lit rendering to ensure changes to
|
|
||||||
// condition state are recognized as changes even though the manager is the
|
|
||||||
// same.
|
|
||||||
export interface ConditionsManagerEpoch {
|
|
||||||
manager: Readonly<ConditionsManager>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ConditionsManagerListener = () => void;
|
|
||||||
|
|
||||||
export class ConditionsManager {
|
|
||||||
protected _api: CardConditionAPI;
|
|
||||||
|
|
||||||
protected _state: ConditionState = {};
|
|
||||||
protected _epoch: ConditionsManagerEpoch = this._createEpoch();
|
|
||||||
protected _listeners: ConditionsManagerListener[];
|
|
||||||
|
|
||||||
// Whether or not to include HA state in ConditionState. Doing so increases
|
|
||||||
// CPU usage as HA state is pumped out very fast, so this is only enabled if
|
|
||||||
// the configuration needs to consume it.
|
|
||||||
protected _hasHAStateConditions = false;
|
|
||||||
protected _mediaQueries: MediaQueryList[] = [];
|
|
||||||
protected _mediaQueryTrigger = () => this._triggerChange();
|
|
||||||
|
|
||||||
constructor(api: CardConditionAPI, listener?: ConditionsManagerListener) {
|
|
||||||
this._api = api;
|
|
||||||
this._listeners = [
|
|
||||||
() => this._api.getConfigManager().computeOverrideConfig(),
|
|
||||||
() => this._api.getAutomationsManager().execute(),
|
|
||||||
...(listener ? [listener] : [])
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeConditions(): void {
|
|
||||||
this._mediaQueries.forEach((mql) =>
|
|
||||||
mql.removeEventListener('change', this._mediaQueryTrigger),
|
|
||||||
);
|
|
||||||
this._mediaQueries = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
public setConditionsFromConfig(): void {
|
|
||||||
this.removeConditions();
|
|
||||||
|
|
||||||
const getAllConditions = (): FrigateCardCondition[] => {
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
|
||||||
const conditions: FrigateCardCondition[] = [];
|
|
||||||
config?.overrides?.forEach((override) => conditions.push(override.conditions));
|
|
||||||
|
|
||||||
// Element conditions can be arbitrarily nested underneath conditionals and
|
|
||||||
// custom elements that this card may not known. Here we recursively parse
|
|
||||||
// down the elements tree, parsing as we go to find valid conditions.
|
|
||||||
const getElementsConditions = (data: unknown): void => {
|
|
||||||
const parseResult = frigateConditionalSchema.safeParse(data);
|
|
||||||
if (parseResult.success) {
|
|
||||||
conditions.push(parseResult.data.conditions);
|
|
||||||
parseResult.data.elements?.forEach(getElementsConditions);
|
|
||||||
} else if (data && typeof data === 'object') {
|
|
||||||
Object.keys(data).forEach((key) => getElementsConditions(data[key]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
config?.elements?.forEach(getElementsConditions);
|
|
||||||
return conditions;
|
|
||||||
};
|
|
||||||
|
|
||||||
const conditions = getAllConditions();
|
|
||||||
this._hasHAStateConditions = conditions.some(
|
|
||||||
(condition) => !!condition.state?.length,
|
|
||||||
);
|
|
||||||
conditions.forEach((condition) => {
|
|
||||||
if (condition.media_query) {
|
|
||||||
const mql = window.matchMedia(condition.media_query);
|
|
||||||
mql.addEventListener('change', this._mediaQueryTrigger);
|
|
||||||
this._mediaQueries.push(mql);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public setState(state: Partial<ConditionState>): void {
|
|
||||||
this._state = {
|
|
||||||
...this._state,
|
|
||||||
...state,
|
|
||||||
};
|
|
||||||
this._triggerChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasHAStateConditions(): boolean {
|
|
||||||
return this._hasHAStateConditions;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getEpoch(): ConditionsManagerEpoch {
|
|
||||||
return this._epoch;
|
|
||||||
}
|
|
||||||
|
|
||||||
public evaluateCondition(
|
|
||||||
condition: Readonly<FrigateCardCondition>,
|
|
||||||
stateOverrides?: Partial<ConditionState>,
|
|
||||||
): boolean {
|
|
||||||
const state = {
|
|
||||||
...this._state,
|
|
||||||
...stateOverrides,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result = true;
|
|
||||||
if (condition.view?.length) {
|
|
||||||
result &&= !!state?.view && condition.view.includes(state.view);
|
|
||||||
}
|
|
||||||
if (condition.fullscreen !== undefined) {
|
|
||||||
result &&=
|
|
||||||
state.fullscreen !== undefined && condition.fullscreen === state.fullscreen;
|
|
||||||
}
|
|
||||||
if (condition.expand !== undefined) {
|
|
||||||
result &&= state.expand !== undefined && condition.expand === state.expand;
|
|
||||||
}
|
|
||||||
if (condition.camera?.length) {
|
|
||||||
result &&= !!state.camera && condition.camera.includes(state.camera);
|
|
||||||
}
|
|
||||||
if (condition.state?.length) {
|
|
||||||
for (const stateTest of condition.state) {
|
|
||||||
result &&=
|
|
||||||
!!state.state &&
|
|
||||||
((!stateTest.state && !stateTest.state_not) ||
|
|
||||||
(stateTest.entity in state.state &&
|
|
||||||
(!stateTest.state ||
|
|
||||||
state.state[stateTest.entity].state === stateTest.state) &&
|
|
||||||
(!stateTest.state_not ||
|
|
||||||
state.state[stateTest.entity].state !== stateTest.state_not)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (condition.media_loaded !== undefined) {
|
|
||||||
result &&=
|
|
||||||
state.media_loaded !== undefined &&
|
|
||||||
condition.media_loaded === state.media_loaded;
|
|
||||||
}
|
|
||||||
if (condition.media_query) {
|
|
||||||
result &&= window.matchMedia(condition.media_query).matches;
|
|
||||||
}
|
|
||||||
if (condition.display_mode) {
|
|
||||||
result &&= !!state.displayMode && condition.display_mode === state.displayMode;
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _createEpoch(): ConditionsManagerEpoch {
|
|
||||||
return { manager: this };
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _triggerChange(): void {
|
|
||||||
this._epoch = this._createEpoch();
|
|
||||||
this._listeners.forEach((listener) => listener());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
import isEqual from 'lodash-es/isEqual';
|
|
||||||
import { isConfigUpgradeable } from '../../config-mgmt';
|
|
||||||
import {
|
|
||||||
CardWideConfig,
|
|
||||||
FrigateCardConfig,
|
|
||||||
frigateCardConfigSchema,
|
|
||||||
RawFrigateCardConfig
|
|
||||||
} from '../../config/types';
|
|
||||||
import { localize } from '../../localize/localize';
|
|
||||||
import { setLowPerformanceProfile } from '../../performance.js';
|
|
||||||
import { getParseErrorPaths } from '../zod.js';
|
|
||||||
import { getOverriddenConfig } from './conditions-manager';
|
|
||||||
import { InitializationAspect } from './initialization-manager';
|
|
||||||
import { CardConfigAPI } from './types';
|
|
||||||
|
|
||||||
export class ConfigManager {
|
|
||||||
protected _api: CardConfigAPI;
|
|
||||||
|
|
||||||
// The main base configuration object. For most usecases use getConfig() to
|
|
||||||
// get the correct configuration (which will return overrides as appropriate).
|
|
||||||
// This variable must be called `_config` or `config` to be compatible with
|
|
||||||
// card-mod.
|
|
||||||
protected _config: FrigateCardConfig | null = null;
|
|
||||||
protected _overriddenConfig: FrigateCardConfig | null = null;
|
|
||||||
protected _rawConfig: RawFrigateCardConfig | null = null;
|
|
||||||
protected _cardWideConfig: CardWideConfig | null = null;
|
|
||||||
|
|
||||||
constructor(api) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasConfig(): boolean {
|
|
||||||
return !!this.getConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getConfig(): FrigateCardConfig | null {
|
|
||||||
return this._overriddenConfig ?? this._config;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCardWideConfig(): CardWideConfig | null {
|
|
||||||
return this._cardWideConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getNonOverriddenConfig(): FrigateCardConfig | null {
|
|
||||||
return this._config;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getRawConfig(): RawFrigateCardConfig | null {
|
|
||||||
return this._rawConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setConfig(inputConfig?: RawFrigateCardConfig): void {
|
|
||||||
if (!inputConfig) {
|
|
||||||
throw new Error(localize('error.invalid_configuration'));
|
|
||||||
}
|
|
||||||
|
|
||||||
const parseResult = frigateCardConfigSchema.safeParse(inputConfig);
|
|
||||||
if (!parseResult.success) {
|
|
||||||
const configUpgradeable = isConfigUpgradeable(inputConfig);
|
|
||||||
const hint = getParseErrorPaths(parseResult.error);
|
|
||||||
let upgradeMessage = '';
|
|
||||||
if (configUpgradeable) {
|
|
||||||
upgradeMessage = `${localize('error.upgrade_available')}. `;
|
|
||||||
}
|
|
||||||
throw new Error(
|
|
||||||
upgradeMessage +
|
|
||||||
`${localize('error.invalid_configuration')}: ` +
|
|
||||||
(hint && hint.size
|
|
||||||
? JSON.stringify([...hint], null, ' ')
|
|
||||||
: localize('error.invalid_configuration_no_hint')),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
const config =
|
|
||||||
parseResult.data.performance.profile !== 'low'
|
|
||||||
? parseResult.data
|
|
||||||
: setLowPerformanceProfile(inputConfig, parseResult.data);
|
|
||||||
|
|
||||||
this._rawConfig = inputConfig;
|
|
||||||
if (isEqual(this._config, config)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._config = config;
|
|
||||||
this._cardWideConfig = {
|
|
||||||
performance: config.performance,
|
|
||||||
debug: config.debug,
|
|
||||||
};
|
|
||||||
|
|
||||||
this._api.getConditionsManager().setConditionsFromConfig();
|
|
||||||
this._api.getConditionsManager().setState({
|
|
||||||
view: undefined,
|
|
||||||
displayMode: undefined,
|
|
||||||
camera: undefined,
|
|
||||||
});
|
|
||||||
this._api.getMediaLoadedInfoManager().clear();
|
|
||||||
this._api.getViewManager().reset();
|
|
||||||
this._api.getMessageManager().reset();
|
|
||||||
this._api.getAutomationsManager().setAutomationsFromConfig();
|
|
||||||
this._api.getStyleManager().setPerformance();
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
|
|
||||||
this.computeOverrideConfig();
|
|
||||||
}
|
|
||||||
|
|
||||||
public computeOverrideConfig(): void {
|
|
||||||
const conditionsManager = this._api.getConditionsManager();
|
|
||||||
if (!this._config) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const overriddenConfig = getOverriddenConfig(
|
|
||||||
conditionsManager,
|
|
||||||
this._config,
|
|
||||||
this._config.overrides,
|
|
||||||
) as FrigateCardConfig;
|
|
||||||
|
|
||||||
// Save on Lit re-rendering costs by only updating the configuration if it
|
|
||||||
// actually changes.
|
|
||||||
if (isEqual(overriddenConfig, this._overriddenConfig)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const previousConfig = this._overriddenConfig;
|
|
||||||
this._overriddenConfig = overriddenConfig;
|
|
||||||
|
|
||||||
this._api.getStyleManager().setMinMaxHeight();
|
|
||||||
|
|
||||||
if (
|
|
||||||
previousConfig &&
|
|
||||||
(!isEqual(previousConfig?.cameras, this._overriddenConfig?.cameras) ||
|
|
||||||
!isEqual(previousConfig?.cameras_global, this._overriddenConfig?.cameras_global))
|
|
||||||
) {
|
|
||||||
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
previousConfig &&
|
|
||||||
previousConfig?.live.microphone.always_connected !==
|
|
||||||
this._overriddenConfig?.live.microphone.always_connected
|
|
||||||
) {
|
|
||||||
this._api
|
|
||||||
.getInitializationManager()
|
|
||||||
.uninitialize(InitializationAspect.MICROPHONE_CONNECT);
|
|
||||||
}
|
|
||||||
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,259 +0,0 @@
|
|||||||
import { LovelaceCardEditor } from 'custom-card-helpers';
|
|
||||||
import { ReactiveController } from 'lit';
|
|
||||||
import { CameraManager } from '../../camera-manager/manager';
|
|
||||||
import { FrigateCardConfig } from '../../config/types';
|
|
||||||
import { EntityRegistryManager } from '../ha/entity-registry';
|
|
||||||
import { EntityCache } from '../ha/entity-registry/cache';
|
|
||||||
import { ResolvedMediaCache } from '../ha/resolved-media';
|
|
||||||
import { ActionsManager } from './actions-manager';
|
|
||||||
import { AutoUpdateManager } from './auto-update-manager';
|
|
||||||
import { AutomationsManager } from './automations-manager';
|
|
||||||
import { CameraURLManager } from './camera-url-manager';
|
|
||||||
import {
|
|
||||||
CardElementManager,
|
|
||||||
CardHTMLElement,
|
|
||||||
MenuToggleCallback,
|
|
||||||
ScrollCallback,
|
|
||||||
} from './card-element-manager';
|
|
||||||
import { ConditionsManager, ConditionsManagerListener } from './conditions-manager';
|
|
||||||
import { ConfigManager } from './config-manager';
|
|
||||||
import { DownloadManager } from './download-manager';
|
|
||||||
import { ExpandManager } from './expand-manager';
|
|
||||||
import { FullscreenManager } from './fullscreen-manager';
|
|
||||||
import { HASSManager } from './hass-manager';
|
|
||||||
import { InitializationManager } from './initialization-manager';
|
|
||||||
import { InteractionManager } from './interaction-manager';
|
|
||||||
import { MediaLoadedInfoManager } from './media-info-manager';
|
|
||||||
import { MediaPlayerManager } from './media-player-manager';
|
|
||||||
import { MessageManager } from './message-manager';
|
|
||||||
import { MicrophoneManager } from './microphone-manager';
|
|
||||||
import { QueryStringManager } from './query-string-manager';
|
|
||||||
import { StyleManager } from './style-manager';
|
|
||||||
import { TriggersManager } from './triggers-manager';
|
|
||||||
import {
|
|
||||||
CardActionsManagerAPI,
|
|
||||||
CardAutoRefreshAPI,
|
|
||||||
CardAutomationsAPI,
|
|
||||||
CardCameraAPI,
|
|
||||||
CardCameraURLAPI,
|
|
||||||
CardConditionAPI,
|
|
||||||
CardConfigAPI,
|
|
||||||
CardDownloadAPI,
|
|
||||||
CardElementAPI,
|
|
||||||
CardExpandAPI,
|
|
||||||
CardFullscreenAPI,
|
|
||||||
CardHASSAPI,
|
|
||||||
CardInitializerAPI,
|
|
||||||
CardInteractionAPI,
|
|
||||||
CardMediaLoadedAPI,
|
|
||||||
CardMediaPlayerAPI,
|
|
||||||
CardMessageAPI,
|
|
||||||
CardMicrophoneAPI,
|
|
||||||
CardQueryStringAPI,
|
|
||||||
CardStyleAPI,
|
|
||||||
CardTriggersAPI,
|
|
||||||
CardViewAPI,
|
|
||||||
} from './types';
|
|
||||||
import { ViewManager } from './view-manager';
|
|
||||||
|
|
||||||
export class CardController
|
|
||||||
implements
|
|
||||||
CardActionsManagerAPI,
|
|
||||||
CardAutomationsAPI,
|
|
||||||
CardAutoRefreshAPI,
|
|
||||||
CardCameraAPI,
|
|
||||||
CardCameraURLAPI,
|
|
||||||
CardConditionAPI,
|
|
||||||
CardConfigAPI,
|
|
||||||
CardDownloadAPI,
|
|
||||||
CardElementAPI,
|
|
||||||
CardExpandAPI,
|
|
||||||
CardFullscreenAPI,
|
|
||||||
CardHASSAPI,
|
|
||||||
CardInitializerAPI,
|
|
||||||
CardInteractionAPI,
|
|
||||||
CardMediaLoadedAPI,
|
|
||||||
CardMediaPlayerAPI,
|
|
||||||
CardMessageAPI,
|
|
||||||
CardMicrophoneAPI,
|
|
||||||
CardQueryStringAPI,
|
|
||||||
CardStyleAPI,
|
|
||||||
CardTriggersAPI,
|
|
||||||
CardViewAPI,
|
|
||||||
ReactiveController
|
|
||||||
{
|
|
||||||
// These properties may be used in the construction of 'managers' (and should
|
|
||||||
// be created first).
|
|
||||||
protected _entityRegistryManager = new EntityRegistryManager(new EntityCache());
|
|
||||||
protected _resolvedMediaCache = new ResolvedMediaCache();
|
|
||||||
|
|
||||||
protected _actionsManager = new ActionsManager(this);
|
|
||||||
protected _automationsManager = new AutomationsManager(this);
|
|
||||||
protected _autoUpdateManager = new AutoUpdateManager(this);
|
|
||||||
protected _cameraManager = new CameraManager(this);
|
|
||||||
protected _cameraURLManager = new CameraURLManager(this);
|
|
||||||
protected _cardElementManager: CardElementManager;
|
|
||||||
protected _conditionsManager: ConditionsManager;
|
|
||||||
protected _configManager = new ConfigManager(this);
|
|
||||||
protected _downloadManager = new DownloadManager(this);
|
|
||||||
protected _expandManager = new ExpandManager(this);
|
|
||||||
protected _fullscreenManager = new FullscreenManager(this);
|
|
||||||
protected _hassManager = new HASSManager(this);
|
|
||||||
protected _initializationManager = new InitializationManager(this);
|
|
||||||
protected _interactionManager = new InteractionManager(this);
|
|
||||||
protected _mediaLoadedInfoManager = new MediaLoadedInfoManager(this);
|
|
||||||
protected _mediaPlayerManager = new MediaPlayerManager(this);
|
|
||||||
protected _messageManager = new MessageManager(this);
|
|
||||||
protected _microphoneManager = new MicrophoneManager(this);
|
|
||||||
protected _queryStringManager = new QueryStringManager(this);
|
|
||||||
protected _styleManager = new StyleManager(this);
|
|
||||||
protected _triggersManager = new TriggersManager(this);
|
|
||||||
protected _viewManager = new ViewManager(this);
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
host: CardHTMLElement,
|
|
||||||
scrollCallback: ScrollCallback,
|
|
||||||
menuToggleCallback: MenuToggleCallback,
|
|
||||||
conditionListener: ConditionsManagerListener,
|
|
||||||
) {
|
|
||||||
host.addController(this);
|
|
||||||
|
|
||||||
this._conditionsManager = new ConditionsManager(this, conditionListener);
|
|
||||||
this._cardElementManager = new CardElementManager(
|
|
||||||
this,
|
|
||||||
host,
|
|
||||||
scrollCallback,
|
|
||||||
menuToggleCallback,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// *************************************************************************
|
|
||||||
// Accessors
|
|
||||||
// *************************************************************************
|
|
||||||
|
|
||||||
public getActionsManager(): ActionsManager {
|
|
||||||
return this._actionsManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAutomationsManager(): AutomationsManager {
|
|
||||||
return this._automationsManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAutoUpdateManager(): AutoUpdateManager {
|
|
||||||
return this._autoUpdateManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCameraManager(): CameraManager {
|
|
||||||
return this._cameraManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCameraURLManager(): CameraURLManager {
|
|
||||||
return this._cameraURLManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getCardElementManager(): CardElementManager {
|
|
||||||
return this._cardElementManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getConditionsManager(): ConditionsManager {
|
|
||||||
return this._conditionsManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
|
||||||
await import('../../editor.js');
|
|
||||||
return document.createElement('frigate-card-editor');
|
|
||||||
}
|
|
||||||
|
|
||||||
public getConfigManager(): ConfigManager {
|
|
||||||
return this._configManager;
|
|
||||||
}
|
|
||||||
public getDownloadManager(): DownloadManager {
|
|
||||||
return this._downloadManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getEntityRegistryManager(): EntityRegistryManager {
|
|
||||||
return this._entityRegistryManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getExpandManager(): ExpandManager {
|
|
||||||
return this._expandManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getFullscreenManager(): FullscreenManager {
|
|
||||||
return this._fullscreenManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getHASSManager(): HASSManager {
|
|
||||||
return this._hassManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getInitializationManager(): InitializationManager {
|
|
||||||
return this._initializationManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getInteractionManager(): InteractionManager {
|
|
||||||
return this._interactionManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getMediaLoadedInfoManager(): MediaLoadedInfoManager {
|
|
||||||
return this._mediaLoadedInfoManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getMediaPlayerManager(): MediaPlayerManager {
|
|
||||||
return this._mediaPlayerManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getMessageManager(): MessageManager {
|
|
||||||
return this._messageManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getMicrophoneManager(): MicrophoneManager {
|
|
||||||
return this._microphoneManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getQueryStringManager(): QueryStringManager {
|
|
||||||
return this._queryStringManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getResolvedMediaCache(): ResolvedMediaCache {
|
|
||||||
return this._resolvedMediaCache;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static getStubConfig(entities: string[]): FrigateCardConfig {
|
|
||||||
const cameraEntity = entities.find((element) => element.startsWith('camera.'));
|
|
||||||
return {
|
|
||||||
cameras: [
|
|
||||||
{
|
|
||||||
camera_entity: cameraEntity ?? 'camera.demo',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
// Need to use 'as unknown' to convince Typescript that this really isn't a
|
|
||||||
// mistake, despite the miniscule size of the configuration vs the full type
|
|
||||||
// description.
|
|
||||||
} as unknown as FrigateCardConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getStyleManager(): StyleManager {
|
|
||||||
return this._styleManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getTriggersManager(): TriggersManager {
|
|
||||||
return this._triggersManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getViewManager(): ViewManager {
|
|
||||||
return this._viewManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
// *************************************************************************
|
|
||||||
// Handlers
|
|
||||||
// *************************************************************************
|
|
||||||
|
|
||||||
public hostConnected(): void {
|
|
||||||
this.getCardElementManager().elementConnected();
|
|
||||||
}
|
|
||||||
|
|
||||||
public hostDisconnected(): void {
|
|
||||||
this.getCardElementManager().elementDisconnected();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { downloadMedia, downloadURL } from '../download';
|
|
||||||
import { generateScreenshotTitle } from '../screenshot';
|
|
||||||
import { CardDownloadAPI } from './types';
|
|
||||||
|
|
||||||
export class DownloadManager {
|
|
||||||
protected _api: CardDownloadAPI;
|
|
||||||
|
|
||||||
constructor(api: CardDownloadAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async downloadViewerMedia(): Promise<boolean> {
|
|
||||||
const media = this._api
|
|
||||||
.getViewManager()
|
|
||||||
.getView()
|
|
||||||
?.queryResults?.getSelectedResult();
|
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
|
||||||
|
|
||||||
if (!media || !hass) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await downloadMedia(hass, this._api.getCameraManager(), media);
|
|
||||||
} catch (error: unknown) {
|
|
||||||
this._api.getMessageManager().setErrorIfHigherPriority(error);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async downloadScreenshot(): Promise<void> {
|
|
||||||
const url = await this._api
|
|
||||||
.getMediaLoadedInfoManager()
|
|
||||||
.get()
|
|
||||||
?.player?.getScreenshotURL();
|
|
||||||
if (url) {
|
|
||||||
downloadURL(url, generateScreenshotTitle(this._api.getViewManager().getView()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
import { CardExpandAPI } from './types';
|
|
||||||
|
|
||||||
export class ExpandManager {
|
|
||||||
protected _expanded = false;
|
|
||||||
protected _api: CardExpandAPI;
|
|
||||||
|
|
||||||
constructor(api: CardExpandAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isExpanded(): boolean {
|
|
||||||
return this._expanded;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toggleExpanded(): void {
|
|
||||||
this.setExpanded(!this._expanded);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setExpanded(expanded: boolean): void {
|
|
||||||
if (expanded && this._api.getFullscreenManager().isInFullscreen()) {
|
|
||||||
// Fullscreen and expanded mode are mutually exclusive.
|
|
||||||
this._api.getFullscreenManager().stopFullscreen();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._expanded = expanded;
|
|
||||||
this._api.getConditionsManager()?.setState({
|
|
||||||
expand: expanded,
|
|
||||||
});
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
import screenfull from 'screenfull';
|
|
||||||
import { CardFullscreenAPI } from './types';
|
|
||||||
|
|
||||||
export class FullscreenManager {
|
|
||||||
protected _api: CardFullscreenAPI;
|
|
||||||
|
|
||||||
constructor(api: CardFullscreenAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public connect(): void {
|
|
||||||
if (screenfull.isEnabled) {
|
|
||||||
screenfull.on('change', this._fullscreenHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public disconnect(): void {
|
|
||||||
if (screenfull.isEnabled) {
|
|
||||||
screenfull.off('change', this._fullscreenHandler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isInFullscreen(): boolean {
|
|
||||||
return screenfull.isEnabled && screenfull.isFullscreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
public toggleFullscreen(): void {
|
|
||||||
screenfull.toggle(this._api.getCardElementManager().getElement());
|
|
||||||
}
|
|
||||||
|
|
||||||
public stopFullscreen(): void {
|
|
||||||
screenfull.exit();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _fullscreenHandler = (): void => {
|
|
||||||
this._api.getExpandManager().setExpanded(false);
|
|
||||||
|
|
||||||
this._api.getConditionsManager()?.setState({
|
|
||||||
fullscreen: this.isInFullscreen(),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Re-render after a change to fullscreen mode to take advantage of
|
|
||||||
// the expanded screen real-estate (vs staying in aspect-ratio locked
|
|
||||||
// modes).
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
import { CameraConfig } from '../../config/types';
|
|
||||||
import { localize } from '../../localize/localize';
|
|
||||||
import { ExtendedHomeAssistant } from '../../types';
|
|
||||||
import { hasHAConnectionStateChanged, isHassDifferent } from '../ha';
|
|
||||||
import { CardHASSAPI } from './types';
|
|
||||||
|
|
||||||
export class HASSManager {
|
|
||||||
protected _hass: ExtendedHomeAssistant | null = null;
|
|
||||||
protected _api: CardHASSAPI;
|
|
||||||
|
|
||||||
constructor(api: CardHASSAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getHASS(): ExtendedHomeAssistant | null {
|
|
||||||
return this._hass;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setHASS(hass: ExtendedHomeAssistant): void {
|
|
||||||
const getSelectedCameraConfig = (): CameraConfig | null => {
|
|
||||||
const view = this._api.getViewManager().getView();
|
|
||||||
const cameraManager = this._api.getCameraManager();
|
|
||||||
|
|
||||||
return view && cameraManager
|
|
||||||
? cameraManager?.getStore().getCameraConfig(view.camera) ?? null
|
|
||||||
: null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const oldHass = this._hass;
|
|
||||||
this._hass = hass;
|
|
||||||
|
|
||||||
const selectedCamera = getSelectedCameraConfig();
|
|
||||||
|
|
||||||
if (hasHAConnectionStateChanged(oldHass, hass)) {
|
|
||||||
if (!this._hass?.connected) {
|
|
||||||
this._api.getMessageManager().setMessageIfHigherPriority({
|
|
||||||
message: localize('error.reconnecting'),
|
|
||||||
icon: 'mdi:lan-disconnect',
|
|
||||||
type: 'connection',
|
|
||||||
dotdotdot: true,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._api.getViewManager().setViewDefault();
|
|
||||||
}
|
|
||||||
} else if (
|
|
||||||
// Home Assistant pumps a lot of updates through. Re-rendering the card is
|
|
||||||
// necessary at times (e.g. to update the 'clip' view as new clips
|
|
||||||
// arrive), but also is a jarring experience for the user (e.g. if they
|
|
||||||
// are browsing the mini-gallery). Do not allow re-rendering from a Home
|
|
||||||
// Assistant update if there's been recent interaction (e.g. clicks on the
|
|
||||||
// card) or if there is media active playing.
|
|
||||||
this._isAutomatedViewUpdateAllowed() &&
|
|
||||||
isHassDifferent(this._hass, oldHass, [
|
|
||||||
...(this._api.getConfigManager().getConfig()?.view.update_entities ?? []),
|
|
||||||
...(selectedCamera?.triggers.entities ?? []),
|
|
||||||
])
|
|
||||||
) {
|
|
||||||
// If entities being monitored have changed then reset the view to the
|
|
||||||
// default.
|
|
||||||
this._api.getViewManager().setViewDefault();
|
|
||||||
} else if (
|
|
||||||
isHassDifferent(this._hass, oldHass, [
|
|
||||||
...(this._api.getConfigManager().getConfig()?.view.render_entities ?? []),
|
|
||||||
|
|
||||||
// Refresh the card if media player state changes:
|
|
||||||
// https://github.com/dermotduffy/frigate-hass-card/issues/881
|
|
||||||
...this._api.getMediaPlayerManager().getMediaPlayers(),
|
|
||||||
])
|
|
||||||
) {
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._api.getTriggersManager().updateTriggeredCameras(oldHass);
|
|
||||||
|
|
||||||
if (this._api.getConditionsManager().hasHAStateConditions()) {
|
|
||||||
this._api.getConditionsManager().setState({ state: this._hass.states });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Dark mode may depend on HASS.
|
|
||||||
this._api.getStyleManager().setLightOrDarkMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _isAutomatedViewUpdateAllowed(): boolean {
|
|
||||||
return (
|
|
||||||
this._api.getConfigManager().getConfig()?.view.update_force ||
|
|
||||||
!this._api.getInteractionManager().hasInteraction()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,134 +0,0 @@
|
|||||||
import { loadLanguages } from '../../localize/localize';
|
|
||||||
import { sideLoadHomeAssistantElements } from '../ha';
|
|
||||||
import { Initializer } from '../initializer/initializer';
|
|
||||||
import { CardInitializerAPI } from './types';
|
|
||||||
|
|
||||||
export enum InitializationAspect {
|
|
||||||
LANGUAGES = 'languages',
|
|
||||||
SIDE_LOAD_ELEMENTS = 'side-load-elements',
|
|
||||||
MEDIA_PLAYERS = 'media-players',
|
|
||||||
CAMERAS = 'cameras',
|
|
||||||
MICROPHONE_CONNECT = 'microphone-connect',
|
|
||||||
}
|
|
||||||
|
|
||||||
export class InitializationManager {
|
|
||||||
protected _api: CardInitializerAPI;
|
|
||||||
protected _initializer;
|
|
||||||
|
|
||||||
constructor(api: CardInitializerAPI, initializer?: Initializer) {
|
|
||||||
this._api = api;
|
|
||||||
this._initializer = initializer ?? new Initializer();
|
|
||||||
}
|
|
||||||
|
|
||||||
public isInitializedMandatory(): boolean {
|
|
||||||
return this._initializer.isInitializedMultiple([
|
|
||||||
InitializationAspect.LANGUAGES,
|
|
||||||
InitializationAspect.SIDE_LOAD_ELEMENTS,
|
|
||||||
InitializationAspect.CAMERAS,
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialize the hard requirements for rendering anything.
|
|
||||||
* @returns `true` if card rendering can continue.
|
|
||||||
*/
|
|
||||||
public async initializeMandatory(): Promise<boolean> {
|
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
|
||||||
if (!hass) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(await this._initializer.initializeMultipleIfNecessary({
|
|
||||||
// Caution: Ensure nothing in this set of initializers requires
|
|
||||||
// config or languages since they will not yet have been initialized.
|
|
||||||
[InitializationAspect.LANGUAGES]: async () => await loadLanguages(hass),
|
|
||||||
[InitializationAspect.SIDE_LOAD_ELEMENTS]: async () =>
|
|
||||||
await sideLoadHomeAssistantElements(),
|
|
||||||
}))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._api.getConfigManager().hasConfig()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(await this._initializer.initializeIfNecessary(
|
|
||||||
InitializationAspect.CAMERAS,
|
|
||||||
async () => await this._api.getCameraManager().initializeCamerasFromConfig(),
|
|
||||||
))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!this._api.getMessageManager().hasMessage()) {
|
|
||||||
// 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().setViewDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
this._initializer.isInitializedMultiple([
|
|
||||||
...(config.menu.buttons.media_player.enabled
|
|
||||||
? [InitializationAspect.MEDIA_PLAYERS]
|
|
||||||
: []),
|
|
||||||
...(config.live.microphone.always_connected
|
|
||||||
? [InitializationAspect.MICROPHONE_CONNECT]
|
|
||||||
: []),
|
|
||||||
])
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
!(await this._initializer.initializeMultipleIfNecessary({
|
|
||||||
...(config.menu.buttons.media_player.enabled && {
|
|
||||||
[InitializationAspect.MEDIA_PLAYERS]: async () =>
|
|
||||||
await this._api.getMediaPlayerManager().initialize(),
|
|
||||||
}),
|
|
||||||
...(config.live.microphone.always_connected && {
|
|
||||||
[InitializationAspect.MICROPHONE_CONNECT]: async () =>
|
|
||||||
await this._api.getMicrophoneManager().connect(),
|
|
||||||
}),
|
|
||||||
}))
|
|
||||||
) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public uninitialize(aspect: InitializationAspect) {
|
|
||||||
return this._initializer.uninitialize(aspect);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
import throttle from 'lodash-es/throttle';
|
|
||||||
import { Timer } from '../timer';
|
|
||||||
import { CardInteractionAPI } from './types';
|
|
||||||
|
|
||||||
export class InteractionManager {
|
|
||||||
protected _timer = new Timer();
|
|
||||||
protected _api: CardInteractionAPI;
|
|
||||||
|
|
||||||
constructor(api: CardInteractionAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The mouse handler may be called continually, throttle it to at most once
|
|
||||||
// per second for performance reasons.
|
|
||||||
public reportInteraction = throttle(() => {
|
|
||||||
this._reportInteraction();
|
|
||||||
}, 1 * 1000);
|
|
||||||
|
|
||||||
public hasInteraction(): boolean {
|
|
||||||
return this._timer.isRunning();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Start the user interaction ('screensaver') timer to reset the view to
|
|
||||||
* default `view.timeout_seconds` after user interaction.
|
|
||||||
*/
|
|
||||||
protected _reportInteraction(): void {
|
|
||||||
this._timer.stop();
|
|
||||||
|
|
||||||
// Interactions reset the trigger state.
|
|
||||||
this._api.getTriggersManager().untrigger();
|
|
||||||
|
|
||||||
const timeoutSeconds = this._api.getConfigManager().getConfig()
|
|
||||||
?.view.timeout_seconds;
|
|
||||||
|
|
||||||
if (timeoutSeconds) {
|
|
||||||
this._timer.start(timeoutSeconds, () => {
|
|
||||||
if (this._isAutomatedUpdateAllowed()) {
|
|
||||||
this._api.getViewManager().setViewDefault();
|
|
||||||
this._api.getStyleManager().setLightOrDarkMode();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
this._api.getStyleManager().setLightOrDarkMode();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _isAutomatedUpdateAllowed(): boolean {
|
|
||||||
return !this._api.getTriggersManager().isTriggered();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
import { MediaLoadedInfo } from '../../types';
|
|
||||||
import { log } from '../debug';
|
|
||||||
import { isValidMediaLoadedInfo } from '../media-info';
|
|
||||||
import { CardMediaLoadedAPI } from './types';
|
|
||||||
|
|
||||||
export class MediaLoadedInfoManager {
|
|
||||||
protected _api: CardMediaLoadedAPI;
|
|
||||||
protected _current: MediaLoadedInfo | null = null;
|
|
||||||
protected _lastKnown: MediaLoadedInfo | null = null;
|
|
||||||
|
|
||||||
constructor(api: CardMediaLoadedAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public set(mediaInfo: MediaLoadedInfo): void {
|
|
||||||
if (!isValidMediaLoadedInfo(mediaInfo)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
log(
|
|
||||||
this._api.getConfigManager().getCardWideConfig(),
|
|
||||||
`Frigate Card media load: `,
|
|
||||||
mediaInfo,
|
|
||||||
);
|
|
||||||
|
|
||||||
this._current = mediaInfo;
|
|
||||||
this._lastKnown = mediaInfo;
|
|
||||||
|
|
||||||
this._api.getConditionsManager().setState({ media_loaded: true });
|
|
||||||
|
|
||||||
// Fresh media information may change how the card is rendered.
|
|
||||||
this._api.getStyleManager().setExpandedMode();
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
}
|
|
||||||
|
|
||||||
public get(): MediaLoadedInfo | null {
|
|
||||||
return this._current;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getLastKnown(): MediaLoadedInfo | null {
|
|
||||||
return this._lastKnown;
|
|
||||||
}
|
|
||||||
|
|
||||||
public clear(): void {
|
|
||||||
this._current = null;
|
|
||||||
this._api.getConditionsManager().setState({ media_loaded: false });
|
|
||||||
}
|
|
||||||
|
|
||||||
public has(): boolean {
|
|
||||||
return !!this._current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,125 +0,0 @@
|
|||||||
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../../const';
|
|
||||||
import { ViewMedia } from '../../view/media';
|
|
||||||
import { ViewMediaClassifier } from '../../view/media-classifier';
|
|
||||||
import { errorToConsole } from '../basic';
|
|
||||||
import { Entity } from '../ha/entity-registry/types';
|
|
||||||
import { supportsFeature } from '../ha/update';
|
|
||||||
import { CardMediaPlayerAPI } from './types';
|
|
||||||
|
|
||||||
export class MediaPlayerManager {
|
|
||||||
protected _mediaPlayers: string[] = [];
|
|
||||||
|
|
||||||
protected _api: CardMediaPlayerAPI;
|
|
||||||
|
|
||||||
constructor(api: CardMediaPlayerAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getMediaPlayers(): string[] {
|
|
||||||
return this._mediaPlayers;
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasMediaPlayers(): boolean {
|
|
||||||
return this._mediaPlayers.length > 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async initialize(): Promise<void> {
|
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
|
||||||
if (!hass) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const isValidMediaPlayer = (entityID: string): boolean => {
|
|
||||||
if (entityID.startsWith('media_player.')) {
|
|
||||||
const stateObj = hass.states[entityID];
|
|
||||||
if (
|
|
||||||
stateObj &&
|
|
||||||
stateObj.state !== 'unavailable' &&
|
|
||||||
supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA)
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
const mediaPlayers = Object.keys(hass.states).filter(isValidMediaPlayer);
|
|
||||||
let mediaPlayerEntities: Map<string, Entity> | null = null;
|
|
||||||
try {
|
|
||||||
mediaPlayerEntities = await this._api
|
|
||||||
.getEntityRegistryManager()
|
|
||||||
.getEntities(hass, mediaPlayers);
|
|
||||||
} catch (e) {
|
|
||||||
// Failing to fetch media player information is not considered
|
|
||||||
// sufficiently serious to block card startup -- it is just logged and we
|
|
||||||
// move on.
|
|
||||||
errorToConsole(e as Error);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Filter out entities that are marked as hidden (this information is not
|
|
||||||
// available in the HA state, only in the registry).
|
|
||||||
this._mediaPlayers = mediaPlayers.filter((entityID) => {
|
|
||||||
// Specifically allow for media players that are not found in the entity registry:
|
|
||||||
// See: https://github.com/dermotduffy/frigate-hass-card/issues/1016
|
|
||||||
const entity = mediaPlayerEntities?.get(entityID);
|
|
||||||
return !entity || !entity.hidden_by;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async stop(mediaPlayer: string): Promise<void> {
|
|
||||||
await this._api
|
|
||||||
.getHASSManager()
|
|
||||||
.getHASS()
|
|
||||||
?.callService('media_player', 'media_stop', {
|
|
||||||
entity_id: mediaPlayer,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async playLive(mediaPlayer: string, cameraID: string): Promise<void> {
|
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
|
||||||
const cameraConfig = this._api
|
|
||||||
.getCameraManager()
|
|
||||||
.getStore()
|
|
||||||
.getCameraConfig(cameraID);
|
|
||||||
const cameraEntity = cameraConfig?.camera_entity ?? null;
|
|
||||||
|
|
||||||
if (!hass || !cameraEntity) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const title =
|
|
||||||
this._api.getCameraManager().getCameraMetadata(cameraID)?.title ?? null;
|
|
||||||
const thumbnail = hass.states[cameraEntity]?.attributes?.entity_picture ?? null;
|
|
||||||
|
|
||||||
await hass.callService('media_player', 'play_media', {
|
|
||||||
entity_id: mediaPlayer,
|
|
||||||
media_content_id: `media-source://camera/${cameraEntity}`,
|
|
||||||
media_content_type: 'application/vnd.apple.mpegurl',
|
|
||||||
extra: {
|
|
||||||
...(title && { title: title }),
|
|
||||||
...(thumbnail && { thumb: thumbnail }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public async playMedia(mediaPlayer: string, media?: ViewMedia | null): Promise<void> {
|
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
|
||||||
|
|
||||||
if (!hass || !media) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const title = media.getTitle();
|
|
||||||
const thumbnail = media.getThumbnail();
|
|
||||||
|
|
||||||
await hass.callService('media_player', 'play_media', {
|
|
||||||
entity_id: mediaPlayer,
|
|
||||||
media_content_id: media.getContentID(),
|
|
||||||
media_content_type: ViewMediaClassifier.isVideo(media) ? 'video' : 'image',
|
|
||||||
extra: {
|
|
||||||
...(title && { title: title }),
|
|
||||||
...(thumbnail && { thumb: thumbnail }),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import { FrigateCardError, MESSAGE_TYPE_PRIORITIES, Message } from '../../types';
|
|
||||||
import { errorToConsole } from '../basic';
|
|
||||||
import { CardMessageAPI } from './types';
|
|
||||||
|
|
||||||
export class MessageManager {
|
|
||||||
protected _message: Message | null = null;
|
|
||||||
protected _api: CardMessageAPI;
|
|
||||||
|
|
||||||
constructor(api: CardMessageAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getMessage(): Message | null {
|
|
||||||
return this._message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasMessage(): boolean {
|
|
||||||
return !!this._message;
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasErrorMessage(): boolean {
|
|
||||||
return this._message?.type === 'error';
|
|
||||||
}
|
|
||||||
|
|
||||||
public reset(): void {
|
|
||||||
const hadMessage = this.hasMessage();
|
|
||||||
this._message = null;
|
|
||||||
|
|
||||||
if (hadMessage) {
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public setErrorIfHigherPriority(error: unknown): void {
|
|
||||||
// This object should accept unknown objects to be able to seamlessly
|
|
||||||
// process arguments to catch() which can only be unknown/any.
|
|
||||||
if (!(error instanceof Error)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
errorToConsole(error);
|
|
||||||
this.setMessageIfHigherPriority({
|
|
||||||
message: error.message,
|
|
||||||
type: 'error',
|
|
||||||
...(error instanceof FrigateCardError && { context: error.context }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
public setMessageIfHigherPriority(message: Message): boolean {
|
|
||||||
const currentPriority = this._message
|
|
||||||
? MESSAGE_TYPE_PRIORITIES[this._message.type]
|
|
||||||
: 0;
|
|
||||||
const newPriority = MESSAGE_TYPE_PRIORITIES[message.type];
|
|
||||||
|
|
||||||
if (this._message && newPriority < currentPriority) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._message = message;
|
|
||||||
|
|
||||||
// When a message is displayed it effectively unloads the media.
|
|
||||||
this._api.getMediaLoadedInfoManager().clear();
|
|
||||||
this._api.getCardElementManager().scrollReset();
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
import { errorToConsole } from '../basic';
|
|
||||||
import { Timer } from '../timer';
|
|
||||||
import { CardMicrophoneAPI } from './types';
|
|
||||||
|
|
||||||
export class MicrophoneManager {
|
|
||||||
protected _api: CardMicrophoneAPI;
|
|
||||||
protected _stream?: MediaStream | null;
|
|
||||||
protected _timer = new Timer();
|
|
||||||
|
|
||||||
// We keep mute state separate from the stream state so that mute/unmute can
|
|
||||||
// be expressed before the stream is created -- and when it's create it will
|
|
||||||
// have the right mute status.
|
|
||||||
protected _mute = true;
|
|
||||||
|
|
||||||
constructor(api: CardMicrophoneAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async connect(): Promise<void> {
|
|
||||||
try {
|
|
||||||
this._stream = await navigator.mediaDevices.getUserMedia({
|
|
||||||
audio: true,
|
|
||||||
video: false,
|
|
||||||
});
|
|
||||||
} catch (e: unknown) {
|
|
||||||
errorToConsole(e as Error);
|
|
||||||
this._stream = null;
|
|
||||||
}
|
|
||||||
this._setMute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async disconnect(): Promise<void> {
|
|
||||||
this._stream?.getTracks().forEach((track) => track.stop());
|
|
||||||
this._stream = undefined;
|
|
||||||
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
}
|
|
||||||
|
|
||||||
public getStream(): MediaStream | undefined {
|
|
||||||
return this._stream ?? undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _setMute(): void {
|
|
||||||
this._stream?.getTracks().forEach((track) => {
|
|
||||||
track.enabled = !this._mute;
|
|
||||||
});
|
|
||||||
this._startTimer();
|
|
||||||
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
}
|
|
||||||
|
|
||||||
public mute(): void {
|
|
||||||
this._mute = true;
|
|
||||||
this._setMute();
|
|
||||||
}
|
|
||||||
|
|
||||||
public async unmute(): Promise<void> {
|
|
||||||
const unmute = (): void => {
|
|
||||||
this._mute = false;
|
|
||||||
this._setMute();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!this.isConnected() && !this.isForbidden()) {
|
|
||||||
// The connect() call is async and make take an arbitrary amount of
|
|
||||||
// time for the user to grant access to their microphone. With a
|
|
||||||
// momentary microphone button the mute call (on mouse release) may
|
|
||||||
// arrive before the connection is even granted, so we unmute first
|
|
||||||
// before the connection is made, so the mute call on release will not
|
|
||||||
// be 'overwritten' incorrectly.
|
|
||||||
unmute();
|
|
||||||
await this.connect();
|
|
||||||
} else if (this.isConnected()) {
|
|
||||||
unmute();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isConnected(): boolean {
|
|
||||||
return !!this._stream;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isForbidden(): boolean {
|
|
||||||
return this._stream === null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isMuted(): boolean {
|
|
||||||
// For safety, this function always returns the stream mute status directly
|
|
||||||
// (rather the internal state).
|
|
||||||
return !this._stream || this._stream.getTracks().every((track) => !track.enabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _startTimer(): void {
|
|
||||||
const microphoneConfig = this._api.getConfigManager().getConfig()
|
|
||||||
?.live.microphone;
|
|
||||||
|
|
||||||
if (microphoneConfig?.always_connected) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const disconnectSeconds = microphoneConfig?.disconnect_seconds ?? 0;
|
|
||||||
|
|
||||||
if (disconnectSeconds) {
|
|
||||||
this._timer.start(disconnectSeconds, () => {
|
|
||||||
this.disconnect();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
import { FrigateCardCustomAction, FrigateCardViewAction } from '../../config/types';
|
|
||||||
import { createFrigateCardCustomAction } from '../action.js';
|
|
||||||
import { CardQueryStringAPI } from './types';
|
|
||||||
import { ViewManagerSetViewParameters } from './view-manager';
|
|
||||||
|
|
||||||
interface QueryStringViewIntent {
|
|
||||||
view?: ViewManagerSetViewParameters & {
|
|
||||||
default?: boolean;
|
|
||||||
};
|
|
||||||
other?: FrigateCardCustomAction[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export class QueryStringManager {
|
|
||||||
protected _api: CardQueryStringAPI;
|
|
||||||
|
|
||||||
constructor(api: CardQueryStringAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public hasViewRelatedActions(): boolean {
|
|
||||||
return !!this._calculateIntent().view;
|
|
||||||
}
|
|
||||||
|
|
||||||
public executeNonViewRelated(): void {
|
|
||||||
this._executeNonViewRelated(this._calculateIntent());
|
|
||||||
}
|
|
||||||
|
|
||||||
public executeViewRelated(): void {
|
|
||||||
this._executeViewRelated(this._calculateIntent());
|
|
||||||
}
|
|
||||||
|
|
||||||
public executeAll(): void {
|
|
||||||
const intent = this._calculateIntent();
|
|
||||||
this._executeViewRelated(intent);
|
|
||||||
this._executeNonViewRelated(intent);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _executeViewRelated(intent: QueryStringViewIntent): void {
|
|
||||||
if (intent.view) {
|
|
||||||
if (intent.view.default) {
|
|
||||||
this._api.getViewManager().setViewDefault({
|
|
||||||
...(intent.view.cameraID && { cameraID: intent.view.cameraID }),
|
|
||||||
...(intent.view.substream && { substream: intent.view.substream }),
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
this._api.getViewManager().setViewByParameters({
|
|
||||||
...(intent.view.viewName && { viewName: intent.view.viewName }),
|
|
||||||
...(intent.view.cameraID && { cameraID: intent.view.cameraID }),
|
|
||||||
...(intent.view.substream && { substream: intent.view.substream }),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _executeNonViewRelated(intent: QueryStringViewIntent): void {
|
|
||||||
// Only execute non-view actions when the card has rendered at least once.
|
|
||||||
if (!this._api.getCardElementManager().hasUpdated()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
intent.other?.forEach((action) =>
|
|
||||||
this._api.getActionsManager().executeAction(action),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _calculateIntent(): QueryStringViewIntent {
|
|
||||||
const result: QueryStringViewIntent = {};
|
|
||||||
for (const action of this._getActions()) {
|
|
||||||
if (this._isViewAction(action)) {
|
|
||||||
(result.view ??= {}).viewName = action.frigate_card_action;
|
|
||||||
(result.view ??= {}).default = undefined;
|
|
||||||
} else if (action.frigate_card_action === 'default') {
|
|
||||||
(result.view ??= {}).default = true;
|
|
||||||
(result.view ??= {}).viewName = undefined;
|
|
||||||
} else if (action.frigate_card_action === 'camera_select') {
|
|
||||||
(result.view ??= {}).cameraID = action.camera;
|
|
||||||
} else if (action.frigate_card_action === 'live_substream_select') {
|
|
||||||
(result.view ??= {}).substream = action.camera;
|
|
||||||
} else {
|
|
||||||
(result.other ??= []).push(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _getActions(): FrigateCardCustomAction[] {
|
|
||||||
const params = new URLSearchParams(window.location.search);
|
|
||||||
const actions: FrigateCardCustomAction[] = [];
|
|
||||||
const actionRE = new RegExp(
|
|
||||||
/^frigate-card-action([.:](?<cardID>\w+))?[.:](?<action>\w+)/,
|
|
||||||
);
|
|
||||||
for (const [key, value] of params.entries()) {
|
|
||||||
const match = key.match(actionRE);
|
|
||||||
if (!match || !match.groups) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const cardID: string | undefined = match.groups['cardID'];
|
|
||||||
const action = match.groups['action'];
|
|
||||||
|
|
||||||
let customAction: FrigateCardCustomAction | null = null;
|
|
||||||
switch (action) {
|
|
||||||
case 'camera_select':
|
|
||||||
case 'live_substream_select':
|
|
||||||
if (value) {
|
|
||||||
customAction = createFrigateCardCustomAction(action, {
|
|
||||||
camera: value,
|
|
||||||
cardID: cardID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'camera_ui':
|
|
||||||
case 'clip':
|
|
||||||
case 'clips':
|
|
||||||
case 'default':
|
|
||||||
case 'diagnostics':
|
|
||||||
case 'download':
|
|
||||||
case 'expand':
|
|
||||||
case 'image':
|
|
||||||
case 'live':
|
|
||||||
case 'menu_toggle':
|
|
||||||
case 'recording':
|
|
||||||
case 'recordings':
|
|
||||||
case 'snapshot':
|
|
||||||
case 'snapshots':
|
|
||||||
case 'timeline':
|
|
||||||
customAction = createFrigateCardCustomAction(action, {
|
|
||||||
cardID: cardID,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn(
|
|
||||||
`Frigate card received unknown card action in query string: ${action}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (customAction) {
|
|
||||||
actions.push(customAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return actions;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _isViewAction = (
|
|
||||||
action: FrigateCardCustomAction,
|
|
||||||
): action is FrigateCardViewAction => {
|
|
||||||
switch (action.frigate_card_action) {
|
|
||||||
case 'clip':
|
|
||||||
case 'clips':
|
|
||||||
case 'diagnostics':
|
|
||||||
case 'image':
|
|
||||||
case 'live':
|
|
||||||
case 'recording':
|
|
||||||
case 'recordings':
|
|
||||||
case 'snapshot':
|
|
||||||
case 'snapshots':
|
|
||||||
case 'timeline':
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import { FrigateCardConfig } from '../../config/types';
|
|
||||||
import { setPerformanceCSSStyles } from '../../performance';
|
|
||||||
import { View } from '../../view/view';
|
|
||||||
import { setOrRemoveAttribute } from '../basic';
|
|
||||||
import { CardStyleAPI } from './types';
|
|
||||||
|
|
||||||
export class StyleManager {
|
|
||||||
protected _api: CardStyleAPI;
|
|
||||||
|
|
||||||
constructor(api: CardStyleAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setLightOrDarkMode(): void {
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
|
||||||
const isDarkMode =
|
|
||||||
config?.view.dark_mode === 'on' ||
|
|
||||||
(config?.view.dark_mode === 'auto' &&
|
|
||||||
(!this._api.getInteractionManager().hasInteraction() ||
|
|
||||||
!!this._api.getHASSManager().getHASS()?.themes.darkMode));
|
|
||||||
|
|
||||||
setOrRemoveAttribute(
|
|
||||||
this._api.getCardElementManager().getElement(),
|
|
||||||
isDarkMode,
|
|
||||||
'dark',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setExpandedMode(): void {
|
|
||||||
const card = this._api.getCardElementManager().getElement();
|
|
||||||
const view = this._api.getViewManager().getView();
|
|
||||||
|
|
||||||
// When a new media loads, set the aspect ratio for when the card is
|
|
||||||
// expanded/popped-up. This is based exclusively on last media content,
|
|
||||||
// as dimension configuration does not apply in fullscreen or expanded mode.
|
|
||||||
const lastKnown = this._api.getMediaLoadedInfoManager().getLastKnown();
|
|
||||||
card.style.setProperty(
|
|
||||||
'--frigate-card-expand-aspect-ratio',
|
|
||||||
view?.isAnyMediaView() && lastKnown
|
|
||||||
? `${lastKnown.width} / ${lastKnown.height}`
|
|
||||||
: 'unset',
|
|
||||||
);
|
|
||||||
// Non-media may have no intrinsic dimensions (or multiple media items in a
|
|
||||||
// grid) and so we need to explicit request the dialog to use all available
|
|
||||||
// space.
|
|
||||||
const isGrid = view?.isGrid();
|
|
||||||
card.style.setProperty(
|
|
||||||
'--frigate-card-expand-width',
|
|
||||||
!isGrid && view?.isAnyMediaView()
|
|
||||||
? 'none'
|
|
||||||
: 'var(--frigate-card-expand-max-width)',
|
|
||||||
);
|
|
||||||
card.style.setProperty(
|
|
||||||
'--frigate-card-expand-height',
|
|
||||||
!isGrid && view?.isAnyMediaView()
|
|
||||||
? 'none'
|
|
||||||
: 'var(--frigate-card-expand-max-height)',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setMinMaxHeight(): void {
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
|
||||||
if (config) {
|
|
||||||
const card = this._api.getCardElementManager().getElement();
|
|
||||||
card.style.setProperty('--frigate-card-min-height', config.dimensions.min_height);
|
|
||||||
card.style.setProperty('--frigate-card-max-height', config.dimensions.max_height);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public setPerformance(): void {
|
|
||||||
setPerformanceCSSStyles(
|
|
||||||
this._api.getCardElementManager().getElement(),
|
|
||||||
this._api.getConfigManager().getCardWideConfig()?.performance,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _isAspectRatioEnforced(
|
|
||||||
config: FrigateCardConfig,
|
|
||||||
view?: View | null,
|
|
||||||
): boolean {
|
|
||||||
const aspectRatioMode = config.dimensions.aspect_ratio_mode;
|
|
||||||
|
|
||||||
// Do not artifically constrain aspect ratio if:
|
|
||||||
// - It's fullscreen.
|
|
||||||
// - It's in expanded mode.
|
|
||||||
// - Aspect ratio enforcement is disabled.
|
|
||||||
// - Aspect ratio enforcement is dynamic and it's a media view (i.e. not the
|
|
||||||
// gallery) or diagnostics / timeline.
|
|
||||||
return !(
|
|
||||||
this._api.getFullscreenManager().isInFullscreen() ||
|
|
||||||
this._api.getExpandManager().isExpanded() ||
|
|
||||||
aspectRatioMode === 'unconstrained' ||
|
|
||||||
(aspectRatioMode === 'dynamic' &&
|
|
||||||
(!view ||
|
|
||||||
view?.isAnyMediaView() ||
|
|
||||||
view?.is('timeline') ||
|
|
||||||
view?.is('diagnostics')))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the aspect ratio padding required to enforce the aspect ratio (if it is
|
|
||||||
* required).
|
|
||||||
* @returns A padding percentage.
|
|
||||||
*/
|
|
||||||
public getAspectRatioStyle(): string {
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
|
||||||
const view = this._api.getViewManager().getView();
|
|
||||||
|
|
||||||
if (config) {
|
|
||||||
if (!this._isAspectRatioEnforced(config, view)) {
|
|
||||||
return 'auto';
|
|
||||||
}
|
|
||||||
|
|
||||||
const aspectRatioMode = config.dimensions.aspect_ratio_mode;
|
|
||||||
|
|
||||||
const lastKnown = this._api.getMediaLoadedInfoManager().getLastKnown();
|
|
||||||
if (lastKnown && aspectRatioMode === 'dynamic') {
|
|
||||||
return `${lastKnown.width} / ${lastKnown.height}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
return `${config.dimensions.aspect_ratio[0]} / ${config.dimensions.aspect_ratio[1]}`;
|
|
||||||
}
|
|
||||||
return '16 / 9';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,126 +0,0 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
|
||||||
import orderBy from 'lodash-es/orderBy';
|
|
||||||
import { getHassDifferences, isTriggeredState } from '../ha';
|
|
||||||
import { Timer } from '../timer';
|
|
||||||
import { CardTriggersAPI } from './types';
|
|
||||||
|
|
||||||
export class TriggersManager {
|
|
||||||
protected _api: CardTriggersAPI;
|
|
||||||
|
|
||||||
protected _triggers: Map<string, Date> = new Map();
|
|
||||||
protected _untriggerTimer = new Timer();
|
|
||||||
|
|
||||||
constructor(api: CardTriggersAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public isTriggered(): boolean {
|
|
||||||
return !!this._triggers.size || this._untriggerTimer.isRunning();
|
|
||||||
}
|
|
||||||
|
|
||||||
public updateTriggeredCameras(oldHass?: HomeAssistant | null): boolean {
|
|
||||||
if (!this._shouldTrackTriggers()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
|
||||||
|
|
||||||
const now = new Date();
|
|
||||||
let triggerChanges = false;
|
|
||||||
|
|
||||||
const cameras = this._api.getCameraManager().getStore().getVisibleCameras();
|
|
||||||
for (const [cameraID, config] of cameras?.entries()) {
|
|
||||||
const triggerEntities = config.triggers.entities;
|
|
||||||
const diffs = getHassDifferences(hass, oldHass, triggerEntities, {
|
|
||||||
stateOnly: true,
|
|
||||||
});
|
|
||||||
const shouldTrigger = diffs.some((diff) => isTriggeredState(diff.newState));
|
|
||||||
const shouldUntrigger = triggerEntities.every(
|
|
||||||
(entity) => !isTriggeredState(hass?.states[entity]),
|
|
||||||
);
|
|
||||||
if (shouldTrigger) {
|
|
||||||
this._triggers.set(cameraID, now);
|
|
||||||
triggerChanges = true;
|
|
||||||
} else if (shouldUntrigger && this._triggers.has(cameraID)) {
|
|
||||||
this._triggers.delete(cameraID);
|
|
||||||
triggerChanges = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (triggerChanges) {
|
|
||||||
const targetCameraID = this._getMostRecentTrigger();
|
|
||||||
if (targetCameraID) {
|
|
||||||
this._triggerAction(targetCameraID);
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
this._startUntriggerTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public untrigger(): void {
|
|
||||||
const wasTriggered = this.isTriggered();
|
|
||||||
this._triggers.clear();
|
|
||||||
this._untriggerTimer.stop();
|
|
||||||
|
|
||||||
if (wasTriggered) {
|
|
||||||
this._untriggerAction();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _triggerAction(cameraID: string): void {
|
|
||||||
const view = this._api.getViewManager().getView();
|
|
||||||
if (
|
|
||||||
this._isAutomatedViewUpdateAllowed() &&
|
|
||||||
(view?.camera !== cameraID || !view?.is('live'))
|
|
||||||
) {
|
|
||||||
this._api.getViewManager().setViewByParameters({
|
|
||||||
viewName: 'live',
|
|
||||||
cameraID: cameraID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _untriggerAction(): void {
|
|
||||||
if (
|
|
||||||
!this.isTriggered() &&
|
|
||||||
this._isAutomatedViewUpdateAllowed() &&
|
|
||||||
this._api.getConfigManager().getConfig()?.view.scan.untrigger_reset
|
|
||||||
) {
|
|
||||||
this._api.getViewManager().setViewDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _isAutomatedViewUpdateAllowed(): boolean {
|
|
||||||
return (
|
|
||||||
this._api.getConfigManager().getConfig()?.view.update_force ||
|
|
||||||
!this._api.getInteractionManager().hasInteraction()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _shouldTrackTriggers(): boolean {
|
|
||||||
return !!this._api.getConfigManager().getConfig()?.view.scan.enabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _startUntriggerTimer(): void {
|
|
||||||
this._untriggerTimer.start(
|
|
||||||
/* istanbul ignore next: the case of config being null here cannot be
|
|
||||||
reached, as there's no way to have the untrigger call happen without
|
|
||||||
a config. -- @preserve */
|
|
||||||
this._api.getConfigManager().getConfig()?.view.scan.untrigger_seconds ?? 0,
|
|
||||||
() => {
|
|
||||||
this._untriggerAction();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _getMostRecentTrigger(): string | null {
|
|
||||||
const sorted = orderBy(
|
|
||||||
[...this._triggers.entries()],
|
|
||||||
(entry) => entry[1].getTime(),
|
|
||||||
'desc',
|
|
||||||
);
|
|
||||||
return sorted.length ? sorted[0][0] : null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
import { CameraManager } from '../../camera-manager/manager';
|
|
||||||
import { ConditionsManager } from './conditions-manager';
|
|
||||||
import { EntityRegistryManager } from '../ha/entity-registry';
|
|
||||||
import { ResolvedMediaCache } from '../ha/resolved-media';
|
|
||||||
import { ActionsManager } from './actions-manager';
|
|
||||||
import { AutoUpdateManager } from './auto-update-manager';
|
|
||||||
import { AutomationsManager } from './automations-manager';
|
|
||||||
import { CameraURLManager } from './camera-url-manager';
|
|
||||||
import { CardElementManager } from './card-element-manager';
|
|
||||||
import { ConfigManager } from './config-manager';
|
|
||||||
import { DownloadManager } from './download-manager';
|
|
||||||
import { ExpandManager } from './expand-manager';
|
|
||||||
import { FullscreenManager } from './fullscreen-manager';
|
|
||||||
import { HASSManager } from './hass-manager';
|
|
||||||
import { InitializationManager } from './initialization-manager';
|
|
||||||
import { InteractionManager } from './interaction-manager';
|
|
||||||
import { MediaLoadedInfoManager } from './media-info-manager';
|
|
||||||
import { MediaPlayerManager } from './media-player-manager';
|
|
||||||
import { MessageManager } from './message-manager';
|
|
||||||
import { MicrophoneManager } from './microphone-manager';
|
|
||||||
import { StyleManager } from './style-manager';
|
|
||||||
import { TriggersManager } from './triggers-manager';
|
|
||||||
import { ViewManager } from './view-manager';
|
|
||||||
import { QueryStringManager } from './query-string-manager';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This defines a series of limited APIs that various manager helpers use to
|
|
||||||
* control the card. Explicitly specifying them helps make coupling intentional
|
|
||||||
* and avoids cyclic importing.
|
|
||||||
*/
|
|
||||||
|
|
||||||
export interface CardActionsManagerAPI {
|
|
||||||
getCameraManager(): CameraManager;
|
|
||||||
getCameraURLManager(): CameraURLManager;
|
|
||||||
getCardElementManager(): CardElementManager;
|
|
||||||
getConfigManager(): ConfigManager;
|
|
||||||
getDownloadManager(): DownloadManager;
|
|
||||||
getExpandManager(): ExpandManager;
|
|
||||||
getFullscreenManager(): FullscreenManager;
|
|
||||||
getHASSManager(): HASSManager;
|
|
||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
|
||||||
getMediaPlayerManager(): MediaPlayerManager;
|
|
||||||
getMessageManager(): MessageManager;
|
|
||||||
getViewManager(): ViewManager;
|
|
||||||
getMicrophoneManager(): MicrophoneManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardAutomationsAPI {
|
|
||||||
getCardElementManager(): CardElementManager;
|
|
||||||
getConditionsManager(): ConditionsManager;
|
|
||||||
getConfigManager(): ConfigManager;
|
|
||||||
getHASSManager(): HASSManager;
|
|
||||||
getMessageManager(): MessageManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardAutoRefreshAPI {
|
|
||||||
getConfigManager(): ConfigManager;
|
|
||||||
getViewManager(): ViewManager;
|
|
||||||
getTriggersManager(): TriggersManager;
|
|
||||||
getInteractionManager(): InteractionManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardCameraAPI {
|
|
||||||
getConfigManager(): ConfigManager;
|
|
||||||
getEntityRegistryManager(): EntityRegistryManager;
|
|
||||||
getResolvedMediaCache(): ResolvedMediaCache;
|
|
||||||
getHASSManager(): HASSManager;
|
|
||||||
getMessageManager(): MessageManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardCameraURLAPI {
|
|
||||||
getCameraManager(): CameraManager;
|
|
||||||
getViewManager(): ViewManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardConditionAPI {
|
|
||||||
getAutomationsManager(): AutomationsManager;
|
|
||||||
getConfigManager(): ConfigManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardConfigAPI {
|
|
||||||
getAutomationsManager(): AutomationsManager;
|
|
||||||
getCardElementManager(): CardElementManager;
|
|
||||||
getConditionsManager(): ConditionsManager;
|
|
||||||
getInitializationManager(): InitializationManager;
|
|
||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
|
||||||
getMessageManager(): MessageManager;
|
|
||||||
getStyleManager(): StyleManager;
|
|
||||||
getViewManager(): ViewManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardDownloadAPI {
|
|
||||||
getCameraManager(): CameraManager;
|
|
||||||
getHASSManager(): HASSManager;
|
|
||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
|
||||||
getMessageManager(): MessageManager;
|
|
||||||
getViewManager(): ViewManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardElementAPI {
|
|
||||||
getActionsManager(): ActionsManager;
|
|
||||||
getFullscreenManager(): FullscreenManager;
|
|
||||||
getInteractionManager(): InteractionManager;
|
|
||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
|
||||||
getQueryStringManager(): QueryStringManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardExpandAPI {
|
|
||||||
getFullscreenManager(): FullscreenManager;
|
|
||||||
getConditionsManager(): ConditionsManager;
|
|
||||||
getCardElementManager(): CardElementManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardFullscreenAPI {
|
|
||||||
getCardElementManager(): CardElementManager;
|
|
||||||
getExpandManager(): ExpandManager;
|
|
||||||
getConditionsManager(): ConditionsManager;
|
|
||||||
getMediaPlayerManager(): MediaPlayerManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardHASSAPI {
|
|
||||||
getCameraManager(): CameraManager;
|
|
||||||
getCardElementManager(): CardElementManager;
|
|
||||||
getConditionsManager(): ConditionsManager;
|
|
||||||
getConfigManager(): ConfigManager;
|
|
||||||
getInteractionManager(): InteractionManager;
|
|
||||||
getMediaPlayerManager(): MediaPlayerManager;
|
|
||||||
getMessageManager(): MessageManager;
|
|
||||||
getStyleManager(): StyleManager;
|
|
||||||
getTriggersManager(): TriggersManager;
|
|
||||||
getViewManager(): ViewManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardInitializerAPI {
|
|
||||||
getCameraManager(): CameraManager;
|
|
||||||
getCardElementManager(): CardElementManager;
|
|
||||||
getConfigManager(): ConfigManager;
|
|
||||||
getEntityRegistryManager(): EntityRegistryManager;
|
|
||||||
getHASSManager(): HASSManager;
|
|
||||||
getMediaPlayerManager(): MediaPlayerManager;
|
|
||||||
getMessageManager(): MessageManager;
|
|
||||||
getMicrophoneManager(): MicrophoneManager;
|
|
||||||
getQueryStringManager(): QueryStringManager;
|
|
||||||
getResolvedMediaCache(): ResolvedMediaCache;
|
|
||||||
getViewManager(): ViewManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardInteractionAPI {
|
|
||||||
getConfigManager(): ConfigManager;
|
|
||||||
getStyleManager(): StyleManager;
|
|
||||||
getTriggersManager(): TriggersManager;
|
|
||||||
getViewManager(): ViewManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardMediaLoadedAPI {
|
|
||||||
getConditionsManager(): ConditionsManager;
|
|
||||||
getConfigManager(): ConfigManager;
|
|
||||||
getCardElementManager(): CardElementManager;
|
|
||||||
getStyleManager(): StyleManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardMediaPlayerAPI {
|
|
||||||
getHASSManager(): HASSManager;
|
|
||||||
getCameraManager(): CameraManager;
|
|
||||||
getEntityRegistryManager(): EntityRegistryManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardMessageAPI {
|
|
||||||
getConditionsManager(): ConditionsManager;
|
|
||||||
getCardElementManager(): CardElementManager;
|
|
||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardMicrophoneAPI {
|
|
||||||
getCardElementManager(): CardElementManager;
|
|
||||||
getConfigManager(): ConfigManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardQueryStringAPI {
|
|
||||||
getCardElementManager(): CardElementManager;
|
|
||||||
getViewManager(): ViewManager;
|
|
||||||
getActionsManager(): ActionsManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardStyleAPI {
|
|
||||||
getCardElementManager(): CardElementManager;
|
|
||||||
getConfigManager(): ConfigManager;
|
|
||||||
getExpandManager(): ExpandManager;
|
|
||||||
getFullscreenManager(): FullscreenManager;
|
|
||||||
getHASSManager(): HASSManager;
|
|
||||||
getInteractionManager(): InteractionManager;
|
|
||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
|
||||||
getViewManager(): ViewManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardTriggersAPI {
|
|
||||||
getCameraManager(): CameraManager;
|
|
||||||
getConfigManager(): ConfigManager;
|
|
||||||
getHASSManager(): HASSManager;
|
|
||||||
getInteractionManager(): InteractionManager;
|
|
||||||
getViewManager(): ViewManager;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CardViewAPI {
|
|
||||||
getAutoUpdateManager(): AutoUpdateManager;
|
|
||||||
getCameraManager(): CameraManager;
|
|
||||||
getConfigManager(): ConfigManager;
|
|
||||||
getHASSManager(): HASSManager;
|
|
||||||
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
|
||||||
getMessageManager(): MessageManager;
|
|
||||||
getStyleManager(): StyleManager;
|
|
||||||
getConditionsManager(): ConditionsManager;
|
|
||||||
getCardElementManager(): CardElementManager;
|
|
||||||
}
|
|
||||||
@@ -1,286 +0,0 @@
|
|||||||
import { ViewContext } from 'view';
|
|
||||||
import { FrigateCardConfig, FrigateCardView, ViewDisplayMode } from '../../config/types';
|
|
||||||
import { View } from '../../view/view';
|
|
||||||
import { getAllDependentCameras } from '../camera';
|
|
||||||
import { log } from '../debug';
|
|
||||||
import { executeMediaQueryForView } from '../media-to-view';
|
|
||||||
import { CardViewAPI } from './types';
|
|
||||||
|
|
||||||
interface ViewManagerSetViewDefaultParameters {
|
|
||||||
cameraID?: string;
|
|
||||||
substream?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ViewManagerSetViewParameters
|
|
||||||
extends ViewManagerSetViewDefaultParameters {
|
|
||||||
viewName?: FrigateCardView;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ViewManager {
|
|
||||||
protected _view: View | null = null;
|
|
||||||
protected _api: CardViewAPI;
|
|
||||||
|
|
||||||
constructor(api: CardViewAPI) {
|
|
||||||
this._api = api;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getView(): View | null {
|
|
||||||
return this._view;
|
|
||||||
}
|
|
||||||
|
|
||||||
public setView(view: View): void {
|
|
||||||
this._setView(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setViewDefault(params?: ViewManagerSetViewDefaultParameters): void {
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
|
||||||
if (config) {
|
|
||||||
let forceCameraID: string | null = params?.cameraID ?? null;
|
|
||||||
if (!forceCameraID && this._view?.camera && config.view.update_cycle_camera) {
|
|
||||||
const cameraIDs = [
|
|
||||||
...this._api.getCameraManager().getStore().getVisibleCameraIDs(),
|
|
||||||
];
|
|
||||||
const currentIndex = cameraIDs.indexOf(this._view.camera);
|
|
||||||
const targetIndex = currentIndex + 1 >= cameraIDs.length ? 0 : currentIndex + 1;
|
|
||||||
forceCameraID = cameraIDs[targetIndex];
|
|
||||||
}
|
|
||||||
|
|
||||||
this.setViewByParameters({
|
|
||||||
...params,
|
|
||||||
viewName: config.view.default,
|
|
||||||
...(forceCameraID && { cameraID: forceCameraID }),
|
|
||||||
});
|
|
||||||
|
|
||||||
// Restart the refresh timer, so the default view is refreshed at a fixed
|
|
||||||
// interval from now (if so configured).
|
|
||||||
this._api.getAutoUpdateManager().startDefaultViewTimer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public setViewByParameters(params: ViewManagerSetViewParameters): void {
|
|
||||||
const config = this._api.getConfigManager().getConfig();
|
|
||||||
|
|
||||||
if (config) {
|
|
||||||
let cameraID: string | null = null;
|
|
||||||
|
|
||||||
const cameras = this._api.getCameraManager().getStore().getVisibleCameraIDs();
|
|
||||||
if (cameras.size) {
|
|
||||||
if (params?.cameraID && cameras.has(params.cameraID)) {
|
|
||||||
cameraID = params.cameraID;
|
|
||||||
} else {
|
|
||||||
// Reset to the default camera.
|
|
||||||
cameraID = cameras.keys().next().value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const viewName = params?.viewName ?? this._view?.view ?? config.view.default;
|
|
||||||
if (cameraID && viewName && this.isViewSupportedByCamera(cameraID, viewName)) {
|
|
||||||
const displayMode =
|
|
||||||
this._view?.displayMode ??
|
|
||||||
this._getDefaultDisplayModeForView(viewName, config);
|
|
||||||
let view: View = new View({
|
|
||||||
view: viewName,
|
|
||||||
camera: cameraID,
|
|
||||||
displayMode: displayMode,
|
|
||||||
});
|
|
||||||
if (params.substream) {
|
|
||||||
view = this._createViewWithSelectedSubstream(view, params.substream);
|
|
||||||
}
|
|
||||||
this._setView(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public setViewWithNewContext(context: ViewContext): void {
|
|
||||||
if (this._view) {
|
|
||||||
return this._setView(this._view?.clone().mergeInContext(context));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public reset(): void {
|
|
||||||
this._view = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async setViewWithNewDisplayMode(displayMode: ViewDisplayMode): Promise<void> {
|
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
|
||||||
|
|
||||||
if (this._view && hass) {
|
|
||||||
const view = this._view.evolve({
|
|
||||||
displayMode: displayMode,
|
|
||||||
});
|
|
||||||
|
|
||||||
const cameraCount = this._api
|
|
||||||
.getCameraManager()
|
|
||||||
.getStore()
|
|
||||||
.getVisibleCameraCount();
|
|
||||||
const queryCameraCount = view.query?.getQueryCameraIDs()?.size ?? 0;
|
|
||||||
const generateNewQuery =
|
|
||||||
view?.query &&
|
|
||||||
queryCameraCount &&
|
|
||||||
((view.isGrid() && queryCameraCount < cameraCount) ||
|
|
||||||
(!view.isGrid() && queryCameraCount > 1));
|
|
||||||
|
|
||||||
if (generateNewQuery && view && view.query) {
|
|
||||||
// If the user requests a grid but the current query does not have a
|
|
||||||
// query for more than one camera, reset the query results, change the
|
|
||||||
// existing query to refer to all cameras and execute it to fetch new
|
|
||||||
// results.
|
|
||||||
let viewWithNewQuery: View | null = null;
|
|
||||||
try {
|
|
||||||
viewWithNewQuery = await executeMediaQueryForView(
|
|
||||||
this._api.getCameraManager(),
|
|
||||||
view,
|
|
||||||
view.query
|
|
||||||
.clone()
|
|
||||||
.setQueryCameraIDs(
|
|
||||||
view.isGrid()
|
|
||||||
? this._api.getCameraManager().getStore().getVisibleCameraIDs()
|
|
||||||
: view.camera,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
} catch (e: unknown) {
|
|
||||||
this._api.getMessageManager().setErrorIfHigherPriority(e);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (viewWithNewQuery) {
|
|
||||||
return this._setView(viewWithNewQuery);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return this._setView(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public setViewWithSubstream(substream?: string): void {
|
|
||||||
if (!this._view) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._setView(
|
|
||||||
substream
|
|
||||||
? this._createViewWithSelectedSubstream(this._view, substream)
|
|
||||||
: this._createViewWithNextStream(this._view),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setViewWithoutSubstream(): void {
|
|
||||||
const view = this._createViewWithoutSubstream();
|
|
||||||
if (view) {
|
|
||||||
return this._setView(view);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public isViewSupportedByCamera(cameraID: string, view: FrigateCardView): boolean {
|
|
||||||
const capabilities = this._api.getCameraManager().getCameraCapabilities(cameraID);
|
|
||||||
switch (view) {
|
|
||||||
case 'live':
|
|
||||||
case 'image':
|
|
||||||
case 'diagnostics':
|
|
||||||
return true;
|
|
||||||
case 'clip':
|
|
||||||
case 'clips':
|
|
||||||
return !!capabilities?.supportsClips;
|
|
||||||
case 'snapshot':
|
|
||||||
case 'snapshots':
|
|
||||||
return !!capabilities?.supportsSnapshots;
|
|
||||||
case 'recording':
|
|
||||||
case 'recordings':
|
|
||||||
return !!capabilities?.supportsRecordings;
|
|
||||||
case 'timeline':
|
|
||||||
return !!capabilities?.supportsTimeline;
|
|
||||||
case 'media':
|
|
||||||
return (
|
|
||||||
!!capabilities?.supportsClips ||
|
|
||||||
!!capabilities?.supportsSnapshots ||
|
|
||||||
!!capabilities?.supportsRecordings
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _getDefaultDisplayModeForView(
|
|
||||||
viewName: FrigateCardView,
|
|
||||||
config?: FrigateCardConfig,
|
|
||||||
): ViewDisplayMode {
|
|
||||||
let mode: ViewDisplayMode | null = null;
|
|
||||||
switch (viewName) {
|
|
||||||
case 'media':
|
|
||||||
case 'clip':
|
|
||||||
case 'recording':
|
|
||||||
case 'snapshot':
|
|
||||||
mode = config?.media_viewer.display?.mode ?? null;
|
|
||||||
break;
|
|
||||||
case 'live':
|
|
||||||
mode = config?.live.display?.mode ?? null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return mode ?? 'single';
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _setView(view: View): void {
|
|
||||||
const oldView = this._view;
|
|
||||||
View.adoptFromViewIfAppropriate(view, oldView);
|
|
||||||
|
|
||||||
log(
|
|
||||||
this._api.getConfigManager().getCardWideConfig(),
|
|
||||||
`Frigate Card view change: `,
|
|
||||||
view.view,
|
|
||||||
);
|
|
||||||
this._view = view;
|
|
||||||
|
|
||||||
if (View.isMajorMediaChange(oldView, view)) {
|
|
||||||
this._api.getMediaLoadedInfoManager().clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (oldView?.view !== view.view) {
|
|
||||||
this._api.getCardElementManager().scrollReset();
|
|
||||||
}
|
|
||||||
|
|
||||||
this._api.getMessageManager().reset();
|
|
||||||
this._api.getStyleManager().setExpandedMode();
|
|
||||||
|
|
||||||
this._api.getConditionsManager()?.setState({
|
|
||||||
view: view.view,
|
|
||||||
camera: view.camera,
|
|
||||||
displayMode: view.displayMode ?? undefined,
|
|
||||||
});
|
|
||||||
this._api.getCardElementManager().update();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _createViewWithSelectedSubstream(baseView: View, substreamID: string): View {
|
|
||||||
const overrides: Map<string, string> =
|
|
||||||
baseView?.context?.live?.overrides ?? new Map();
|
|
||||||
overrides.set(baseView.camera, substreamID);
|
|
||||||
return baseView.clone().mergeInContext({
|
|
||||||
live: { overrides: overrides },
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _createViewWithNextStream(baseView: View): View {
|
|
||||||
const dependencies = [
|
|
||||||
...getAllDependentCameras(this._api.getCameraManager(), baseView.camera),
|
|
||||||
];
|
|
||||||
if (dependencies.length <= 1) {
|
|
||||||
return baseView.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
const view = baseView.clone();
|
|
||||||
const overrides: Map<string, string> = view.context?.live?.overrides ?? new Map();
|
|
||||||
const currentOverride = overrides.get(view.camera) ?? view.camera;
|
|
||||||
const currentIndex = dependencies.indexOf(currentOverride);
|
|
||||||
const newIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % dependencies.length;
|
|
||||||
overrides.set(view.camera, dependencies[newIndex]);
|
|
||||||
view.mergeInContext({ live: { overrides: overrides } });
|
|
||||||
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected _createViewWithoutSubstream(): View | null {
|
|
||||||
if (!this._view) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const view = this._view.clone();
|
|
||||||
const overrides: Map<string, string> | undefined = view.context?.live?.overrides;
|
|
||||||
if (overrides && overrides.has(view.camera)) {
|
|
||||||
view.context?.live?.overrides?.delete(view.camera);
|
|
||||||
}
|
|
||||||
return view;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,8 +15,8 @@ import {
|
|||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
import { createFrigateCardCustomAction } from './action';
|
import { createFrigateCardCustomAction } from './action';
|
||||||
import { getAllDependentCameras } from './camera';
|
import { getAllDependentCameras } from './camera';
|
||||||
import { MediaPlayerManager } from './card-controller/media-player-manager';
|
import { MediaPlayerManager } from '../card-controller/media-player-manager';
|
||||||
import { MicrophoneManager } from './card-controller/microphone-manager';
|
import { MicrophoneManager } from '../card-controller/microphone-manager';
|
||||||
import { getEntityIcon, getEntityTitle } from './ha';
|
import { getEntityIcon, getEntityTitle } from './ha';
|
||||||
import { hasSubstream } from './substream';
|
import { hasSubstream } from './substream';
|
||||||
export interface MenuButtonControllerOptions {
|
export interface MenuButtonControllerOptions {
|
||||||
|
|||||||
+22
-22
@@ -22,28 +22,28 @@ import {
|
|||||||
performanceConfigSchema,
|
performanceConfigSchema,
|
||||||
} from '../src/config/types';
|
} from '../src/config/types';
|
||||||
import { ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types';
|
import { ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types';
|
||||||
import { ActionsManager } from '../src/utils/card-controller/actions-manager';
|
import { ActionsManager } from '../src/card-controller/actions-manager';
|
||||||
import { AutoUpdateManager } from '../src/utils/card-controller/auto-update-manager';
|
import { AutoUpdateManager } from '../src/card-controller/auto-update-manager';
|
||||||
import { AutomationsManager } from '../src/utils/card-controller/automations-manager';
|
import { AutomationsManager } from '../src/card-controller/automations-manager';
|
||||||
import { CameraURLManager } from '../src/utils/card-controller/camera-url-manager';
|
import { CameraURLManager } from '../src/card-controller/camera-url-manager';
|
||||||
import { CardElementManager } from '../src/utils/card-controller/card-element-manager';
|
import { CardElementManager } from '../src/card-controller/card-element-manager';
|
||||||
import { ConditionsManager } from '../src/utils/card-controller/conditions-manager';
|
import { ConditionsManager } from '../src/card-controller/conditions-manager';
|
||||||
import { ConfigManager } from '../src/utils/card-controller/config-manager';
|
import { ConfigManager } from '../src/card-controller/config-manager';
|
||||||
import { CardController } from '../src/utils/card-controller/controller';
|
import { CardController } from '../src/card-controller/controller';
|
||||||
import { DownloadManager } from '../src/utils/card-controller/download-manager';
|
import { DownloadManager } from '../src/card-controller/download-manager';
|
||||||
import { ExpandManager } from '../src/utils/card-controller/expand-manager';
|
import { ExpandManager } from '../src/card-controller/expand-manager';
|
||||||
import { FullscreenManager } from '../src/utils/card-controller/fullscreen-manager';
|
import { FullscreenManager } from '../src/card-controller/fullscreen-manager';
|
||||||
import { HASSManager } from '../src/utils/card-controller/hass-manager';
|
import { HASSManager } from '../src/card-controller/hass-manager';
|
||||||
import { InitializationManager } from '../src/utils/card-controller/initialization-manager';
|
import { InitializationManager } from '../src/card-controller/initialization-manager';
|
||||||
import { InteractionManager } from '../src/utils/card-controller/interaction-manager';
|
import { InteractionManager } from '../src/card-controller/interaction-manager';
|
||||||
import { MediaLoadedInfoManager } from '../src/utils/card-controller/media-info-manager';
|
import { MediaLoadedInfoManager } from '../src/card-controller/media-info-manager';
|
||||||
import { MediaPlayerManager } from '../src/utils/card-controller/media-player-manager';
|
import { MediaPlayerManager } from '../src/card-controller/media-player-manager';
|
||||||
import { MessageManager } from '../src/utils/card-controller/message-manager';
|
import { MessageManager } from '../src/card-controller/message-manager';
|
||||||
import { MicrophoneManager } from '../src/utils/card-controller/microphone-manager';
|
import { MicrophoneManager } from '../src/card-controller/microphone-manager';
|
||||||
import { QueryStringManager } from '../src/utils/card-controller/query-string-manager';
|
import { QueryStringManager } from '../src/card-controller/query-string-manager';
|
||||||
import { StyleManager } from '../src/utils/card-controller/style-manager';
|
import { StyleManager } from '../src/card-controller/style-manager';
|
||||||
import { TriggersManager } from '../src/utils/card-controller/triggers-manager';
|
import { TriggersManager } from '../src/card-controller/triggers-manager';
|
||||||
import { ViewManager } from '../src/utils/card-controller/view-manager';
|
import { ViewManager } from '../src/card-controller/view-manager';
|
||||||
import { Entity } from '../src/utils/ha/entity-registry/types';
|
import { Entity } from '../src/utils/ha/entity-registry/types';
|
||||||
import { ViewMedia, ViewMediaType } from '../src/view/media';
|
import { ViewMedia, ViewMediaType } from '../src/view/media';
|
||||||
import { MediaQueriesResults } from '../src/view/media-queries-results';
|
import { MediaQueriesResults } from '../src/view/media-queries-results';
|
||||||
|
|||||||
@@ -1,728 +0,0 @@
|
|||||||
import { afterAll, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
import {
|
|
||||||
ActionType,
|
|
||||||
FrigateCardCustomAction,
|
|
||||||
FrigateCardView,
|
|
||||||
frigateCardCustomActionSchema,
|
|
||||||
} from '../../../src/config/types';
|
|
||||||
import { FrigateCardMediaPlayer } from '../../../src/types';
|
|
||||||
import {
|
|
||||||
convertActionToFrigateCardCustomAction,
|
|
||||||
frigateCardHandleActionConfig,
|
|
||||||
getActionConfigGivenAction,
|
|
||||||
} from '../../../src/utils/action.js';
|
|
||||||
import { ActionsManager } from '../../../src/utils/card-controller/actions-manager';
|
|
||||||
import {
|
|
||||||
createCardAPI,
|
|
||||||
createConfig,
|
|
||||||
createHASS,
|
|
||||||
createMediaLoadedInfo,
|
|
||||||
createView,
|
|
||||||
createViewWithMedia,
|
|
||||||
} from '../../test-utils';
|
|
||||||
|
|
||||||
vi.mock('../../../src/utils/action.js');
|
|
||||||
vi.mock('../../../src/camera-manager/manager.js');
|
|
||||||
|
|
||||||
const createAction = (
|
|
||||||
action: Record<string, unknown>,
|
|
||||||
): FrigateCardCustomAction | null => {
|
|
||||||
const result = frigateCardCustomActionSchema.safeParse({
|
|
||||||
action: 'custom:frigate-card-action',
|
|
||||||
...action,
|
|
||||||
});
|
|
||||||
return result.success ? result.data : null;
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('ActionsManager.getMergedActions', () => {
|
|
||||||
const config = {
|
|
||||||
view: {
|
|
||||||
actions: {
|
|
||||||
tap_action: {
|
|
||||||
action: 'navigate',
|
|
||||||
navigation_path: '1',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
live: {
|
|
||||||
actions: {
|
|
||||||
tap_action: {
|
|
||||||
action: 'navigate',
|
|
||||||
navigation_path: '2',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
media_gallery: {
|
|
||||||
actions: {
|
|
||||||
tap_action: {
|
|
||||||
action: 'navigate',
|
|
||||||
navigation_path: '3',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
media_viewer: {
|
|
||||||
actions: {
|
|
||||||
tap_action: {
|
|
||||||
action: 'navigate',
|
|
||||||
navigation_path: '4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
image: {
|
|
||||||
actions: {
|
|
||||||
tap_action: {
|
|
||||||
action: 'navigate',
|
|
||||||
navigation_path: '5',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
afterAll(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get no merged actions with a message', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
||||||
createView({ view: 'live' }),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true);
|
|
||||||
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
expect(manager.getMergedActions()).toEqual({});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should get merged actions with live view', () => {
|
|
||||||
it.each([
|
|
||||||
[
|
|
||||||
'live' as const,
|
|
||||||
{
|
|
||||||
tap_action: {
|
|
||||||
action: 'navigate',
|
|
||||||
navigation_path: '2',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'clips' as const,
|
|
||||||
{
|
|
||||||
tap_action: {
|
|
||||||
action: 'navigate',
|
|
||||||
navigation_path: '3',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'clip' as const,
|
|
||||||
{
|
|
||||||
tap_action: {
|
|
||||||
action: 'navigate',
|
|
||||||
navigation_path: '4',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
[
|
|
||||||
'image' as const,
|
|
||||||
{
|
|
||||||
tap_action: {
|
|
||||||
action: 'navigate',
|
|
||||||
navigation_path: '5',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
['timeline' as const, {}],
|
|
||||||
])('%s', (viewName: FrigateCardView, result: Record<string, unknown>) => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
||||||
createView({ view: viewName }),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config));
|
|
||||||
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
expect(manager.getMergedActions()).toEqual(result);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('ActionsManager.handleInteraction', () => {
|
|
||||||
it('should handle interaction', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
const hass = createHASS();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
||||||
|
|
||||||
const actionForThisInteraction: ActionType = {
|
|
||||||
action: 'none',
|
|
||||||
};
|
|
||||||
vi.mocked(getActionConfigGivenAction).mockReturnValue(actionForThisInteraction);
|
|
||||||
|
|
||||||
manager.handleInteraction('tap');
|
|
||||||
|
|
||||||
expect(frigateCardHandleActionConfig).toBeCalledWith(
|
|
||||||
element,
|
|
||||||
hass,
|
|
||||||
manager.getMergedActions(),
|
|
||||||
'tap',
|
|
||||||
actionForThisInteraction,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not handle interaction', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
|
||||||
|
|
||||||
// No values of hass.
|
|
||||||
manager.handleInteraction('tap');
|
|
||||||
expect(frigateCardHandleActionConfig).not.toBeCalledWith();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ActionsManager.handleActionEvent', () => {
|
|
||||||
it('should handle event', () => {
|
|
||||||
const action = createAction({ frigate_card_action: 'default' })!;
|
|
||||||
const event: CustomEvent<FrigateCardCustomAction> = new CustomEvent('ll-custom', {
|
|
||||||
detail: action,
|
|
||||||
});
|
|
||||||
|
|
||||||
// The file containing convertActionToFrigateCardCustomAction (action.ts) is
|
|
||||||
// mocked, so need to provide a value here.
|
|
||||||
vi.mocked(convertActionToFrigateCardCustomAction).mockReturnValue(action);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
manager.handleActionEvent(event);
|
|
||||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not handle event without detail', () => {
|
|
||||||
const action = createAction({ frigate_card_action: 'default' })!;
|
|
||||||
const event = new Event('ll-custom');
|
|
||||||
|
|
||||||
// Mock this out just so that if the sentinel in handleActionEvent failed,
|
|
||||||
// it would still trigger a test failure below.
|
|
||||||
vi.mocked(convertActionToFrigateCardCustomAction).mockReturnValue(action);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
manager.handleActionEvent(event);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not handle malformed action', () => {
|
|
||||||
const action = createAction({ frigate_card_action: 'default' })!;
|
|
||||||
const event: CustomEvent<FrigateCardCustomAction> = new CustomEvent('ll-custom', {
|
|
||||||
detail: action,
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mocked(convertActionToFrigateCardCustomAction).mockReturnValue(null);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
manager.handleActionEvent(event);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ActionsManager.executeAction', () => {
|
|
||||||
it('should not handle actions with different card_id', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
card_id: 'foo',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
card_id: 'NOT_foo',
|
|
||||||
frigate_card_action: 'default',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle default action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'default',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should handle view action', async () => {
|
|
||||||
it.each([
|
|
||||||
['clip' as const],
|
|
||||||
['clips' as const],
|
|
||||||
['image' as const],
|
|
||||||
['live' as const],
|
|
||||||
['recording' as const],
|
|
||||||
['recordings' as const],
|
|
||||||
['snapshot' as const],
|
|
||||||
['snapshots' as const],
|
|
||||||
['timeline' as const],
|
|
||||||
])('%s', async (viewName: FrigateCardView) => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: viewName,
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
viewName: viewName,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle download action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'download',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getDownloadManager().downloadViewerMedia).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle camera ui action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'camera_ui',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getCameraURLManager().openURL).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle expand action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'expand',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getExpandManager().toggleExpanded).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle fullscreen action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'fullscreen',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getFullscreenManager().toggleFullscreen).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle menu toggle action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'menu_toggle',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getCardElementManager().toggleMenu).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should handle camera_select action', () => {
|
|
||||||
it('with valid camera and view', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
|
|
||||||
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'camera_select',
|
|
||||||
camera: 'camera',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
viewName: 'live',
|
|
||||||
cameraID: 'camera',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('without config', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(null);
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
||||||
createView({
|
|
||||||
view: 'timeline',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'camera_select',
|
|
||||||
camera: 'camera',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
viewName: 'timeline',
|
|
||||||
cameraID: 'camera',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with target view', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
// Change to clips view when the camera changes.
|
|
||||||
camera_select: 'clips',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
||||||
createView({
|
|
||||||
view: 'live',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'camera_select',
|
|
||||||
camera: 'camera',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
viewName: 'clips',
|
|
||||||
cameraID: 'camera',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('without a current view', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'camera_select',
|
|
||||||
camera: 'camera',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with an unsupported view', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
||||||
createView({
|
|
||||||
view: 'timeline',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'camera_select',
|
|
||||||
camera: 'camera',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
// Should have fallen back to the default view.
|
|
||||||
viewName: 'live',
|
|
||||||
cameraID: 'camera',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle live_substream_select action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'live_substream_select',
|
|
||||||
camera: 'substream',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewWithSubstream).toBeCalledWith('substream');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle live_substream_off action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'live_substream_off',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewWithoutSubstream).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle live_substream_on action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'live_substream_on',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewWithSubstream).toBeCalledWith();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should handle media_player action', () => {
|
|
||||||
it('to stop', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'media_player',
|
|
||||||
media_player_action: 'stop',
|
|
||||||
media_player: 'this_is_a_media_player',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getMediaPlayerManager().stop).toBeCalledWith('this_is_a_media_player');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('to play live', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
||||||
createView({
|
|
||||||
camera: 'camera',
|
|
||||||
view: 'live',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'media_player',
|
|
||||||
media_player_action: 'play',
|
|
||||||
media_player: 'this_is_a_media_player',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getMediaPlayerManager().playLive).toBeCalledWith(
|
|
||||||
'this_is_a_media_player',
|
|
||||||
'camera',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('to play media', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const view = createViewWithMedia({
|
|
||||||
camera: 'camera',
|
|
||||||
view: 'media',
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'media_player',
|
|
||||||
media_player_action: 'play',
|
|
||||||
media_player: 'this_is_a_media_player',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getMediaPlayerManager().playMedia).toBeCalledWith(
|
|
||||||
'this_is_a_media_player',
|
|
||||||
view.queryResults?.getSelectedResult(),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('to play media without selected media', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
||||||
createView({
|
|
||||||
view: 'media',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'media_player',
|
|
||||||
media_player_action: 'play',
|
|
||||||
media_player: 'this_is_a_media_player',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getMediaPlayerManager().playMedia).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle diagnostics action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'diagnostics',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
viewName: 'diagnostics',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle microphone_mute action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'microphone_mute',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getMicrophoneManager().mute).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle microphone_unmute action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'microphone_unmute',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getMicrophoneManager().unmute).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should handle media player action', () => {
|
|
||||||
it.each([
|
|
||||||
['mute' as const],
|
|
||||||
['unmute' as const],
|
|
||||||
['play' as const],
|
|
||||||
['pause' as const],
|
|
||||||
])('%s', async (action: 'mute' | 'unmute' | 'play' | 'pause') => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const player = mock<FrigateCardMediaPlayer>();
|
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
|
||||||
createMediaLoadedInfo({
|
|
||||||
player: player,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: action,
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(player[action]).toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle screenshot action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'screenshot',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getDownloadManager().downloadScreenshot).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle display_mode_select action', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ActionsManager(api);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
createAction({
|
|
||||||
frigate_card_action: 'display_mode_select',
|
|
||||||
display_mode: 'grid',
|
|
||||||
})!,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewWithNewDisplayMode).toBeCalledWith('grid');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle unknown action', async () => {
|
|
||||||
const manager = new ActionsManager(createCardAPI());
|
|
||||||
|
|
||||||
const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true);
|
|
||||||
|
|
||||||
await manager.executeAction(
|
|
||||||
// Have to manually create the action (vs using `createAction()`) since
|
|
||||||
// it's malformed.
|
|
||||||
{
|
|
||||||
frigate_card_action: 'not_a_real_action',
|
|
||||||
} as unknown as FrigateCardCustomAction,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(spy).toBeCalledWith(
|
|
||||||
'Frigate card received unknown card action: not_a_real_action',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,73 +0,0 @@
|
|||||||
import add from 'date-fns/add';
|
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { AutoUpdateManager } from '../../../src/utils/card-controller/auto-update-manager';
|
|
||||||
import { createCardAPI, createConfig } from '../../test-utils';
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('AutoUpdateManager', () => {
|
|
||||||
const start = new Date('2023-09-23T19:12:00');
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set default view when allowed', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
update_seconds: 10,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
// Card is triggered.
|
|
||||||
vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(true);
|
|
||||||
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
|
|
||||||
|
|
||||||
vi.useFakeTimers();
|
|
||||||
vi.setSystemTime(start);
|
|
||||||
|
|
||||||
const manager = new AutoUpdateManager(api);
|
|
||||||
manager.startDefaultViewTimer();
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
|
|
||||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
|
||||||
vi.runOnlyPendingTimers();
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
|
|
||||||
vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(false);
|
|
||||||
|
|
||||||
vi.setSystemTime(add(start, { seconds: 20 }));
|
|
||||||
vi.runOnlyPendingTimers();
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set default view when not configured', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
update_seconds: 0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(false);
|
|
||||||
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
|
|
||||||
|
|
||||||
vi.useFakeTimers();
|
|
||||||
vi.setSystemTime(start);
|
|
||||||
|
|
||||||
const manager = new AutoUpdateManager(api);
|
|
||||||
manager.startDefaultViewTimer();
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
|
|
||||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
|
||||||
vi.runOnlyPendingTimers();
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { frigateCardHandleAction } from '../../../src/utils/action.js';
|
|
||||||
import {
|
|
||||||
AutomationsManager,
|
|
||||||
} from '../../../src/utils/card-controller/automations-manager';
|
|
||||||
import { createCardAPI, createConfig, createHASS } from '../../test-utils';
|
|
||||||
|
|
||||||
vi.mock('../../../src/utils/action.js');
|
|
||||||
|
|
||||||
describe('AutomationsManager', () => {
|
|
||||||
const actions = [
|
|
||||||
{
|
|
||||||
action: 'custom:frigate-card-action',
|
|
||||||
frigate_card_action: 'clips',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const conditions = { fullscreen: true };
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing without hass', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
|
|
||||||
const automationsManager = new AutomationsManager(api);
|
|
||||||
automationsManager.execute();
|
|
||||||
expect(frigateCardHandleAction).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing without automations', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
|
|
||||||
const automationsManager = new AutomationsManager(api);
|
|
||||||
automationsManager.setAutomationsFromConfig();
|
|
||||||
automationsManager.execute();
|
|
||||||
expect(frigateCardHandleAction).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should execute actions', () => {
|
|
||||||
const config = createConfig({
|
|
||||||
automations: [
|
|
||||||
{
|
|
||||||
conditions: conditions,
|
|
||||||
actions: actions,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(config);
|
|
||||||
|
|
||||||
const automationsManager = new AutomationsManager(api);
|
|
||||||
automationsManager.setAutomationsFromConfig();
|
|
||||||
|
|
||||||
automationsManager.execute();
|
|
||||||
expect(frigateCardHandleAction).not.toBeCalled();
|
|
||||||
|
|
||||||
vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(true);
|
|
||||||
|
|
||||||
automationsManager.execute();
|
|
||||||
expect(frigateCardHandleAction).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
// Automation will not re-fire when condition continues to evaluate the
|
|
||||||
// same.
|
|
||||||
automationsManager.execute();
|
|
||||||
expect(frigateCardHandleAction).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(false);
|
|
||||||
|
|
||||||
automationsManager.execute();
|
|
||||||
expect(frigateCardHandleAction).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(true);
|
|
||||||
|
|
||||||
automationsManager.execute();
|
|
||||||
expect(frigateCardHandleAction).toBeCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should execute actions_not', () => {
|
|
||||||
const config = createConfig({
|
|
||||||
automations: [
|
|
||||||
{
|
|
||||||
conditions: conditions,
|
|
||||||
actions_not: actions,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(config);
|
|
||||||
vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(false);
|
|
||||||
|
|
||||||
const automationsManager = new AutomationsManager(api);
|
|
||||||
automationsManager.setAutomationsFromConfig();
|
|
||||||
|
|
||||||
automationsManager.execute();
|
|
||||||
|
|
||||||
expect(frigateCardHandleAction).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should prevent automation loops', () => {
|
|
||||||
const config = createConfig({
|
|
||||||
automations: [
|
|
||||||
{
|
|
||||||
conditions: { fullscreen: true },
|
|
||||||
actions: actions,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
conditions: { fullscreen: true },
|
|
||||||
actions_not: actions,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(config);
|
|
||||||
|
|
||||||
const automationsManager = new AutomationsManager(api);
|
|
||||||
automationsManager.setAutomationsFromConfig();
|
|
||||||
|
|
||||||
// Create a setup where one automation action causes another...
|
|
||||||
let evaluation = true;
|
|
||||||
vi.mocked(frigateCardHandleAction).mockImplementation(() => {
|
|
||||||
evaluation = !evaluation;
|
|
||||||
vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(
|
|
||||||
evaluation,
|
|
||||||
);
|
|
||||||
automationsManager.execute();
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(evaluation);
|
|
||||||
|
|
||||||
automationsManager.execute();
|
|
||||||
|
|
||||||
expect(api.getMessageManager().setMessageIfHigherPriority).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'error',
|
|
||||||
message:
|
|
||||||
'Too many nested automation calls, please check your configuration for loops',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(frigateCardHandleAction).toBeCalledTimes(10);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { CameraEndpoint } from '../../../src/camera-manager/types';
|
|
||||||
import { CameraURLManager } from '../../../src/utils/card-controller/camera-url-manager';
|
|
||||||
import {
|
|
||||||
CardCameraURLAPI
|
|
||||||
} from '../../../src/utils/card-controller/types';
|
|
||||||
import { createCardAPI, createViewWithMedia } from '../../test-utils';
|
|
||||||
|
|
||||||
const createAPIWithMedia = (): CardCameraURLAPI => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
||||||
createViewWithMedia()
|
|
||||||
)
|
|
||||||
return api;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('CameraURLManager', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get URL', () => {
|
|
||||||
const api = createAPIWithMedia();
|
|
||||||
const manager = new CameraURLManager(api);
|
|
||||||
|
|
||||||
const endpoint: CameraEndpoint = {
|
|
||||||
endpoint: 'http://frigate',
|
|
||||||
};
|
|
||||||
|
|
||||||
vi.mocked(api.getCameraManager().getCameraEndpoints)?.mockReturnValue({
|
|
||||||
ui: endpoint,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(manager.getCameraURL()).toBe('http://frigate');
|
|
||||||
expect(manager.hasCameraURL()).toBeTruthy();
|
|
||||||
|
|
||||||
const windowSpy = vi.spyOn(window, 'open').mockReturnValue(null);
|
|
||||||
manager.openURL();
|
|
||||||
expect(windowSpy).toBeCalledWith('http://frigate');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not get URL without view', () => {
|
|
||||||
const manager = new CameraURLManager(createCardAPI());
|
|
||||||
expect(manager.getCameraURL()).toBeNull();
|
|
||||||
|
|
||||||
const windowSpy = vi.spyOn(window, 'open').mockReturnValue(null);
|
|
||||||
manager.openURL();
|
|
||||||
expect(windowSpy).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not get URL without cameraManager endpoints', () => {
|
|
||||||
const api = createAPIWithMedia();
|
|
||||||
vi.mocked(api.getCameraManager().getCameraEndpoints)?.mockReturnValue(null);
|
|
||||||
const manager = new CameraURLManager(api);
|
|
||||||
expect(manager.getCameraURL()).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,182 +0,0 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import {
|
|
||||||
CardElementManager,
|
|
||||||
CardHTMLElement,
|
|
||||||
} from '../../../src/utils/card-controller/card-element-manager';
|
|
||||||
import { createCardAPI } from '../../test-utils';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
|
|
||||||
const createElement = (): CardHTMLElement => {
|
|
||||||
const element = document.createElement('div') as unknown as CardHTMLElement;
|
|
||||||
element.requestUpdate = vi.fn();
|
|
||||||
return element as CardHTMLElement;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('CardElementManager', () => {
|
|
||||||
afterEach(() => {
|
|
||||||
vi.unstubAllGlobals();
|
|
||||||
global.window.location = mock<Location>();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get element', () => {
|
|
||||||
const element = createElement();
|
|
||||||
const manager = new CardElementManager(
|
|
||||||
createCardAPI(),
|
|
||||||
element,
|
|
||||||
() => undefined,
|
|
||||||
() => undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(manager.getElement()).toBe(element);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reset scroll', () => {
|
|
||||||
const callback = vi.fn();
|
|
||||||
const manager = new CardElementManager(
|
|
||||||
createCardAPI(),
|
|
||||||
createElement(),
|
|
||||||
callback,
|
|
||||||
() => undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
manager.scrollReset();
|
|
||||||
|
|
||||||
expect(callback).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should toggle menu', () => {
|
|
||||||
const callback = vi.fn();
|
|
||||||
const manager = new CardElementManager(
|
|
||||||
createCardAPI(),
|
|
||||||
createElement(),
|
|
||||||
() => undefined,
|
|
||||||
callback,
|
|
||||||
);
|
|
||||||
|
|
||||||
manager.toggleMenu();
|
|
||||||
|
|
||||||
expect(callback).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update', () => {
|
|
||||||
const element = createElement();
|
|
||||||
const manager = new CardElementManager(
|
|
||||||
createCardAPI(),
|
|
||||||
element,
|
|
||||||
() => undefined,
|
|
||||||
() => undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
manager.update();
|
|
||||||
expect(element.requestUpdate).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get hasUpdated', () => {
|
|
||||||
const element = createElement();
|
|
||||||
element.hasUpdated = true;
|
|
||||||
const manager = new CardElementManager(
|
|
||||||
createCardAPI(),
|
|
||||||
element,
|
|
||||||
() => undefined,
|
|
||||||
() => undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(manager.hasUpdated()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get height', () => {
|
|
||||||
const element = createElement();
|
|
||||||
element.getBoundingClientRect = vi.fn().mockReturnValue({
|
|
||||||
width: 200,
|
|
||||||
height: 800,
|
|
||||||
});
|
|
||||||
|
|
||||||
const manager = new CardElementManager(
|
|
||||||
createCardAPI(),
|
|
||||||
element,
|
|
||||||
() => undefined,
|
|
||||||
() => undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(manager.getCardHeight()).toBe(800);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should connect', () => {
|
|
||||||
const windowAddEventListener = vi.spyOn(global.window, 'addEventListener');
|
|
||||||
|
|
||||||
const addEventListener = vi.fn();
|
|
||||||
const element = createElement();
|
|
||||||
element.addEventListener = addEventListener;
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new CardElementManager(
|
|
||||||
api,
|
|
||||||
element,
|
|
||||||
() => undefined,
|
|
||||||
() => undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
manager.elementConnected();
|
|
||||||
|
|
||||||
expect(element.getAttribute('panel')).toBeNull();
|
|
||||||
expect(api.getFullscreenManager().connect).toBeCalled();
|
|
||||||
|
|
||||||
expect(addEventListener).toBeCalledWith(
|
|
||||||
'mousemove',
|
|
||||||
api.getInteractionManager().reportInteraction,
|
|
||||||
);
|
|
||||||
expect(addEventListener).toBeCalledWith(
|
|
||||||
'll-custom',
|
|
||||||
api.getActionsManager().handleActionEvent,
|
|
||||||
);
|
|
||||||
expect(addEventListener).toBeCalledWith(
|
|
||||||
'@action',
|
|
||||||
api.getInteractionManager().reportInteraction,
|
|
||||||
);
|
|
||||||
expect(windowAddEventListener).toBeCalledWith('location-changed', expect.anything());
|
|
||||||
expect(windowAddEventListener).toBeCalledWith('popstate', expect.anything());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should disconnect', () => {
|
|
||||||
const windowRemoveEventListener = vi.spyOn(global.window, 'removeEventListener');
|
|
||||||
|
|
||||||
const element = createElement();
|
|
||||||
element.setAttribute('panel', '');
|
|
||||||
|
|
||||||
const removeEventListener = vi.fn();
|
|
||||||
element.removeEventListener = removeEventListener;
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new CardElementManager(
|
|
||||||
api,
|
|
||||||
element,
|
|
||||||
() => undefined,
|
|
||||||
() => undefined,
|
|
||||||
);
|
|
||||||
|
|
||||||
manager.elementDisconnected();
|
|
||||||
|
|
||||||
expect(element.getAttribute('panel')).toBeNull();
|
|
||||||
expect(api.getMediaLoadedInfoManager().clear).toBeCalled();
|
|
||||||
expect(api.getFullscreenManager().disconnect).toBeCalled();
|
|
||||||
|
|
||||||
expect(removeEventListener).toBeCalledWith(
|
|
||||||
'mousemove',
|
|
||||||
api.getInteractionManager().reportInteraction,
|
|
||||||
);
|
|
||||||
expect(removeEventListener).toBeCalledWith(
|
|
||||||
'll-custom',
|
|
||||||
api.getActionsManager().handleActionEvent,
|
|
||||||
);
|
|
||||||
expect(removeEventListener).toBeCalledWith(
|
|
||||||
'@action',
|
|
||||||
api.getInteractionManager().reportInteraction,
|
|
||||||
);
|
|
||||||
expect(windowRemoveEventListener).toBeCalledWith(
|
|
||||||
'location-changed',
|
|
||||||
expect.anything(),
|
|
||||||
);
|
|
||||||
expect(windowRemoveEventListener).toBeCalledWith('popstate', expect.anything());
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,363 +0,0 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { FrigateCardCondition } from '../../../src/config/types';
|
|
||||||
import {
|
|
||||||
ConditionEvaluateRequestEvent,
|
|
||||||
ConditionsManager,
|
|
||||||
evaluateConditionViaEvent,
|
|
||||||
getOverriddenConfig,
|
|
||||||
getOverridesByKey,
|
|
||||||
} from '../../../src/utils/card-controller/conditions-manager';
|
|
||||||
import {
|
|
||||||
createCardAPI,
|
|
||||||
createCondition,
|
|
||||||
createConfig,
|
|
||||||
createStateEntity,
|
|
||||||
} from '../../test-utils';
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('ConditionEvaluateRequestEvent', () => {
|
|
||||||
it('should construct', () => {
|
|
||||||
const condition = createCondition({ fullscreen: true });
|
|
||||||
const event = new ConditionEvaluateRequestEvent(condition, {
|
|
||||||
bubbles: true,
|
|
||||||
composed: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(event.type).toBe('frigate-card:condition:evaluate');
|
|
||||||
expect(event.condition).toBe(condition);
|
|
||||||
expect(event.bubbles).toBeTruthy();
|
|
||||||
expect(event.composed).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('evaluateConditionViaEvent', () => {
|
|
||||||
it('should evaluate true without condition', () => {
|
|
||||||
const element = document.createElement('div');
|
|
||||||
expect(evaluateConditionViaEvent(element)).toBeTruthy();
|
|
||||||
});
|
|
||||||
it('should dispatch event with condition and evaluate true', () => {
|
|
||||||
const element = document.createElement('div');
|
|
||||||
const condition = createCondition({ fullscreen: true });
|
|
||||||
const handler = vi.fn().mockImplementation((ev: ConditionEvaluateRequestEvent) => {
|
|
||||||
expect(ev.condition).toBe(condition);
|
|
||||||
ev.evaluation = true;
|
|
||||||
});
|
|
||||||
element.addEventListener('frigate-card:condition:evaluate', handler);
|
|
||||||
|
|
||||||
expect(evaluateConditionViaEvent(element, condition)).toBeTruthy();
|
|
||||||
expect(handler).toBeCalled();
|
|
||||||
});
|
|
||||||
it('should dispatch event with condition and evaluate false', () => {
|
|
||||||
const element = document.createElement('div');
|
|
||||||
const condition = createCondition({ fullscreen: true });
|
|
||||||
const handler = vi.fn().mockImplementation((ev: ConditionEvaluateRequestEvent) => {
|
|
||||||
expect(ev.condition).toBe(condition);
|
|
||||||
ev.evaluation = false;
|
|
||||||
});
|
|
||||||
element.addEventListener('frigate-card:condition:evaluate', handler);
|
|
||||||
|
|
||||||
expect(evaluateConditionViaEvent(element, condition)).toBeFalsy();
|
|
||||||
expect(handler).toBeCalled();
|
|
||||||
});
|
|
||||||
it('should dispatch event evaluate false if no evaluation', () => {
|
|
||||||
const element = document.createElement('div');
|
|
||||||
const condition = createCondition({ fullscreen: true });
|
|
||||||
const handler = vi.fn();
|
|
||||||
element.addEventListener('frigate-card:condition:evaluate', handler);
|
|
||||||
|
|
||||||
expect(evaluateConditionViaEvent(element, condition)).toBeFalsy();
|
|
||||||
expect(handler).toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getOverriddenConfig', () => {
|
|
||||||
const config = {
|
|
||||||
menu: {
|
|
||||||
style: 'none',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const overrides = [
|
|
||||||
{
|
|
||||||
overrides: {
|
|
||||||
menu: {
|
|
||||||
style: 'above',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
conditions: {
|
|
||||||
fullscreen: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
it('should not override config', () => {
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
expect(getOverriddenConfig(manager, config, overrides)).toBe(config);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should override config', () => {
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
manager.setState({ fullscreen: true });
|
|
||||||
|
|
||||||
expect(getOverriddenConfig(manager, config, overrides)).toEqual({
|
|
||||||
menu: {
|
|
||||||
style: 'above',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing without overrides', () => {
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
manager.setState({ fullscreen: true });
|
|
||||||
|
|
||||||
expect(getOverriddenConfig(manager, config)).toBe(config);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getOverridesByKey', () => {
|
|
||||||
const condition = {
|
|
||||||
fullscreen: true,
|
|
||||||
};
|
|
||||||
const override = {
|
|
||||||
menu: {
|
|
||||||
style: 'above',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
const overrides = [
|
|
||||||
{
|
|
||||||
overrides: override,
|
|
||||||
conditions: condition,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
it('should get overrides', () => {
|
|
||||||
expect(getOverridesByKey('menu', overrides)).toEqual([
|
|
||||||
{ conditions: condition, overrides: { style: 'above' } },
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get no overrides', () => {
|
|
||||||
expect(getOverridesByKey('live', overrides)).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get no overrides when undefined', () => {
|
|
||||||
expect(getOverridesByKey('live')).toEqual([]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ConditionsManager', () => {
|
|
||||||
const config = {
|
|
||||||
type: 'custom:frigate-card',
|
|
||||||
cameras: [],
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
type: 'custom:frigate-card-conditional',
|
|
||||||
conditions: {
|
|
||||||
fullscreen: true,
|
|
||||||
},
|
|
||||||
elements: [
|
|
||||||
{
|
|
||||||
type: 'custom:nested-unknown-object',
|
|
||||||
unknown_key: {
|
|
||||||
type: 'custom:frigate-card-conditional',
|
|
||||||
conditions: {
|
|
||||||
media_query: 'media query goes here',
|
|
||||||
},
|
|
||||||
elements: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
overrides: [
|
|
||||||
{
|
|
||||||
overrides: {
|
|
||||||
menu: {
|
|
||||||
style: 'overlay',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
conditions: {
|
|
||||||
fullscreen: true,
|
|
||||||
state: [
|
|
||||||
{
|
|
||||||
entity: 'binary_sensor.foo',
|
|
||||||
state: 'on',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get epoch', () => {
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
const epoch_1 = manager.getEpoch();
|
|
||||||
expect(epoch_1).toEqual({ manager: manager });
|
|
||||||
|
|
||||||
manager.setState({ fullscreen: true });
|
|
||||||
|
|
||||||
const epoch_2 = manager.getEpoch();
|
|
||||||
expect(epoch_2).toEqual({ manager: manager });
|
|
||||||
|
|
||||||
// Since the state was set the wrappers should be different.
|
|
||||||
expect(epoch_1).not.toBe(epoch_2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not return hasHAStateConditions without HA state conditions', () => {
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
expect(manager.hasHAStateConditions()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return hasHAStateConditions with HA state conditions', () => {
|
|
||||||
vi.spyOn(window, 'matchMedia').mockReturnValueOnce({
|
|
||||||
matches: false,
|
|
||||||
addEventListener: vi.fn(),
|
|
||||||
} as unknown as MediaQueryList);
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config));
|
|
||||||
const manager = new ConditionsManager(api);
|
|
||||||
|
|
||||||
manager.setConditionsFromConfig();
|
|
||||||
|
|
||||||
expect(manager.hasHAStateConditions()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should evaluate conditions with a view', () => {
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
const condition = { view: ['foo'] };
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
manager.setState({ view: 'foo' });
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should evaluate conditions with fullscreen', () => {
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
const condition = { fullscreen: true };
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
manager.setState({ fullscreen: true });
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
|
||||||
manager.setState({ fullscreen: false });
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should evaluate conditions with expand', () => {
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
const condition = { expand: true };
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
manager.setState({ expand: true });
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
|
||||||
manager.setState({ expand: false });
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should evaluate conditions with camera', () => {
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
const condition = { camera: ['bar'] };
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
manager.setState({ camera: 'bar' });
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
|
||||||
manager.setState({ camera: 'will-not-match' });
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should evaluate conditions with ha state positive check', () => {
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
const condition = {
|
|
||||||
state: [
|
|
||||||
{
|
|
||||||
entity: 'binary_sensor.foo',
|
|
||||||
state: 'on',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
manager.setState({ state: { 'binary_sensor.foo': createStateEntity() } });
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
|
||||||
manager.setState({
|
|
||||||
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
|
||||||
});
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should evaluate conditions with ha state negative check', () => {
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
const condition = {
|
|
||||||
state: [
|
|
||||||
{
|
|
||||||
entity: 'binary_sensor.foo',
|
|
||||||
state_not: 'on',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
manager.setState({ state: { 'binary_sensor.foo': createStateEntity() } });
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
manager.setState({
|
|
||||||
state: { 'binary_sensor.foo': createStateEntity({ state: 'off' }) },
|
|
||||||
});
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should evaluate conditions with media_loaded', () => {
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
const condition = { media_loaded: true };
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
manager.setState({ media_loaded: true });
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
|
||||||
manager.setState({ media_loaded: false });
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should evaluate conditions with media query', () => {
|
|
||||||
vi.spyOn(window, 'matchMedia')
|
|
||||||
.mockReturnValueOnce(<MediaQueryList>{ matches: true })
|
|
||||||
.mockReturnValueOnce(<MediaQueryList>{ matches: false });
|
|
||||||
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
const condition = { media_query: 'whatever' };
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should trigger on changes to media query conditions', () => {
|
|
||||||
const addEventListener = vi.fn();
|
|
||||||
const removeEventListener = vi.fn();
|
|
||||||
vi.spyOn(window, 'matchMedia').mockReturnValueOnce({
|
|
||||||
matches: true,
|
|
||||||
addEventListener: addEventListener,
|
|
||||||
removeEventListener: removeEventListener,
|
|
||||||
} as unknown as MediaQueryList);
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config));
|
|
||||||
const callback = vi.fn();
|
|
||||||
const manager = new ConditionsManager(api, callback);
|
|
||||||
|
|
||||||
manager.setConditionsFromConfig();
|
|
||||||
|
|
||||||
expect(addEventListener).toHaveBeenCalledWith('change', expect.anything());
|
|
||||||
|
|
||||||
// Call the media query callback and use it to pretend a match happened. The
|
|
||||||
// callback is the 0th mock innvocation and the 1st argument.
|
|
||||||
addEventListener.mock.calls[0][1]();
|
|
||||||
|
|
||||||
// This should result in a callback to our state listener.
|
|
||||||
expect(callback).toBeCalled();
|
|
||||||
|
|
||||||
// Remove the conditions, which should remove the media query listener.
|
|
||||||
manager.removeConditions();
|
|
||||||
expect(removeEventListener).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should evaluate conditions with display mode', () => {
|
|
||||||
const manager = new ConditionsManager(createCardAPI());
|
|
||||||
const condition: FrigateCardCondition = { display_mode: 'grid' };
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
manager.setState({ displayMode: 'grid' });
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeTruthy();
|
|
||||||
manager.setState({ displayMode: 'single' });
|
|
||||||
expect(manager.evaluateCondition(condition)).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { ZodError } from 'zod';
|
|
||||||
import { frigateCardConfigSchema } from '../../../src/config/types';
|
|
||||||
import { getOverriddenConfig } from '../../../src/utils/card-controller/conditions-manager';
|
|
||||||
import { ConfigManager } from '../../../src/utils/card-controller/config-manager';
|
|
||||||
import { InitializationAspect } from '../../../src/utils/card-controller/initialization-manager';
|
|
||||||
import { createCardAPI, createConfig } from '../../test-utils';
|
|
||||||
|
|
||||||
vi.mock('../../../src/utils/card-controller/conditions-manager.js');
|
|
||||||
|
|
||||||
describe('ConfigManager', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should handle error when', () => {
|
|
||||||
it('no input', () => {
|
|
||||||
const manager = new ConfigManager(createCardAPI());
|
|
||||||
expect(() => manager.setConfig()).toThrowError(/Invalid configuration/);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('invalid configuration', () => {
|
|
||||||
const spy = vi.spyOn(frigateCardConfigSchema, 'safeParse').mockReturnValue({
|
|
||||||
success: false,
|
|
||||||
error: new ZodError([]),
|
|
||||||
});
|
|
||||||
|
|
||||||
const manager = new ConfigManager(createCardAPI());
|
|
||||||
expect(() => manager.setConfig({})).toThrowError(
|
|
||||||
'Invalid configuration: No location hint available (bad or missing type?)',
|
|
||||||
);
|
|
||||||
|
|
||||||
spy.mockRestore();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('invalid configuration with hint', () => {
|
|
||||||
const manager = new ConfigManager(createCardAPI());
|
|
||||||
expect(() => manager.setConfig({})).toThrowError(
|
|
||||||
'Invalid configuration: [\n "cameras",\n "type"\n]',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('upgradeable', () => {
|
|
||||||
const manager = new ConfigManager(createCardAPI());
|
|
||||||
expect(() =>
|
|
||||||
manager.setConfig({
|
|
||||||
cameras: [
|
|
||||||
{
|
|
||||||
frigate: {
|
|
||||||
label: 'foo',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
|
||||||
).toThrowError(
|
|
||||||
'An automated card configuration upgrade is ' +
|
|
||||||
'available, please visit the visual card editor. ' +
|
|
||||||
'Invalid configuration: [\n "type"\n]',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have initial state', () => {
|
|
||||||
const manager = new ConfigManager(createCardAPI());
|
|
||||||
|
|
||||||
expect(manager.getConfig()).toBeNull();
|
|
||||||
expect(manager.getNonOverriddenConfig()).toBeNull();
|
|
||||||
expect(manager.getRawConfig()).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should successfully parse basic config', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ConfigManager(api);
|
|
||||||
const config = {
|
|
||||||
type: 'custom:frigate-card',
|
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
|
||||||
};
|
|
||||||
|
|
||||||
manager.setConfig(config);
|
|
||||||
|
|
||||||
expect(manager.hasConfig()).toBeTruthy()
|
|
||||||
expect(manager.getRawConfig()).toBe(config);
|
|
||||||
|
|
||||||
// Verify at least the camera is set.
|
|
||||||
expect(manager.getConfig()?.cameras[0].camera_entity).toBe('camera.office');
|
|
||||||
|
|
||||||
// Verify at least one default was set.
|
|
||||||
expect(manager.getConfig()?.menu.alignment).toBe('left');
|
|
||||||
|
|
||||||
// Verify appropriate API calls are made.
|
|
||||||
expect(api.getConditionsManager().setConditionsFromConfig).toBeCalled();
|
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({
|
|
||||||
view: undefined,
|
|
||||||
displayMode: undefined,
|
|
||||||
camera: undefined,
|
|
||||||
});
|
|
||||||
expect(api.getMediaLoadedInfoManager().clear).toBeCalled();
|
|
||||||
expect(api.getViewManager().reset).toBeCalled();
|
|
||||||
expect(api.getMessageManager().reset).toBeCalled();
|
|
||||||
expect(api.getAutomationsManager().setAutomationsFromConfig).toBeCalled();
|
|
||||||
expect(api.getStyleManager().setPerformance).toBeCalled();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should apply low performance defaults', () => {
|
|
||||||
const manager = new ConfigManager(createCardAPI());
|
|
||||||
const config = {
|
|
||||||
type: 'custom:frigate-card',
|
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
|
||||||
performance: { profile: 'low' },
|
|
||||||
};
|
|
||||||
|
|
||||||
manager.setConfig(config);
|
|
||||||
|
|
||||||
// Verify at least one low performance default.
|
|
||||||
expect(manager.getConfig()?.live.draggable).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should skip identical configs', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ConfigManager(api);
|
|
||||||
const config = {
|
|
||||||
type: 'custom:frigate-card',
|
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
|
||||||
};
|
|
||||||
|
|
||||||
manager.setConfig(config);
|
|
||||||
expect(api.getViewManager().reset).toBeCalled();
|
|
||||||
|
|
||||||
vi.mocked(api.getViewManager().reset).mockClear();
|
|
||||||
|
|
||||||
manager.setConfig(config);
|
|
||||||
expect(api.getViewManager().reset).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get card wide config', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ConfigManager(api);
|
|
||||||
const config = {
|
|
||||||
type: 'custom:frigate-card',
|
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
|
||||||
debug: {
|
|
||||||
logging: true,
|
|
||||||
},
|
|
||||||
performance: {
|
|
||||||
profile: 'low',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
manager.setConfig(config);
|
|
||||||
|
|
||||||
expect(manager.getCardWideConfig()).toEqual({
|
|
||||||
debug: {
|
|
||||||
logging: true,
|
|
||||||
},
|
|
||||||
performance: {
|
|
||||||
features: {
|
|
||||||
animated_progress_indicator: false,
|
|
||||||
media_chunk_size: 10,
|
|
||||||
},
|
|
||||||
profile: 'low',
|
|
||||||
style: {
|
|
||||||
border_radius: false,
|
|
||||||
box_shadow: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore overrides without a config', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ConfigManager(api);
|
|
||||||
|
|
||||||
manager.computeOverrideConfig();
|
|
||||||
|
|
||||||
expect(manager.getConfig()).toBeNull();
|
|
||||||
expect(api.getStyleManager().setMinMaxHeight).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore overrides with same config', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ConfigManager(api);
|
|
||||||
const config = {
|
|
||||||
type: 'custom:frigate-card',
|
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(config);
|
|
||||||
|
|
||||||
manager.setConfig(config);
|
|
||||||
expect(api.getStyleManager().setMinMaxHeight).toBeCalled();
|
|
||||||
|
|
||||||
vi.mocked(api.getStyleManager().setMinMaxHeight).mockClear();
|
|
||||||
manager.computeOverrideConfig();
|
|
||||||
|
|
||||||
expect(api.getStyleManager().setMinMaxHeight).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should override', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ConfigManager(api);
|
|
||||||
const config_1 = {
|
|
||||||
type: 'custom:frigate-card',
|
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
|
||||||
};
|
|
||||||
manager.setConfig(config_1);
|
|
||||||
vi.mocked(api.getStyleManager().setMinMaxHeight).mockClear();
|
|
||||||
|
|
||||||
const config_2 = {
|
|
||||||
type: 'custom:frigate-card',
|
|
||||||
cameras: [{ camera_entity: 'camera.kitchen' }],
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(config_2);
|
|
||||||
manager.computeOverrideConfig();
|
|
||||||
|
|
||||||
expect(api.getStyleManager().setMinMaxHeight).toBeCalled();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
expect(manager.getConfig()).not.toEqual(manager.getNonOverriddenConfig());
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should uninitialize on override', () => {
|
|
||||||
it('cameras', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ConfigManager(api);
|
|
||||||
const config_1 = {
|
|
||||||
type: 'custom:frigate-card',
|
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
|
|
||||||
|
|
||||||
manager.setConfig(config_1);
|
|
||||||
expect(api.getInitializationManager().uninitialize).not.toBeCalled();
|
|
||||||
|
|
||||||
const config_2 = {
|
|
||||||
type: 'custom:frigate-card',
|
|
||||||
cameras: [{ camera_entity: 'camera.kitchen' }],
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
|
|
||||||
manager.computeOverrideConfig();
|
|
||||||
|
|
||||||
expect(api.getInitializationManager().uninitialize).toBeCalledWith(
|
|
||||||
InitializationAspect.CAMERAS,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('cameras_global', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ConfigManager(api);
|
|
||||||
const config_1 = {
|
|
||||||
type: 'custom:frigate-card',
|
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
|
|
||||||
|
|
||||||
manager.setConfig(config_1);
|
|
||||||
expect(api.getInitializationManager().uninitialize).not.toBeCalled();
|
|
||||||
|
|
||||||
const config_2 = {
|
|
||||||
...config_1,
|
|
||||||
cameras_global: {
|
|
||||||
live_provider: 'jsmpeg'
|
|
||||||
}
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
|
|
||||||
manager.computeOverrideConfig();
|
|
||||||
|
|
||||||
expect(api.getInitializationManager().uninitialize).toBeCalledWith(
|
|
||||||
InitializationAspect.CAMERAS,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('live.microphone.always_connected', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ConfigManager(api);
|
|
||||||
const config_1 = {
|
|
||||||
type: 'custom:frigate-card',
|
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
|
||||||
live: {
|
|
||||||
microphone: {
|
|
||||||
always_connected: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_1));
|
|
||||||
|
|
||||||
manager.setConfig(config_1);
|
|
||||||
expect(api.getInitializationManager().uninitialize).not.toBeCalled();
|
|
||||||
|
|
||||||
const config_2 = {
|
|
||||||
...config_1,
|
|
||||||
live: {
|
|
||||||
microphone: {
|
|
||||||
always_connected: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
vi.mocked(getOverriddenConfig).mockReturnValue(createConfig(config_2));
|
|
||||||
manager.computeOverrideConfig();
|
|
||||||
|
|
||||||
expect(api.getInitializationManager().uninitialize).toBeCalledWith(
|
|
||||||
InitializationAspect.MICROPHONE_CONNECT,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,276 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { CameraManager } from '../../../src/camera-manager/manager';
|
|
||||||
import { FrigateCardEditor } from '../../../src/editor';
|
|
||||||
import { ActionsManager } from '../../../src/utils/card-controller/actions-manager';
|
|
||||||
import { AutoUpdateManager } from '../../../src/utils/card-controller/auto-update-manager';
|
|
||||||
import { AutomationsManager } from '../../../src/utils/card-controller/automations-manager';
|
|
||||||
import { CameraURLManager } from '../../../src/utils/card-controller/camera-url-manager';
|
|
||||||
import {
|
|
||||||
CardElementManager,
|
|
||||||
CardHTMLElement,
|
|
||||||
} from '../../../src/utils/card-controller/card-element-manager';
|
|
||||||
import { ConditionsManager } from '../../../src/utils/card-controller/conditions-manager';
|
|
||||||
import { ConfigManager } from '../../../src/utils/card-controller/config-manager';
|
|
||||||
import { CardController } from '../../../src/utils/card-controller/controller';
|
|
||||||
import { DownloadManager } from '../../../src/utils/card-controller/download-manager';
|
|
||||||
import { ExpandManager } from '../../../src/utils/card-controller/expand-manager';
|
|
||||||
import { FullscreenManager } from '../../../src/utils/card-controller/fullscreen-manager';
|
|
||||||
import { HASSManager } from '../../../src/utils/card-controller/hass-manager';
|
|
||||||
import { InitializationManager } from '../../../src/utils/card-controller/initialization-manager';
|
|
||||||
import { InteractionManager } from '../../../src/utils/card-controller/interaction-manager';
|
|
||||||
import { MediaLoadedInfoManager } from '../../../src/utils/card-controller/media-info-manager';
|
|
||||||
import { MediaPlayerManager } from '../../../src/utils/card-controller/media-player-manager';
|
|
||||||
import { MessageManager } from '../../../src/utils/card-controller/message-manager';
|
|
||||||
import { MicrophoneManager } from '../../../src/utils/card-controller/microphone-manager';
|
|
||||||
import { QueryStringManager } from '../../../src/utils/card-controller/query-string-manager';
|
|
||||||
import { StyleManager } from '../../../src/utils/card-controller/style-manager';
|
|
||||||
import { TriggersManager } from '../../../src/utils/card-controller/triggers-manager';
|
|
||||||
import { ViewManager } from '../../../src/utils/card-controller/view-manager';
|
|
||||||
import { EntityRegistryManager } from '../../../src/utils/ha/entity-registry';
|
|
||||||
import { ResolvedMediaCache } from '../../../src/utils/ha/resolved-media';
|
|
||||||
|
|
||||||
vi.mock('../../../src/camera-manager/manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/actions-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/auto-update-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/automations-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/camera-url-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/card-element-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/conditions-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/config-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/download-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/expand-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/fullscreen-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/hass-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/initialization-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/interaction-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/media-info-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/media-player-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/message-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/microphone-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/query-string-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/style-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/triggers-manager');
|
|
||||||
vi.mock('../../../src/utils/card-controller/view-manager');
|
|
||||||
vi.mock('../../../src/utils/ha/entity-registry');
|
|
||||||
vi.mock('../../../src/utils/ha/resolved-media');
|
|
||||||
|
|
||||||
const createCardElement = (): CardHTMLElement => {
|
|
||||||
const element = document.createElement('div') as unknown as CardHTMLElement;
|
|
||||||
element.addController = vi.fn();
|
|
||||||
return element;
|
|
||||||
};
|
|
||||||
|
|
||||||
const createController = (): CardController => {
|
|
||||||
return new CardController(createCardElement(), vi.fn(), vi.fn(), vi.fn());
|
|
||||||
};
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('CardController', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should construct correctly', () => {
|
|
||||||
const element = createCardElement();
|
|
||||||
const scrollCallback = vi.fn();
|
|
||||||
const menuToggleCallback = vi.fn();
|
|
||||||
const conditionListener = vi.fn();
|
|
||||||
|
|
||||||
const manager = new CardController(
|
|
||||||
element,
|
|
||||||
scrollCallback,
|
|
||||||
menuToggleCallback,
|
|
||||||
conditionListener,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(ConditionsManager).toBeCalledWith(manager, conditionListener);
|
|
||||||
expect(CardElementManager).toBeCalledWith(
|
|
||||||
manager,
|
|
||||||
element,
|
|
||||||
scrollCallback,
|
|
||||||
menuToggleCallback,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('accessors', () => {
|
|
||||||
it('getActionsManager', () => {
|
|
||||||
expect(createController().getActionsManager()).toBe(
|
|
||||||
vi.mocked(ActionsManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getAutomationsManager', () => {
|
|
||||||
expect(createController().getAutomationsManager()).toBe(
|
|
||||||
vi.mocked(AutomationsManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getAutoUpdateManager', () => {
|
|
||||||
expect(createController().getAutoUpdateManager()).toBe(
|
|
||||||
vi.mocked(AutoUpdateManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getCameraManager', () => {
|
|
||||||
expect(createController().getCameraManager()).toBe(
|
|
||||||
vi.mocked(CameraManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getCameraURLManager', () => {
|
|
||||||
expect(createController().getCameraURLManager()).toBe(
|
|
||||||
vi.mocked(CameraURLManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getCardElementManager', () => {
|
|
||||||
expect(createController().getCardElementManager()).toBe(
|
|
||||||
vi.mocked(CardElementManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getConditionsManager', () => {
|
|
||||||
expect(createController().getConditionsManager()).toBe(
|
|
||||||
vi.mocked(ConditionsManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getConfigElement', async () => {
|
|
||||||
expect((await CardController.getConfigElement()) instanceof FrigateCardEditor);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getConfigManager', () => {
|
|
||||||
expect(createController().getConfigManager()).toBe(
|
|
||||||
vi.mocked(ConfigManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getDownloadManager', () => {
|
|
||||||
expect(createController().getDownloadManager()).toBe(
|
|
||||||
vi.mocked(DownloadManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getEntityRegistryManager', () => {
|
|
||||||
expect(createController().getEntityRegistryManager()).toBe(
|
|
||||||
vi.mocked(EntityRegistryManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getExpandManager', () => {
|
|
||||||
expect(createController().getExpandManager()).toBe(
|
|
||||||
vi.mocked(ExpandManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getFullscreenManager', () => {
|
|
||||||
expect(createController().getFullscreenManager()).toBe(
|
|
||||||
vi.mocked(FullscreenManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getHASSManager', () => {
|
|
||||||
expect(createController().getHASSManager()).toBe(
|
|
||||||
vi.mocked(HASSManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getInitializationManager', () => {
|
|
||||||
expect(createController().getInitializationManager()).toBe(
|
|
||||||
vi.mocked(InitializationManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getInteractionManager', () => {
|
|
||||||
expect(createController().getInteractionManager()).toBe(
|
|
||||||
vi.mocked(InteractionManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getMediaLoadedInfoManager', () => {
|
|
||||||
expect(createController().getMediaLoadedInfoManager()).toBe(
|
|
||||||
vi.mocked(MediaLoadedInfoManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getMediaPlayerManager', () => {
|
|
||||||
expect(createController().getMediaPlayerManager()).toBe(
|
|
||||||
vi.mocked(MediaPlayerManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getMessageManager', () => {
|
|
||||||
expect(createController().getMessageManager()).toBe(
|
|
||||||
vi.mocked(MessageManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getMicrophoneManager', () => {
|
|
||||||
expect(createController().getMicrophoneManager()).toBe(
|
|
||||||
vi.mocked(MicrophoneManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getResolvedMediaCache', () => {
|
|
||||||
expect(createController().getResolvedMediaCache()).toBe(
|
|
||||||
vi.mocked(ResolvedMediaCache).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getStubConfig', () => {
|
|
||||||
it('with camera entities', () => {
|
|
||||||
expect(
|
|
||||||
CardController.getStubConfig(['camera.office', 'binary_sensor.motion']),
|
|
||||||
).toEqual({
|
|
||||||
cameras: [{ camera_entity: 'camera.office' }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('without camera entities', () => {
|
|
||||||
expect(CardController.getStubConfig(['binary_sensor.motion'])).toEqual({
|
|
||||||
cameras: [{ camera_entity: 'camera.demo' }],
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getQueryStringManager', () => {
|
|
||||||
expect(createController().getQueryStringManager()).toBe(
|
|
||||||
vi.mocked(QueryStringManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getStyleManager', () => {
|
|
||||||
expect(createController().getStyleManager()).toBe(
|
|
||||||
vi.mocked(StyleManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getTriggersManager', () => {
|
|
||||||
expect(createController().getTriggersManager()).toBe(
|
|
||||||
vi.mocked(TriggersManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('getViewManager', () => {
|
|
||||||
expect(createController().getViewManager()).toBe(
|
|
||||||
vi.mocked(ViewManager).mock.instances[0],
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('handlers', () => {
|
|
||||||
it('hostConnected', () => {
|
|
||||||
createController().hostConnected();
|
|
||||||
expect(
|
|
||||||
vi.mocked(CardElementManager).mock.instances[0].elementConnected,
|
|
||||||
).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('hostDisconnected', () => {
|
|
||||||
createController().hostDisconnected();
|
|
||||||
expect(
|
|
||||||
vi.mocked(CardElementManager).mock.instances[0].elementDisconnected,
|
|
||||||
).toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
import { FrigateCardMediaPlayer } from '../../../src/types';
|
|
||||||
import { DownloadManager } from '../../../src/utils/card-controller/download-manager';
|
|
||||||
import { downloadMedia, downloadURL } from '../../../src/utils/download.js';
|
|
||||||
import {
|
|
||||||
createCardAPI,
|
|
||||||
createHASS,
|
|
||||||
createMediaLoadedInfo,
|
|
||||||
createViewWithMedia,
|
|
||||||
} from '../../test-utils';
|
|
||||||
|
|
||||||
vi.mock('../../../src/utils/download.js');
|
|
||||||
|
|
||||||
describe('DownloadManager.downloadViewerMedia', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.resetAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should download', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(createViewWithMedia());
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
const manager = new DownloadManager(api);
|
|
||||||
|
|
||||||
expect(await manager.downloadViewerMedia()).toBeTruthy();
|
|
||||||
expect(downloadMedia).toBeCalledWith(
|
|
||||||
api.getHASSManager().getHASS(),
|
|
||||||
api.getCameraManager(),
|
|
||||||
api.getViewManager().getView()?.queryResults?.getResult(0),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not download due to exception thrown', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(createViewWithMedia());
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
const manager = new DownloadManager(api);
|
|
||||||
|
|
||||||
const error = new Error();
|
|
||||||
vi.mocked(downloadMedia).mockRejectedValue(error);
|
|
||||||
|
|
||||||
expect(await manager.downloadViewerMedia()).toBeFalsy();
|
|
||||||
expect(api.getMessageManager().setErrorIfHigherPriority).toBeCalledWith(error);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not download without hass', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(createViewWithMedia());
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
|
||||||
const manager = new DownloadManager(api);
|
|
||||||
|
|
||||||
expect(await manager.downloadViewerMedia()).toBeFalsy();
|
|
||||||
expect(downloadMedia).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('DownloadManager.downloadScreenshot', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with url', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const player = mock<FrigateCardMediaPlayer>();
|
|
||||||
player.getScreenshotURL.mockResolvedValue('http://screenshot');
|
|
||||||
|
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
|
||||||
createMediaLoadedInfo({
|
|
||||||
player: player,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new DownloadManager(api);
|
|
||||||
await manager.downloadScreenshot();
|
|
||||||
|
|
||||||
expect(downloadURL).toBeCalledWith('http://screenshot', 'screenshot.jpg');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('without url', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const player = mock<FrigateCardMediaPlayer>();
|
|
||||||
player.getScreenshotURL.mockResolvedValue(null);
|
|
||||||
|
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
|
||||||
createMediaLoadedInfo({
|
|
||||||
player: player,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new DownloadManager(api);
|
|
||||||
await manager.downloadScreenshot();
|
|
||||||
|
|
||||||
expect(downloadURL).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
|
||||||
import { ExpandManager } from '../../../src/utils/card-controller/expand-manager';
|
|
||||||
import { createCardAPI } from '../../test-utils';
|
|
||||||
|
|
||||||
describe('ExpandManager', () => {
|
|
||||||
it('should construct', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ExpandManager(api);
|
|
||||||
expect(manager.isExpanded()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set expanded', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getFullscreenManager().isInFullscreen).mockReturnValue(true);
|
|
||||||
const manager = new ExpandManager(api);
|
|
||||||
|
|
||||||
manager.setExpanded(true);
|
|
||||||
|
|
||||||
expect(manager.isExpanded()).toBeTruthy();
|
|
||||||
expect(api.getFullscreenManager().stopFullscreen).toBeCalled();
|
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({ expand: true });
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not exit fullscreen when not in fullscreen', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getFullscreenManager().isInFullscreen).mockReturnValue(false);
|
|
||||||
const manager = new ExpandManager(api);
|
|
||||||
|
|
||||||
manager.setExpanded(true);
|
|
||||||
|
|
||||||
expect(api.getFullscreenManager().stopFullscreen).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should toggle expanded', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getFullscreenManager().isInFullscreen).mockReturnValue(false);
|
|
||||||
const manager = new ExpandManager(api);
|
|
||||||
|
|
||||||
manager.toggleExpanded();
|
|
||||||
expect(manager.isExpanded()).toBeTruthy();
|
|
||||||
|
|
||||||
manager.toggleExpanded();
|
|
||||||
expect(manager.isExpanded()).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import screenfull from 'screenfull';
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { FullscreenManager } from '../../../src/utils/card-controller/fullscreen-manager';
|
|
||||||
import { createCardAPI } from '../../test-utils';
|
|
||||||
|
|
||||||
vi.mock('screenfull', () => ({
|
|
||||||
default: {
|
|
||||||
exit: vi.fn(),
|
|
||||||
toggle: vi.fn(),
|
|
||||||
off: vi.fn(),
|
|
||||||
on: vi.fn(),
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
const setScreenfulEnabled = (enabled: boolean): void => {
|
|
||||||
Object.defineProperty(screenfull, 'isEnabled', { value: enabled, writable: true });
|
|
||||||
};
|
|
||||||
|
|
||||||
const setScreenfulFullscreen = (fullscreen: boolean): void => {
|
|
||||||
Object.defineProperty(screenfull, 'isFullscreen', {
|
|
||||||
value: fullscreen,
|
|
||||||
writable: true,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('FullscreenManager', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should correctly determine whether in fullscreen', () => {
|
|
||||||
const manager = new FullscreenManager(createCardAPI());
|
|
||||||
|
|
||||||
setScreenfulEnabled(true);
|
|
||||||
setScreenfulFullscreen(true);
|
|
||||||
expect(manager.isInFullscreen()).toBeTruthy();
|
|
||||||
|
|
||||||
setScreenfulFullscreen(false);
|
|
||||||
expect(manager.isInFullscreen()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should toggle fullscreen', () => {
|
|
||||||
const toggle = vi.mocked(screenfull.toggle);
|
|
||||||
const element = document.createElement('div')
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(
|
|
||||||
element,
|
|
||||||
);
|
|
||||||
const manager = new FullscreenManager(api);
|
|
||||||
|
|
||||||
manager.toggleFullscreen();
|
|
||||||
|
|
||||||
expect(toggle).toBeCalledWith(element);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stop fullscreen', () => {
|
|
||||||
const manager = new FullscreenManager(createCardAPI());
|
|
||||||
const exit = vi.mocked(screenfull.exit);
|
|
||||||
|
|
||||||
manager.stopFullscreen();
|
|
||||||
|
|
||||||
expect(exit).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should disconnect', () => {
|
|
||||||
const manager = new FullscreenManager(createCardAPI());
|
|
||||||
const off = vi.mocked(screenfull.off);
|
|
||||||
|
|
||||||
setScreenfulEnabled(true);
|
|
||||||
|
|
||||||
manager.disconnect();
|
|
||||||
|
|
||||||
expect(off).toBeCalledWith('change', expect.anything());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not disconnect when screenfull disabled', () => {
|
|
||||||
const manager = new FullscreenManager(createCardAPI());
|
|
||||||
const off = vi.mocked(screenfull.off);
|
|
||||||
|
|
||||||
setScreenfulEnabled(false);
|
|
||||||
|
|
||||||
manager.disconnect();
|
|
||||||
|
|
||||||
expect(off).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should connect', () => {
|
|
||||||
const manager = new FullscreenManager(createCardAPI());
|
|
||||||
const on = vi.mocked(screenfull.on);
|
|
||||||
|
|
||||||
setScreenfulEnabled(true);
|
|
||||||
|
|
||||||
manager.connect();
|
|
||||||
|
|
||||||
expect(on).toBeCalledWith('change', expect.anything());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not connect when screenfull disabled', () => {
|
|
||||||
const manager = new FullscreenManager(createCardAPI());
|
|
||||||
const on = vi.mocked(screenfull.on);
|
|
||||||
|
|
||||||
setScreenfulEnabled(false);
|
|
||||||
|
|
||||||
manager.connect();
|
|
||||||
|
|
||||||
expect(on).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should make correct api calls on fullscreen change', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new FullscreenManager(api);
|
|
||||||
const on = vi.mocked(screenfull.on);
|
|
||||||
|
|
||||||
setScreenfulEnabled(true);
|
|
||||||
setScreenfulFullscreen(true);
|
|
||||||
|
|
||||||
manager.connect();
|
|
||||||
|
|
||||||
expect(on).toBeCalled();
|
|
||||||
on.mock.calls[0][1](new Event('fullscreen'));
|
|
||||||
|
|
||||||
expect(api.getExpandManager().setExpanded).toBeCalledWith(false);
|
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith({
|
|
||||||
fullscreen: true,
|
|
||||||
});
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,279 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { HASSManager } from '../../../src/utils/card-controller/hass-manager';
|
|
||||||
import { CardHASSAPI } from '../../../src/utils/card-controller/types';
|
|
||||||
import {
|
|
||||||
createCameraConfig,
|
|
||||||
createCameraManager,
|
|
||||||
createCardAPI,
|
|
||||||
createConfig,
|
|
||||||
createHASS,
|
|
||||||
createStateEntity,
|
|
||||||
createView,
|
|
||||||
} from '../../test-utils';
|
|
||||||
|
|
||||||
vi.mock('../../../src/camera-manager/manager.js');
|
|
||||||
|
|
||||||
const createAPIWithoutMediaPlayers = (): CardHASSAPI => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getMediaPlayerManager().getMediaPlayers).mockReturnValue([]);
|
|
||||||
return api;
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('HASSManager', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.resetAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have null hass on construction', () => {
|
|
||||||
const manager = new HASSManager(createCardAPI());
|
|
||||||
expect(manager.getHASS()).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set light or dark mode upon setting hass', () => {
|
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
|
|
||||||
manager.setHASS(createHASS());
|
|
||||||
|
|
||||||
expect(api.getStyleManager().setLightOrDarkMode).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should set condition manager state', () => {
|
|
||||||
it('positively', () => {
|
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
vi.mocked(api.getConditionsManager().hasHAStateConditions).mockReturnValue(true);
|
|
||||||
|
|
||||||
const states = { 'switch.foo': createStateEntity() };
|
|
||||||
const hass = createHASS(states);
|
|
||||||
|
|
||||||
manager.setHASS(hass);
|
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
state: states,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('negatively', () => {
|
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
vi.mocked(api.getConditionsManager().hasHAStateConditions).mockReturnValue(false);
|
|
||||||
|
|
||||||
manager.setHASS(createHASS());
|
|
||||||
|
|
||||||
expect(api.getConditionsManager().setState).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should update triggered cameras', () => {
|
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
|
|
||||||
const originalHASS = createHASS();
|
|
||||||
manager.setHASS(originalHASS);
|
|
||||||
expect(api.getTriggersManager().updateTriggeredCameras).toBeCalledWith(null);
|
|
||||||
|
|
||||||
manager.setHASS(createHASS());
|
|
||||||
expect(api.getTriggersManager().updateTriggeredCameras).toBeCalledWith(originalHASS);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should handle connection state change when', () => {
|
|
||||||
it('initially disconnected', () => {
|
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
|
|
||||||
const disconnectedHASS = createHASS();
|
|
||||||
disconnectedHASS.connected = false;
|
|
||||||
|
|
||||||
manager.setHASS(disconnectedHASS);
|
|
||||||
|
|
||||||
expect(api.getMessageManager().setMessageIfHigherPriority).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
message: 'Reconnecting',
|
|
||||||
icon: 'mdi:lan-disconnect',
|
|
||||||
type: 'connection',
|
|
||||||
dotdotdot: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('disconnected', () => {
|
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
|
|
||||||
manager.setHASS(createHASS());
|
|
||||||
|
|
||||||
const disconnectedHASS = createHASS();
|
|
||||||
disconnectedHASS.connected = false;
|
|
||||||
manager.setHASS(disconnectedHASS);
|
|
||||||
|
|
||||||
expect(api.getMessageManager().setMessageIfHigherPriority).toBeCalledWith(
|
|
||||||
expect.objectContaining({
|
|
||||||
message: 'Reconnecting',
|
|
||||||
icon: 'mdi:lan-disconnect',
|
|
||||||
type: 'connection',
|
|
||||||
dotdotdot: true,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('reconnected', () => {
|
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
|
|
||||||
const disconnectedHASS = createHASS();
|
|
||||||
disconnectedHASS.connected = false;
|
|
||||||
manager.setHASS(disconnectedHASS);
|
|
||||||
|
|
||||||
const reconnectedHASS = createHASS();
|
|
||||||
manager.setHASS(reconnectedHASS);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should set default view when', () => {
|
|
||||||
it('selected camera trigger entity changes', () => {
|
|
||||||
const cameraManager = createCameraManager({
|
|
||||||
configs: new Map([
|
|
||||||
[
|
|
||||||
'camera.foo',
|
|
||||||
createCameraConfig({
|
|
||||||
triggers: {
|
|
||||||
entities: ['binary_sensor.motion'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
||||||
createView({
|
|
||||||
camera: 'camera.foo',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
const hass = createHASS({
|
|
||||||
'binary_sensor.motion': createStateEntity(),
|
|
||||||
});
|
|
||||||
|
|
||||||
manager.setHASS(hass);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('selected camera is unknown', () => {
|
|
||||||
const cameraManager = createCameraManager({
|
|
||||||
configs: new Map([
|
|
||||||
[
|
|
||||||
'camera.foo',
|
|
||||||
createCameraConfig({
|
|
||||||
triggers: {
|
|
||||||
entities: ['binary_sensor.motion'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
]),
|
|
||||||
});
|
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
||||||
createView({
|
|
||||||
camera: 'camera.UNKNOWN',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
const hass = createHASS({
|
|
||||||
'binary_sensor.motion': createStateEntity(),
|
|
||||||
});
|
|
||||||
|
|
||||||
manager.setHASS(hass);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('view.update_entities changes', () => {
|
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
update_entities: ['sensor.force_default_view'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
const hass = createHASS({
|
|
||||||
'sensor.force_default_view': createStateEntity(),
|
|
||||||
});
|
|
||||||
|
|
||||||
manager.setHASS(hass);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should update card when', () => {
|
|
||||||
it('render entity changes', () => {
|
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
render_entities: ['sensor.force_update'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
const hass = createHASS({
|
|
||||||
'sensor.force_update': createStateEntity(),
|
|
||||||
});
|
|
||||||
|
|
||||||
manager.setHASS(hass);
|
|
||||||
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('media player entity changes', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getMediaPlayerManager().getMediaPlayers).mockReturnValue([
|
|
||||||
'media_player.foo',
|
|
||||||
]);
|
|
||||||
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
const hass = createHASS({
|
|
||||||
'media_player.foo': createStateEntity(),
|
|
||||||
});
|
|
||||||
|
|
||||||
manager.setHASS(hass);
|
|
||||||
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('set view default is not called when there is card interaction', () => {
|
|
||||||
const api = createAPIWithoutMediaPlayers();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
update_entities: ['sensor.force_default_view'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(true);
|
|
||||||
|
|
||||||
const manager = new HASSManager(api);
|
|
||||||
const hass = createHASS({
|
|
||||||
'sensor.force_default_view': createStateEntity(),
|
|
||||||
});
|
|
||||||
|
|
||||||
manager.setHASS(hass);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
import { loadLanguages } from '../../../src/localize/localize';
|
|
||||||
import {
|
|
||||||
InitializationAspect,
|
|
||||||
InitializationManager,
|
|
||||||
} from '../../../src/utils/card-controller/initialization-manager';
|
|
||||||
import { sideLoadHomeAssistantElements } from '../../../src/utils/ha';
|
|
||||||
import { Initializer } from '../../../src/utils/initializer/initializer';
|
|
||||||
import { createCardAPI, createConfig, createHASS } from '../../test-utils';
|
|
||||||
|
|
||||||
vi.mock('../../../src/localize/localize.js');
|
|
||||||
vi.mock('../../../src/utils/ha/index.js');
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('InitializationManager', () => {
|
|
||||||
beforeEach(async () => {
|
|
||||||
vi.resetAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not be initialized', () => {
|
|
||||||
const manager = new InitializationManager(createCardAPI());
|
|
||||||
expect(manager.isInitializedMandatory()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should initialize mandatory', () => {
|
|
||||||
it('without hass', async () => {
|
|
||||||
const manager = new InitializationManager(createCardAPI());
|
|
||||||
expect(await manager.initializeMandatory()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('without config', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new InitializationManager(api);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
expect(await manager.initializeMandatory()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('successfully', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true);
|
|
||||||
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(false);
|
|
||||||
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(false);
|
|
||||||
const manager = new InitializationManager(api);
|
|
||||||
|
|
||||||
expect(await manager.initializeMandatory()).toBeTruthy();
|
|
||||||
|
|
||||||
expect(loadLanguages).toBeCalled();
|
|
||||||
expect(sideLoadHomeAssistantElements).toBeCalled();
|
|
||||||
expect(api.getCameraManager().initializeCamerasFromConfig).toBeCalled();
|
|
||||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('successfully with querystring view', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true);
|
|
||||||
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 () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true);
|
|
||||||
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true);
|
|
||||||
vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(false);
|
|
||||||
const manager = new InitializationManager(api);
|
|
||||||
|
|
||||||
expect(await manager.initializeMandatory()).toBeTruthy();
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with languages and side load elements in progress', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
const initializer = mock<Initializer>();
|
|
||||||
const manager = new InitializationManager(api, initializer);
|
|
||||||
initializer.initializeMultipleIfNecessary.mockResolvedValue(false);
|
|
||||||
|
|
||||||
expect(await manager.initializeMandatory()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with cameras in progress', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true);
|
|
||||||
|
|
||||||
const initializer = mock<Initializer>();
|
|
||||||
const manager = new InitializationManager(api, initializer);
|
|
||||||
initializer.initializeMultipleIfNecessary.mockResolvedValue(true);
|
|
||||||
initializer.initializeIfNecessary.mockResolvedValue(false);
|
|
||||||
|
|
||||||
expect(await manager.initializeMandatory()).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should initialize background', () => {
|
|
||||||
it('without hass and config', async () => {
|
|
||||||
const manager = new InitializationManager(createCardAPI());
|
|
||||||
expect(await manager.initializeBackgroundIfNecessary()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('successfully with minimal initializers', 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: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
live: {
|
|
||||||
microphone: {
|
|
||||||
always_connected: false,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(await manager.initializeBackgroundIfNecessary()).toBeTruthy();
|
|
||||||
expect(api.getMediaPlayerManager().initialize).not.toBeCalled();
|
|
||||||
expect(api.getMicrophoneManager().connect).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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
live: {
|
|
||||||
microphone: {
|
|
||||||
always_connected: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(await manager.initializeBackgroundIfNecessary()).toBeTruthy();
|
|
||||||
expect(api.getMediaPlayerManager().initialize).toBeCalled();
|
|
||||||
expect(api.getMicrophoneManager().connect).toBeCalled();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with media player and microphone connect 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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
live: {
|
|
||||||
microphone: {
|
|
||||||
always_connected: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const initializer = mock<Initializer>();
|
|
||||||
|
|
||||||
const manager = new InitializationManager(api, initializer);
|
|
||||||
initializer.initializeMultipleIfNecessary.mockResolvedValue(false);
|
|
||||||
|
|
||||||
expect(await manager.initializeBackgroundIfNecessary()).toBeFalsy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should uninitialize', () => {
|
|
||||||
const initializer = mock<Initializer>();
|
|
||||||
const manager = new InitializationManager(createCardAPI(), initializer);
|
|
||||||
|
|
||||||
manager.uninitialize(InitializationAspect.CAMERAS);
|
|
||||||
|
|
||||||
expect(initializer.uninitialize).toBeCalledWith(InitializationAspect.CAMERAS);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
import add from 'date-fns/add';
|
|
||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { InteractionManager } from '../../../src/utils/card-controller/interaction-manager';
|
|
||||||
import { createCardAPI, createConfig } from '../../test-utils';
|
|
||||||
|
|
||||||
vi.mock('lodash-es/throttle', () => ({
|
|
||||||
default: vi.fn((fn) => fn),
|
|
||||||
}));
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('InteractionManager', () => {
|
|
||||||
const start = new Date('2023-09-24T20:20:00');
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should take action when interaction is reported', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
timeout_seconds: 10,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new InteractionManager(api);
|
|
||||||
vi.useFakeTimers();
|
|
||||||
vi.setSystemTime(start);
|
|
||||||
|
|
||||||
manager.reportInteraction();
|
|
||||||
|
|
||||||
expect(api.getTriggersManager().untrigger).toBeCalled();
|
|
||||||
expect(manager.hasInteraction()).toBeTruthy();
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
|
|
||||||
vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(false);
|
|
||||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
|
||||||
vi.runOnlyPendingTimers();
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not take action when triggered', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
timeout_seconds: 10,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new InteractionManager(api);
|
|
||||||
vi.useFakeTimers();
|
|
||||||
vi.setSystemTime(start);
|
|
||||||
|
|
||||||
manager.reportInteraction();
|
|
||||||
|
|
||||||
vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(true);
|
|
||||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
|
||||||
vi.runOnlyPendingTimers();
|
|
||||||
|
|
||||||
// First call is blocked by triggers (above), so interaction will report
|
|
||||||
// true but the default view will not have been set.
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not take action when not configured', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
timeout_seconds: 0,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new InteractionManager(api);
|
|
||||||
|
|
||||||
manager.reportInteraction();
|
|
||||||
|
|
||||||
// First call is blocked by triggers (above), so interaction will report
|
|
||||||
// true but the default view will not have been set.
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
|
||||||
import { MediaLoadedInfoManager } from '../../../src/utils/card-controller/media-info-manager';
|
|
||||||
import { createCardAPI, createMediaLoadedInfo } from '../../test-utils.js';
|
|
||||||
|
|
||||||
describe('MediaLoadedInfoManager', () => {
|
|
||||||
it('should set', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MediaLoadedInfoManager(api);
|
|
||||||
const mediaInfo = createMediaLoadedInfo();
|
|
||||||
|
|
||||||
manager.set(mediaInfo);
|
|
||||||
|
|
||||||
expect(manager.has()).toBeTruthy();
|
|
||||||
expect(manager.get()).toBe(mediaInfo);
|
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith(
|
|
||||||
expect.objectContaining({ media_loaded: true }),
|
|
||||||
);
|
|
||||||
expect(api.getStyleManager().setExpandedMode).toBeCalled();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set invalid media info', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MediaLoadedInfoManager(api);
|
|
||||||
const mediaInfo = createMediaLoadedInfo({ width: 0, height: 0 });
|
|
||||||
|
|
||||||
manager.set(mediaInfo);
|
|
||||||
|
|
||||||
expect(manager.has()).toBeFalsy();
|
|
||||||
expect(manager.get()).toBeNull();
|
|
||||||
expect(api.getConditionsManager().setState).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should get last known', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MediaLoadedInfoManager(api);
|
|
||||||
const mediaInfo = createMediaLoadedInfo();
|
|
||||||
|
|
||||||
manager.set(mediaInfo);
|
|
||||||
|
|
||||||
expect(manager.has()).toBeTruthy();
|
|
||||||
|
|
||||||
manager.clear();
|
|
||||||
|
|
||||||
expect(manager.has()).toBeFalsy();
|
|
||||||
expect(manager.getLastKnown()).toBe(mediaInfo);
|
|
||||||
expect(api.getConditionsManager().setState).toBeCalledWith(
|
|
||||||
expect.objectContaining({ media_loaded: false }),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import {
|
|
||||||
TestViewMedia,
|
|
||||||
createCameraConfig,
|
|
||||||
createCameraManager,
|
|
||||||
createCardAPI,
|
|
||||||
createHASS,
|
|
||||||
createRegistryEntity,
|
|
||||||
createStateEntity,
|
|
||||||
} from '../../test-utils';
|
|
||||||
import { MediaPlayerManager } from '../../../src/utils/card-controller/media-player-manager';
|
|
||||||
import { ExtendedHomeAssistant } from '../../../src/types';
|
|
||||||
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../../../src/const';
|
|
||||||
import { EntityRegistryManager } from '../../../src/utils/ha/entity-registry';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
|
|
||||||
vi.mock('../../../src/camera-manager/manager.js');
|
|
||||||
|
|
||||||
const createHASSWithMediaPlayers = (): ExtendedHomeAssistant => {
|
|
||||||
const attributesSupported = {
|
|
||||||
supported_features: MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA,
|
|
||||||
};
|
|
||||||
const attributesUnsupported = {
|
|
||||||
supported_features: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
return createHASS({
|
|
||||||
'media_player.ok1': createStateEntity({
|
|
||||||
entity_id: 'media_player.ok1',
|
|
||||||
state: 'on',
|
|
||||||
attributes: attributesSupported,
|
|
||||||
}),
|
|
||||||
'media_player.ok2': createStateEntity({
|
|
||||||
entity_id: 'media_player.ok2',
|
|
||||||
state: 'on',
|
|
||||||
attributes: attributesSupported,
|
|
||||||
}),
|
|
||||||
'media_player.ok3': createStateEntity({
|
|
||||||
entity_id: 'media_player.ok3',
|
|
||||||
state: 'on',
|
|
||||||
attributes: attributesSupported,
|
|
||||||
}),
|
|
||||||
'media_player.unavailable': createStateEntity({
|
|
||||||
entity_id: 'media_player.sitting_room',
|
|
||||||
state: 'unavailable',
|
|
||||||
attributes: attributesSupported,
|
|
||||||
}),
|
|
||||||
'media_player.unsupported': createStateEntity({
|
|
||||||
entity_id: 'media_player.sitting_room',
|
|
||||||
state: 'on',
|
|
||||||
attributes: attributesUnsupported,
|
|
||||||
}),
|
|
||||||
'switch.unrelated': createStateEntity({
|
|
||||||
entity_id: 'switch.unrelated',
|
|
||||||
state: 'on',
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('MediaPlayerManager', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should initialize', () => {
|
|
||||||
it('correctly', 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(),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getEntityRegistryManager).mockReturnValue(entityRegistryManager);
|
|
||||||
const manager = new MediaPlayerManager(api);
|
|
||||||
|
|
||||||
await manager.initialize();
|
|
||||||
|
|
||||||
expect(manager.getMediaPlayers()).toEqual([
|
|
||||||
'media_player.ok1',
|
|
||||||
'media_player.ok3',
|
|
||||||
]);
|
|
||||||
expect(manager.hasMediaPlayers()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('without hass', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
|
||||||
const manager = new MediaPlayerManager(api);
|
|
||||||
|
|
||||||
await manager.initialize();
|
|
||||||
|
|
||||||
expect(manager.getMediaPlayers()).toEqual([]);
|
|
||||||
expect(manager.hasMediaPlayers()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('even if entity registry call fails', async () => {
|
|
||||||
const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true);
|
|
||||||
|
|
||||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
|
||||||
entityRegistryManager.getEntities.mockRejectedValue(new Error('message'));
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
|
||||||
createHASSWithMediaPlayers(),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getEntityRegistryManager).mockReturnValue(entityRegistryManager);
|
|
||||||
const manager = new MediaPlayerManager(api);
|
|
||||||
|
|
||||||
await manager.initialize();
|
|
||||||
|
|
||||||
expect(manager.getMediaPlayers()).toEqual([
|
|
||||||
'media_player.ok1',
|
|
||||||
'media_player.ok2',
|
|
||||||
'media_player.ok3',
|
|
||||||
]);
|
|
||||||
expect(manager.hasMediaPlayers()).toBeTruthy();
|
|
||||||
expect(spy).toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should stop', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
|
|
||||||
const manager = new MediaPlayerManager(api);
|
|
||||||
|
|
||||||
await manager.stop('media_player.foo');
|
|
||||||
|
|
||||||
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
|
||||||
'media_player',
|
|
||||||
'media_stop',
|
|
||||||
{
|
|
||||||
entity_id: 'media_player.foo',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should play', () => {
|
|
||||||
describe('live', () => {
|
|
||||||
it('successfully', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
|
||||||
createCameraConfig({
|
|
||||||
camera_entity: 'camera.foo',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
|
|
||||||
title: 'camera title',
|
|
||||||
icon: 'icon',
|
|
||||||
});
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
const hass = createHASS({
|
|
||||||
'camera.foo': createStateEntity({
|
|
||||||
attributes: {
|
|
||||||
entity_picture: 'http://thumbnail',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
||||||
const manager = new MediaPlayerManager(api);
|
|
||||||
|
|
||||||
await manager.playLive('media_player.foo', 'camera');
|
|
||||||
|
|
||||||
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
|
||||||
'media_player',
|
|
||||||
'play_media',
|
|
||||||
{
|
|
||||||
entity_id: 'media_player.foo',
|
|
||||||
media_content_id: 'media-source://camera/camera.foo',
|
|
||||||
media_content_type: 'application/vnd.apple.mpegurl',
|
|
||||||
extra: {
|
|
||||||
title: 'camera title',
|
|
||||||
thumb: 'http://thumbnail',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('without camera_entity', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
|
||||||
createCameraConfig({}),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
const manager = new MediaPlayerManager(api);
|
|
||||||
|
|
||||||
await manager.playLive('media_player.foo', 'camera');
|
|
||||||
|
|
||||||
expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('without title and thumbnail', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
|
|
||||||
createCameraConfig({
|
|
||||||
camera_entity: 'camera.foo',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
const manager = new MediaPlayerManager(api);
|
|
||||||
|
|
||||||
await manager.playLive('media_player.foo', 'camera');
|
|
||||||
|
|
||||||
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
|
||||||
'media_player',
|
|
||||||
'play_media',
|
|
||||||
{
|
|
||||||
entity_id: 'media_player.foo',
|
|
||||||
media_content_id: 'media-source://camera/camera.foo',
|
|
||||||
media_content_type: 'application/vnd.apple.mpegurl',
|
|
||||||
extra: {},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('media', () => {
|
|
||||||
describe('successfully with', () => {
|
|
||||||
it.each([
|
|
||||||
['clip' as const, 'video' as const],
|
|
||||||
['snapshot' as const, 'image' as const],
|
|
||||||
])(
|
|
||||||
'%s',
|
|
||||||
async (mediaType: 'clip' | 'snapshot', contentType: 'video' | 'image') => {
|
|
||||||
const media = new TestViewMedia({
|
|
||||||
title: 'media title',
|
|
||||||
thumbnail: 'http://thumbnail',
|
|
||||||
contentID: 'media-source://contentid',
|
|
||||||
mediaType: mediaType,
|
|
||||||
});
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
const manager = new MediaPlayerManager(api);
|
|
||||||
|
|
||||||
await manager.playMedia('media_player.foo', media);
|
|
||||||
|
|
||||||
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
|
|
||||||
'media_player',
|
|
||||||
'play_media',
|
|
||||||
{
|
|
||||||
entity_id: 'media_player.foo',
|
|
||||||
media_content_id: 'media-source://contentid',
|
|
||||||
media_content_type: contentType,
|
|
||||||
extra: {
|
|
||||||
title: 'media title',
|
|
||||||
thumb: 'http://thumbnail',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('without hass', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
|
||||||
const manager = new MediaPlayerManager(api);
|
|
||||||
const media = new TestViewMedia();
|
|
||||||
|
|
||||||
await manager.playMedia('media_player.foo', media);
|
|
||||||
|
|
||||||
// No actual test can be performed here as nothing observable happens.
|
|
||||||
// This test serves only as code-coverage long-tail.
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import { afterAll, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { FrigateCardError, Message } from '../../../src/types';
|
|
||||||
import { MessageManager } from '../../../src/utils/card-controller/message-manager';
|
|
||||||
import { createCardAPI } from '../../test-utils';
|
|
||||||
|
|
||||||
const createMessage = (options?: Partial<Message>): Message => {
|
|
||||||
return {
|
|
||||||
message: options?.message ?? 'message',
|
|
||||||
type: options?.type ?? 'info',
|
|
||||||
...(!!options?.icon && { icon: options.icon }),
|
|
||||||
...(!!options?.context && { context: options.context }),
|
|
||||||
...(!!options?.dotdotdot && { dotdotdot: options.dotdotdot }),
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('MessageManager', () => {
|
|
||||||
afterAll(() => {
|
|
||||||
vi.restoreAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should construct', () => {
|
|
||||||
const manager = new MessageManager(createCardAPI());
|
|
||||||
expect(manager.hasMessage()).toBeFalsy();
|
|
||||||
expect(manager.getMessage()).toBeNull();
|
|
||||||
expect(manager.hasErrorMessage()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set info message', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MessageManager(api);
|
|
||||||
const message = createMessage();
|
|
||||||
manager.setMessageIfHigherPriority(message);
|
|
||||||
expect(manager.hasMessage()).toBeTruthy();
|
|
||||||
expect(manager.getMessage()).toBe(message);
|
|
||||||
expect(manager.hasErrorMessage()).toBeFalsy();
|
|
||||||
|
|
||||||
expect(api.getMediaLoadedInfoManager().clear).toBeCalled();
|
|
||||||
expect(api.getCardElementManager().scrollReset).toBeCalled();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set error message', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MessageManager(api);
|
|
||||||
const message = createMessage({ type: 'error' });
|
|
||||||
manager.setMessageIfHigherPriority(message);
|
|
||||||
expect(manager.hasMessage()).toBeTruthy();
|
|
||||||
expect(manager.getMessage()).toBe(message);
|
|
||||||
expect(manager.hasErrorMessage()).toBeTruthy();
|
|
||||||
|
|
||||||
expect(api.getMediaLoadedInfoManager().clear).toBeCalled();
|
|
||||||
expect(api.getCardElementManager().scrollReset).toBeCalled();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reset message', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MessageManager(api);
|
|
||||||
|
|
||||||
manager.reset();
|
|
||||||
expect(manager.hasMessage()).toBeFalsy();
|
|
||||||
|
|
||||||
const message = createMessage({ type: 'error' });
|
|
||||||
manager.setMessageIfHigherPriority(message);
|
|
||||||
expect(manager.hasMessage()).toBeTruthy();
|
|
||||||
|
|
||||||
vi.mocked(api.getCardElementManager().update).mockClear();
|
|
||||||
manager.reset();
|
|
||||||
|
|
||||||
expect(manager.hasMessage()).toBeFalsy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should respect priority', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MessageManager(api);
|
|
||||||
|
|
||||||
manager.reset();
|
|
||||||
expect(manager.hasMessage()).toBeFalsy();
|
|
||||||
|
|
||||||
const errorMessage = createMessage({ type: 'error' });
|
|
||||||
manager.setMessageIfHigherPriority(errorMessage);
|
|
||||||
|
|
||||||
const infoMessage = createMessage({ type: 'info' });
|
|
||||||
manager.setMessageIfHigherPriority(infoMessage);
|
|
||||||
|
|
||||||
expect(manager.getMessage()).toBe(errorMessage);
|
|
||||||
|
|
||||||
const connectionMessage = createMessage({ type: 'connection' });
|
|
||||||
manager.setMessageIfHigherPriority(connectionMessage);
|
|
||||||
|
|
||||||
expect(manager.getMessage()).toBe(connectionMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set FrigateCardError object', () => {
|
|
||||||
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MessageManager(api);
|
|
||||||
const context = { foo: 'bar' };
|
|
||||||
|
|
||||||
manager.setErrorIfHigherPriority(
|
|
||||||
new FrigateCardError('frigate card message', context),
|
|
||||||
);
|
|
||||||
expect(manager.hasMessage()).toBeTruthy();
|
|
||||||
expect(manager.getMessage()).toEqual({
|
|
||||||
message: 'frigate card message',
|
|
||||||
type: 'error',
|
|
||||||
context: context,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(consoleSpy).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set Error object', () => {
|
|
||||||
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MessageManager(api);
|
|
||||||
|
|
||||||
manager.setErrorIfHigherPriority(new Error('generic error message'));
|
|
||||||
expect(manager.hasMessage()).toBeTruthy();
|
|
||||||
expect(manager.getMessage()).toEqual({
|
|
||||||
message: 'generic error message',
|
|
||||||
type: 'error',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(consoleSpy).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set unknown error type', () => {
|
|
||||||
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MessageManager(api);
|
|
||||||
|
|
||||||
manager.setErrorIfHigherPriority('not_an_error_object');
|
|
||||||
expect(manager.hasMessage()).toBeFalsy();
|
|
||||||
expect(consoleSpy).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,193 +0,0 @@
|
|||||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
import { MicrophoneManager } from '../../../src/utils/card-controller/microphone-manager';
|
|
||||||
import { createCardAPI, createConfig } from '../../test-utils';
|
|
||||||
|
|
||||||
const navigatorMock = {
|
|
||||||
mediaDevices: {
|
|
||||||
getUserMedia: vi.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('MicrophoneManager', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.stubGlobal('navigator', navigatorMock);
|
|
||||||
vi.useRealTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.resetAllMocks();
|
|
||||||
vi.unstubAllGlobals;
|
|
||||||
});
|
|
||||||
|
|
||||||
const createMockStream = (mute?: boolean): MediaStream => {
|
|
||||||
const stream = mock<MediaStream>();
|
|
||||||
const track = mock<MediaStreamTrack>();
|
|
||||||
track.enabled = !mute;
|
|
||||||
stream.getTracks.mockImplementation(() => [track]);
|
|
||||||
return stream;
|
|
||||||
};
|
|
||||||
|
|
||||||
it('should be muted on creation', () => {
|
|
||||||
const manager = new MicrophoneManager(createCardAPI());
|
|
||||||
expect(manager).toBeTruthy();
|
|
||||||
expect(manager.isMuted()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be undefined without creation', () => {
|
|
||||||
const manager = new MicrophoneManager(createCardAPI());
|
|
||||||
expect(manager.getStream()).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should connect', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MicrophoneManager(api);
|
|
||||||
|
|
||||||
const stream = createMockStream();
|
|
||||||
navigatorMock.mediaDevices.getUserMedia.mockReturnValue(stream);
|
|
||||||
|
|
||||||
await manager.connect();
|
|
||||||
|
|
||||||
expect(manager.isConnected()).toBeTruthy();
|
|
||||||
expect(manager.getStream()).toBe(stream);
|
|
||||||
expect(manager.isMuted()).toBeTruthy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be forbidden when permission denied', async () => {
|
|
||||||
// Don't actually log messages to the console during the test.
|
|
||||||
vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MicrophoneManager(api);
|
|
||||||
navigatorMock.mediaDevices.getUserMedia.mockRejectedValue(new Error());
|
|
||||||
|
|
||||||
await manager.connect();
|
|
||||||
|
|
||||||
expect(manager.isConnected()).toBeFalsy();
|
|
||||||
expect(manager.isForbidden()).toBeTruthy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should mute and unmute', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MicrophoneManager(api);
|
|
||||||
navigatorMock.mediaDevices.getUserMedia.mockReturnValue(createMockStream());
|
|
||||||
|
|
||||||
await manager.connect();
|
|
||||||
expect(manager.isMuted()).toBeTruthy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
manager.mute();
|
|
||||||
expect(manager.isMuted()).toBeTruthy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(2);
|
|
||||||
|
|
||||||
await manager.unmute();
|
|
||||||
expect(manager.isMuted()).toBeFalsy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not unmute when microphone forbidden', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MicrophoneManager(api);
|
|
||||||
navigatorMock.mediaDevices.getUserMedia.mockReturnValue(null);
|
|
||||||
|
|
||||||
await manager.connect();
|
|
||||||
|
|
||||||
expect(manager.isMuted()).toBeTruthy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
await manager.unmute();
|
|
||||||
expect(manager.isMuted()).toBeTruthy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should connect on unmute', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MicrophoneManager(api);
|
|
||||||
navigatorMock.mediaDevices.getUserMedia.mockReturnValue(createMockStream());
|
|
||||||
|
|
||||||
expect(manager.isConnected()).toBeFalsy();
|
|
||||||
|
|
||||||
await manager.unmute();
|
|
||||||
|
|
||||||
expect(manager.isConnected()).toBeTruthy();
|
|
||||||
expect(manager.isMuted()).toBeFalsy();
|
|
||||||
|
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should disconnect', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MicrophoneManager(api);
|
|
||||||
|
|
||||||
navigatorMock.mediaDevices.getUserMedia.mockReturnValue(createMockStream());
|
|
||||||
|
|
||||||
await manager.connect();
|
|
||||||
expect(manager.isConnected()).toBeTruthy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
await manager.disconnect();
|
|
||||||
expect(manager.isConnected()).toBeFalsy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should automatically disconnect', async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
const disconnectSeconds = 10;
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MicrophoneManager(api);
|
|
||||||
navigatorMock.mediaDevices.getUserMedia.mockReturnValue(createMockStream());
|
|
||||||
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
live: {
|
|
||||||
microphone: {
|
|
||||||
always_connected: false,
|
|
||||||
disconnect_seconds: disconnectSeconds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await manager.connect();
|
|
||||||
expect(manager.isConnected()).toBeTruthy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
vi.advanceTimersByTime(disconnectSeconds * 1000);
|
|
||||||
|
|
||||||
expect(manager.isConnected()).toBeFalsy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not automatically disconnect when always connected', async () => {
|
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
const disconnectSeconds = 10;
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new MicrophoneManager(api);
|
|
||||||
navigatorMock.mediaDevices.getUserMedia.mockReturnValue(createMockStream());
|
|
||||||
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
live: {
|
|
||||||
microphone: {
|
|
||||||
always_connected: true,
|
|
||||||
disconnect_seconds: disconnectSeconds,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await manager.connect();
|
|
||||||
expect(manager.isConnected()).toBeTruthy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
vi.advanceTimersByTime(disconnectSeconds * 1000);
|
|
||||||
|
|
||||||
expect(manager.isConnected()).toBeTruthy();
|
|
||||||
expect(api.getCardElementManager().update).toBeCalledTimes(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { createCardAPI } from '../../test-utils';
|
|
||||||
import { QueryStringManager } from '../../../src/utils/card-controller/query-string-manager';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
|
|
||||||
const setQueryString = (qs: string): void => {
|
|
||||||
const location: Location = mock<Location>();
|
|
||||||
location.search = qs;
|
|
||||||
global.window.location = location;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('QueryStringManager', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
global.window.location = mock<Location>();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reject malformed query string', () => {
|
|
||||||
setQueryString('BOGUS_KEY=BOGUS_VALUE');
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true);
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeAll();
|
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeFalsy();
|
|
||||||
expect(api.getActionsManager().executeAction).not.toBeCalled();
|
|
||||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should execute view name action from query string', () => {
|
|
||||||
it.each([
|
|
||||||
['clip' as const],
|
|
||||||
['clips' as const],
|
|
||||||
['diagnostics' as const],
|
|
||||||
['image' as const],
|
|
||||||
['live' as const],
|
|
||||||
['recording' as const],
|
|
||||||
['recordings' as const],
|
|
||||||
['snapshot' as const],
|
|
||||||
['snapshots' as const],
|
|
||||||
['timeline' as const],
|
|
||||||
])('%s', (viewName: string) => {
|
|
||||||
setQueryString(`?frigate-card-action.id.${viewName}=`);
|
|
||||||
const api = createCardAPI();
|
|
||||||
|
|
||||||
// View actions do not need the card to have been updated.
|
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeAll();
|
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
|
||||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
|
||||||
viewName: viewName,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should execute non-view action from query string', () => {
|
|
||||||
it.each([
|
|
||||||
['camera_ui' as const],
|
|
||||||
['download' as const],
|
|
||||||
['expand' as const],
|
|
||||||
['menu_toggle' as const],
|
|
||||||
])('%s', (action: string) => {
|
|
||||||
setQueryString(`?frigate-card-action.id.${action}=`);
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeAll();
|
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeFalsy();
|
|
||||||
expect(api.getActionsManager().executeAction).toBeCalledWith({
|
|
||||||
action: 'fire-dom-event',
|
|
||||||
card_id: 'id',
|
|
||||||
frigate_card_action: action,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should execute view default action', () => {
|
|
||||||
setQueryString('?frigate-card-action.id.default=');
|
|
||||||
const api = createCardAPI();
|
|
||||||
// View actions do not need the card to have been updated.
|
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
|
|
||||||
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeAll();
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
|
||||||
expect(api.getActionsManager().executeAction).not.toBeCalled();
|
|
||||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should execute camera_select action', () => {
|
|
||||||
setQueryString('?frigate-card-action.id.camera_select=camera.office');
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeAll();
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
|
||||||
cameraID: 'camera.office',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
|
||||||
expect(api.getActionsManager().executeAction).not.toBeCalled();
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should execute live_substream_select action', () => {
|
|
||||||
setQueryString('?frigate-card-action.id.live_substream_select=camera.office_hd');
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeAll();
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
|
||||||
substream: 'camera.office_hd',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
|
||||||
expect(api.getActionsManager().executeAction).not.toBeCalled();
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should ignore action without value', () => {
|
|
||||||
it.each([['camera_select' as const], ['live_substream_select' as const]])(
|
|
||||||
'%s',
|
|
||||||
(action: string) => {
|
|
||||||
setQueryString(`?frigate-card-action.id.${action}=`);
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeAll();
|
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeFalsy();
|
|
||||||
expect(api.getActionsManager().executeAction).not.toBeCalled();
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle unknown action', () => {
|
|
||||||
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
|
||||||
|
|
||||||
setQueryString('?frigate-card-action.id.not_an_action=value');
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeAll();
|
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeFalsy();
|
|
||||||
expect(api.getActionsManager().executeAction).not.toBeCalled();
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
|
||||||
expect(consoleSpy).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should execute view name action from query string', () => {
|
|
||||||
it.each([
|
|
||||||
['clip' as const],
|
|
||||||
['clips' as const],
|
|
||||||
['diagnostics' as const],
|
|
||||||
['image' as const],
|
|
||||||
['live' as const],
|
|
||||||
['recording' as const],
|
|
||||||
['recordings' as const],
|
|
||||||
['snapshot' as const],
|
|
||||||
['snapshots' as const],
|
|
||||||
['timeline' as const],
|
|
||||||
])('%s', (viewName: string) => {
|
|
||||||
setQueryString(`?frigate-card-action.id.${viewName}=`);
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeAll();
|
|
||||||
|
|
||||||
expect(manager.hasViewRelatedActions()).toBeTruthy();
|
|
||||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
|
||||||
viewName: viewName,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should not execute non-view actions without an initial update', () => {
|
|
||||||
it.each([
|
|
||||||
['camera_ui' as const],
|
|
||||||
['download' as const],
|
|
||||||
['expand' as const],
|
|
||||||
['menu_toggle' as const],
|
|
||||||
])('%s', (action: string) => {
|
|
||||||
setQueryString(`?frigate-card-action.id.${action}=value`);
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(false);
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeAll();
|
|
||||||
|
|
||||||
expect(api.getActionsManager().executeAction).not.toBeCalled();
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should handle conflicting but valid actions', () => {
|
|
||||||
it('view and default with camera and substream specified', () => {
|
|
||||||
setQueryString(
|
|
||||||
'?frigate-card-action.id.clips=' +
|
|
||||||
'&frigate-card-action.id.live_substream_select=camera.kitchen_hd' +
|
|
||||||
'&frigate-card-action.id.default=' +
|
|
||||||
'&frigate-card-action.id.camera_select=camera.kitchen',
|
|
||||||
);
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeAll();
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewDefault).toBeCalledWith({
|
|
||||||
cameraID: 'camera.kitchen',
|
|
||||||
substream: 'camera.kitchen_hd',
|
|
||||||
});
|
|
||||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('multiple cameras specified', () => {
|
|
||||||
setQueryString(
|
|
||||||
'?frigate-card-action.id.camera_select=camera.kitchen' +
|
|
||||||
'&frigate-card-action.id.camera_select=camera.office',
|
|
||||||
);
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeAll();
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
|
||||||
cameraID: 'camera.office',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should not execute view related actions', () => {
|
|
||||||
it.each([
|
|
||||||
['clip' as const],
|
|
||||||
['clips' as const],
|
|
||||||
['default' as const],
|
|
||||||
['diagnostics' as const],
|
|
||||||
['image' as const],
|
|
||||||
['live' as const],
|
|
||||||
['recording' as const],
|
|
||||||
['recordings' as const],
|
|
||||||
['snapshot' as const],
|
|
||||||
['snapshots' as const],
|
|
||||||
['timeline' as const],
|
|
||||||
])('%s', (viewName: string) => {
|
|
||||||
setQueryString(`?frigate-card-action.id.${viewName}=`);
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeNonViewRelated();
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should not execute non-view related actions', () => {
|
|
||||||
it.each([
|
|
||||||
['camera_ui' as const],
|
|
||||||
['download' as const],
|
|
||||||
['expand' as const],
|
|
||||||
['menu_toggle' as const],
|
|
||||||
])('%s', (viewName: string) => {
|
|
||||||
setQueryString(`?frigate-card-action.id.${viewName}=`);
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCardElementManager().hasUpdated).mockReturnValue(true);
|
|
||||||
const manager = new QueryStringManager(api);
|
|
||||||
|
|
||||||
manager.executeViewRelated();
|
|
||||||
|
|
||||||
expect(api.getActionsManager().executeAction).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,387 +0,0 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { FrigateCardView } from '../../../src/config/types';
|
|
||||||
import { setPerformanceCSSStyles } from '../../../src/performance';
|
|
||||||
import { StyleManager } from '../../../src/utils/card-controller/style-manager';
|
|
||||||
import { createCardAPI, createConfig, createHASS, createView } from '../../test-utils';
|
|
||||||
|
|
||||||
vi.mock('../../../src/performance');
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('StyleManager', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.resetAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('setLightOrDarkMode', () => {
|
|
||||||
it('dark mode unspecified', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
manager.setLightOrDarkMode();
|
|
||||||
|
|
||||||
expect(element.getAttribute('dark')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dark mode explicitly off', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
dark_mode: 'off',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
manager.setLightOrDarkMode();
|
|
||||||
|
|
||||||
expect(element.getAttribute('dark')).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dark mode explicitly set', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
dark_mode: 'on',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
manager.setLightOrDarkMode();
|
|
||||||
|
|
||||||
expect(element.getAttribute('dark')).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dark mode auto without interaction', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
dark_mode: 'auto',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
manager.setLightOrDarkMode();
|
|
||||||
|
|
||||||
expect(element.getAttribute('dark')).not.toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('dark mode auto with HA dark mode', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
dark_mode: 'auto',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(true);
|
|
||||||
const hass = createHASS();
|
|
||||||
hass.themes.darkMode = true;
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
manager.setLightOrDarkMode();
|
|
||||||
|
|
||||||
expect(element.getAttribute('dark')).not.toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('setExpandedMode', () => {
|
|
||||||
it('with no view or known media', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().getLastKnown).mockReturnValue(null);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
manager.setExpandedMode();
|
|
||||||
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-expand-aspect-ratio')).toBe(
|
|
||||||
'unset',
|
|
||||||
);
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-expand-width')).toBe(
|
|
||||||
'var(--frigate-card-expand-max-width)',
|
|
||||||
);
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-expand-height')).toBe(
|
|
||||||
'var(--frigate-card-expand-max-height)',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with view but without media', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
||||||
const view = createView({ view: 'media', displayMode: 'single' });
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().getLastKnown).mockReturnValue(null);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
manager.setExpandedMode();
|
|
||||||
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-expand-aspect-ratio')).toBe(
|
|
||||||
'unset',
|
|
||||||
);
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-expand-width')).toBe('none');
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-expand-height')).toBe(
|
|
||||||
'none',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with view and media', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
||||||
const view = createView({ view: 'media', displayMode: 'single' });
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().getLastKnown).mockReturnValue({
|
|
||||||
width: 800,
|
|
||||||
height: 600,
|
|
||||||
});
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
manager.setExpandedMode();
|
|
||||||
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-expand-aspect-ratio')).toBe(
|
|
||||||
'800 / 600',
|
|
||||||
);
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-expand-width')).toBe('none');
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-expand-height')).toBe(
|
|
||||||
'none',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with view and grid display mode', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
||||||
const view = createView({ view: 'media', displayMode: 'grid' });
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().getLastKnown).mockReturnValue({
|
|
||||||
width: 800,
|
|
||||||
height: 600,
|
|
||||||
});
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
manager.setExpandedMode();
|
|
||||||
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-expand-aspect-ratio')).toBe(
|
|
||||||
'800 / 600',
|
|
||||||
);
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-expand-width')).toBe(
|
|
||||||
'var(--frigate-card-expand-max-width)',
|
|
||||||
);
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-expand-height')).toBe(
|
|
||||||
'var(--frigate-card-expand-max-height)',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('setMinMaxHeight', () => {
|
|
||||||
it('without a config', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
manager.setMinMaxHeight();
|
|
||||||
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-max-height')).toBeFalsy();
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-expand-height')).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('with a config', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
dimensions: {
|
|
||||||
max_height: '800px',
|
|
||||||
min_height: '400px',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
manager.setMinMaxHeight();
|
|
||||||
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-min-height')).toBe('400px');
|
|
||||||
expect(element.style.getPropertyValue('--frigate-card-max-height')).toBe('800px');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('setPerformance', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const element = document.createElement('div');
|
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
|
||||||
const config = createConfig();
|
|
||||||
vi.mocked(api.getConfigManager().getCardWideConfig).mockReturnValue({
|
|
||||||
performance: config.performance,
|
|
||||||
});
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
manager.setPerformance();
|
|
||||||
|
|
||||||
expect(setPerformanceCSSStyles).toBeCalledWith(element, config.performance);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getAspectRatioStyle', () => {
|
|
||||||
it('without config or view', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
expect(manager.getAspectRatioStyle()).toBe('16 / 9');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be auto with unconstrained aspect ratio', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const view = createView({ view: 'media' });
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
dimensions: {
|
|
||||||
aspect_ratio_mode: 'unconstrained',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
expect(manager.getAspectRatioStyle()).toBe('auto');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be auto in fullscreen', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const view = createView({ view: 'media' });
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getFullscreenManager().isInFullscreen).mockReturnValue(true);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
expect(manager.getAspectRatioStyle()).toBe('auto');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be auto when expanded', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const view = createView({ view: 'media' });
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getExpandManager().isExpanded).mockReturnValue(true);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
expect(manager.getAspectRatioStyle()).toBe('auto');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be auto when there is yet to be a view', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(null);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
expect(manager.getAspectRatioStyle()).toBe('auto');
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should be auto when dynamic in certain views', () => {
|
|
||||||
it.each([
|
|
||||||
['clip' as const],
|
|
||||||
['diagnostics' as const],
|
|
||||||
['image' as const],
|
|
||||||
['media' as const],
|
|
||||||
['live' as const],
|
|
||||||
['recording' as const],
|
|
||||||
['snapshot' as const],
|
|
||||||
['timeline' as const],
|
|
||||||
])('%s', (viewName: FrigateCardView) => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const view = createView({ view: viewName });
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
dimensions: {
|
|
||||||
aspect_ratio_mode: 'dynamic',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
expect(manager.getAspectRatioStyle()).toBe('auto');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should be enforced when dynamic in certain views', () => {
|
|
||||||
it.each([['clips' as const], ['recordings' as const], ['snapshots' as const]])(
|
|
||||||
'%s',
|
|
||||||
(viewName: FrigateCardView) => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const view = createView({ view: viewName });
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
dimensions: {
|
|
||||||
aspect_ratio_mode: 'dynamic',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
expect(manager.getAspectRatioStyle()).toBe('16 / 9');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should use media dimensions in dynamic', () => {
|
|
||||||
it.each([['clips' as const], ['recordings' as const], ['snapshots' as const]])(
|
|
||||||
'%s',
|
|
||||||
(viewName: FrigateCardView) => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const view = createView({ view: viewName });
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
dimensions: {
|
|
||||||
aspect_ratio_mode: 'dynamic',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().getLastKnown).mockReturnValue({
|
|
||||||
width: 800,
|
|
||||||
height: 600,
|
|
||||||
});
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
expect(manager.getAspectRatioStyle()).toBe('800 / 600');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should respect default aspect ratio', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const view = createView({ view: 'clips' });
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
dimensions: {
|
|
||||||
aspect_ratio_mode: 'dynamic',
|
|
||||||
aspect_ratio: '4:3',
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new StyleManager(api);
|
|
||||||
|
|
||||||
expect(manager.getAspectRatioStyle()).toBe('4 / 3');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
import add from 'date-fns/add';
|
|
||||||
import { HassEntities } from 'home-assistant-js-websocket';
|
|
||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { ScanOptions } from '../../../src/config/types';
|
|
||||||
import { TriggersManager } from '../../../src/utils/card-controller/triggers-manager';
|
|
||||||
import {
|
|
||||||
createCameraConfig,
|
|
||||||
createCameraManager,
|
|
||||||
createCardAPI,
|
|
||||||
createConfig,
|
|
||||||
createHASS,
|
|
||||||
createStateEntity,
|
|
||||||
createView,
|
|
||||||
} from '../../test-utils';
|
|
||||||
|
|
||||||
vi.mock('../../../src/camera-manager/manager.js');
|
|
||||||
|
|
||||||
// Creating and mocking a trigger API is a lot of boilerplate, this convenience
|
|
||||||
// function reduces it.
|
|
||||||
const createTriggerAPI = (options?: {
|
|
||||||
config?: Partial<ScanOptions>;
|
|
||||||
hassStates?: HassEntities;
|
|
||||||
interaction?: boolean;
|
|
||||||
}) => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
scan: options?.config ?? {
|
|
||||||
enabled: true,
|
|
||||||
untrigger_reset: true,
|
|
||||||
untrigger_seconds: 10,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
|
||||||
createHASS(options?.hassStates),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(
|
|
||||||
createCameraManager({
|
|
||||||
configs: new Map([
|
|
||||||
[
|
|
||||||
'camera_1',
|
|
||||||
createCameraConfig({
|
|
||||||
triggers: {
|
|
||||||
entities: ['binary_sensor.motion'],
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
]),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(
|
|
||||||
options?.interaction ?? false,
|
|
||||||
);
|
|
||||||
|
|
||||||
return api;
|
|
||||||
};
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('TriggersManager', () => {
|
|
||||||
const hassActiveState = {
|
|
||||||
'binary_sensor.motion': createStateEntity({ state: 'on' }),
|
|
||||||
};
|
|
||||||
const hassInactiveState = {
|
|
||||||
'binary_sensor.motion': createStateEntity({ state: 'off' }),
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.resetAllMocks();
|
|
||||||
vi.useFakeTimers();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not be triggered by default', () => {
|
|
||||||
const manager = new TriggersManager(createCardAPI());
|
|
||||||
expect(manager.isTriggered()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not trigger when scan mode disabled default', () => {
|
|
||||||
const api = createTriggerAPI({
|
|
||||||
config: { enabled: false },
|
|
||||||
hassStates: hassActiveState,
|
|
||||||
});
|
|
||||||
const manager = new TriggersManager(api);
|
|
||||||
|
|
||||||
manager.updateTriggeredCameras(null);
|
|
||||||
|
|
||||||
expect(manager.isTriggered()).toBeFalsy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should trigger and untrigger based on entity state', () => {
|
|
||||||
const start = new Date('2023-10-01T17:14');
|
|
||||||
vi.setSystemTime(start);
|
|
||||||
const api = createTriggerAPI({
|
|
||||||
hassStates: hassActiveState,
|
|
||||||
});
|
|
||||||
const manager = new TriggersManager(api);
|
|
||||||
|
|
||||||
manager.updateTriggeredCameras(createHASS(hassInactiveState));
|
|
||||||
|
|
||||||
expect(manager.isTriggered()).toBeTruthy();
|
|
||||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
|
||||||
viewName: 'live',
|
|
||||||
cameraID: 'camera_1',
|
|
||||||
});
|
|
||||||
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(
|
|
||||||
createHASS(hassInactiveState),
|
|
||||||
);
|
|
||||||
|
|
||||||
manager.updateTriggeredCameras(createHASS(hassActiveState));
|
|
||||||
|
|
||||||
// Intentional state update with no change.
|
|
||||||
manager.updateTriggeredCameras(createHASS(hassActiveState));
|
|
||||||
|
|
||||||
// Will still be triggered, but untrigger timer will be running.
|
|
||||||
expect(manager.isTriggered()).toBeTruthy();
|
|
||||||
|
|
||||||
vi.setSystemTime(add(start, { seconds: 10 }));
|
|
||||||
vi.runOnlyPendingTimers();
|
|
||||||
|
|
||||||
expect(manager.isTriggered()).toBeFalsy();
|
|
||||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should trigger and set view if current view is wrong', () => {
|
|
||||||
const api = createTriggerAPI({
|
|
||||||
hassStates: hassActiveState,
|
|
||||||
});
|
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
|
||||||
createView({
|
|
||||||
// Correct camera, but wrong view.
|
|
||||||
view: 'clips',
|
|
||||||
camera: 'camera_1',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new TriggersManager(api);
|
|
||||||
manager.updateTriggeredCameras(null);
|
|
||||||
|
|
||||||
expect(api.getViewManager().setViewByParameters).toBeCalledWith({
|
|
||||||
viewName: 'live',
|
|
||||||
cameraID: 'camera_1',
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should trigger when entity state is active on startup', () => {
|
|
||||||
const api = createTriggerAPI({
|
|
||||||
hassStates: hassActiveState,
|
|
||||||
});
|
|
||||||
const manager = new TriggersManager(api);
|
|
||||||
expect(manager.isTriggered()).toBeFalsy();
|
|
||||||
|
|
||||||
manager.updateTriggeredCameras(null);
|
|
||||||
expect(manager.isTriggered()).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should untrigger manually', () => {
|
|
||||||
const api = createTriggerAPI({
|
|
||||||
hassStates: hassActiveState,
|
|
||||||
});
|
|
||||||
const manager = new TriggersManager(api);
|
|
||||||
|
|
||||||
// Untriggering when not triggered.
|
|
||||||
manager.untrigger();
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
|
|
||||||
manager.updateTriggeredCameras(null);
|
|
||||||
expect(manager.isTriggered()).toBeTruthy();
|
|
||||||
|
|
||||||
manager.untrigger();
|
|
||||||
expect(manager.isTriggered()).toBeFalsy();
|
|
||||||
expect(api.getViewManager().setViewDefault).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should take no actions when automated actions are not allowed', () => {
|
|
||||||
const api = createTriggerAPI({
|
|
||||||
hassStates: hassActiveState,
|
|
||||||
// Interaction present.
|
|
||||||
interaction: true,
|
|
||||||
});
|
|
||||||
const manager = new TriggersManager(api);
|
|
||||||
manager.updateTriggeredCameras(null);
|
|
||||||
expect(manager.isTriggered()).toBeTruthy();
|
|
||||||
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
|
||||||
|
|
||||||
manager.untrigger();
|
|
||||||
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,698 +0,0 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
|
||||||
import { QueryType } from '../../../src/camera-manager/types';
|
|
||||||
import { FrigateCardView } from '../../../src/config/types';
|
|
||||||
import { getAllDependentCameras } from '../../../src/utils/camera';
|
|
||||||
import { ViewManager } from '../../../src/utils/card-controller/view-manager';
|
|
||||||
import { EventMediaQueries } from '../../../src/view/media-queries';
|
|
||||||
import {
|
|
||||||
createCameraManager,
|
|
||||||
createCardAPI,
|
|
||||||
createConfig,
|
|
||||||
createHASS,
|
|
||||||
createView,
|
|
||||||
generateViewMediaArray,
|
|
||||||
} from '../../test-utils';
|
|
||||||
|
|
||||||
vi.mock('../../../src/camera-manager/manager.js');
|
|
||||||
vi.mock('../../../src/utils/camera');
|
|
||||||
|
|
||||||
describe('ViewManager.setView', () => {
|
|
||||||
it('should set view', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
|
|
||||||
const view = createView({
|
|
||||||
view: 'live',
|
|
||||||
camera: 'camera',
|
|
||||||
displayMode: 'grid',
|
|
||||||
});
|
|
||||||
manager.setView(view);
|
|
||||||
|
|
||||||
expect(manager.getView()).toBe(view);
|
|
||||||
expect(api.getMediaLoadedInfoManager().clear).toBeCalled();
|
|
||||||
expect(api.getCardElementManager().scrollReset).toBeCalled();
|
|
||||||
expect(api.getMessageManager().reset).toBeCalled();
|
|
||||||
expect(api.getStyleManager().setExpandedMode).toBeCalled();
|
|
||||||
expect(api.getConditionsManager()?.setState).toBeCalledWith({
|
|
||||||
view: 'live',
|
|
||||||
camera: 'camera',
|
|
||||||
displayMode: 'grid',
|
|
||||||
});
|
|
||||||
expect(api.getCardElementManager().update).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set view with minor changes without media clearing or scroll', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
|
|
||||||
const view_1 = createView({
|
|
||||||
view: 'live',
|
|
||||||
camera: 'camera',
|
|
||||||
});
|
|
||||||
manager.setView(view_1);
|
|
||||||
|
|
||||||
vi.mocked(api.getMediaLoadedInfoManager().clear).mockClear();
|
|
||||||
vi.mocked(api.getCardElementManager().scrollReset).mockClear();
|
|
||||||
|
|
||||||
const view_2 = createView({
|
|
||||||
view: 'live',
|
|
||||||
camera: 'camera',
|
|
||||||
displayMode: 'single',
|
|
||||||
});
|
|
||||||
|
|
||||||
manager.setView(view_2);
|
|
||||||
|
|
||||||
expect(manager.getView()).toBe(view_2);
|
|
||||||
|
|
||||||
// The new view is neither a major media change, nor a different view name,
|
|
||||||
// so media clearing and scrolling should not happen.
|
|
||||||
expect(api.getMediaLoadedInfoManager().clear).not.toBeCalled();
|
|
||||||
expect(api.getCardElementManager().scrollReset).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set view with new context', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
const context = { thumbnails: { fetch: false } };
|
|
||||||
|
|
||||||
// Setting context with no existing view does nothing.
|
|
||||||
manager.setViewWithNewContext(context);
|
|
||||||
expect(manager.getView()).toBeNull();
|
|
||||||
|
|
||||||
const view = createView({
|
|
||||||
view: 'live',
|
|
||||||
camera: 'camera',
|
|
||||||
});
|
|
||||||
manager.setView(view);
|
|
||||||
manager.setViewWithNewContext(context);
|
|
||||||
|
|
||||||
expect(manager.getView()?.camera).toBe('camera');
|
|
||||||
expect(manager.getView()?.view).toBe('live');
|
|
||||||
expect(manager.getView()?.context).toEqual(context);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ViewManager.reset', () => {
|
|
||||||
it('should reset', () => {
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
|
|
||||||
const view = createView();
|
|
||||||
manager.setView(view);
|
|
||||||
manager.reset();
|
|
||||||
|
|
||||||
expect(manager.getView()).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ViewManager.setViewDefault', () => {
|
|
||||||
it('should set default view', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
manager.setViewDefault();
|
|
||||||
|
|
||||||
expect(manager.getView()?.view).toBe('live');
|
|
||||||
expect(manager.getView()?.camera).toBe('camera');
|
|
||||||
expect(api.getAutoUpdateManager().startDefaultViewTimer).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set default view without config', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(null);
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
manager.setViewDefault();
|
|
||||||
|
|
||||||
expect(manager.getView()).toBeNull();
|
|
||||||
expect(api.getAutoUpdateManager().startDefaultViewTimer).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should cycle camera when configured', () => {
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
view: {
|
|
||||||
update_cycle_camera: true,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
|
|
||||||
manager.setViewDefault();
|
|
||||||
expect(manager.getView()?.camera).toBe('camera_1');
|
|
||||||
|
|
||||||
manager.setViewDefault();
|
|
||||||
expect(manager.getView()?.camera).toBe('camera_2');
|
|
||||||
|
|
||||||
manager.setViewDefault();
|
|
||||||
expect(manager.getView()?.camera).toBe('camera_1');
|
|
||||||
|
|
||||||
// When a parameter is specified, it will not cycle.
|
|
||||||
manager.setViewDefault({ cameraID: 'camera_1' });
|
|
||||||
expect(manager.getView()?.camera).toBe('camera_1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should respect parameters', () => {
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera.kitchen', 'camera.office']),
|
|
||||||
);
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
|
|
||||||
manager.setViewDefault({
|
|
||||||
cameraID: 'camera.office',
|
|
||||||
substream: 'camera.office_hd',
|
|
||||||
});
|
|
||||||
expect(manager.getView()?.view).toBe('live');
|
|
||||||
expect(manager.getView()?.camera).toBe('camera.office');
|
|
||||||
expect(manager.getView()?.context?.live?.overrides).toEqual(
|
|
||||||
new Map([['camera.office', 'camera.office_hd']]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ViewManager.setViewByParameters', () => {
|
|
||||||
it('should set view by parameters specifying camera and view', () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
manager.setViewByParameters({
|
|
||||||
cameraID: 'camera',
|
|
||||||
viewName: 'clips',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(manager.getView()?.view).toBe('clips');
|
|
||||||
expect(manager.getView()?.camera).toBe('camera');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set view by parameters using existing view if unspecified', () => {
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
manager.setViewByParameters({
|
|
||||||
cameraID: 'camera_1',
|
|
||||||
viewName: 'clips',
|
|
||||||
});
|
|
||||||
|
|
||||||
manager.setViewByParameters({
|
|
||||||
cameraID: 'camera_2',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(manager.getView()?.view).toBe('clips');
|
|
||||||
expect(manager.getView()?.camera).toBe('camera_2');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set view by parameters using config as fallback', () => {
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
manager.setViewByParameters({
|
|
||||||
cameraID: 'camera_1',
|
|
||||||
// No prior view, and no specified view. This could happen during query
|
|
||||||
// string based initialization.
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(manager.getView()?.view).toBe('live');
|
|
||||||
expect(manager.getView()?.camera).toBe('camera_1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set view by parameters without config', () => {
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
|
|
||||||
manager.setViewByParameters({
|
|
||||||
viewName: 'live',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(manager.getView()).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set view by parameters without visible cameras', () => {
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(new Set());
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
manager.setViewByParameters({
|
|
||||||
viewName: 'live',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(manager.getView()).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should set view by parameters and respect display mode in config for view', () => {
|
|
||||||
it.each([
|
|
||||||
['media' as const],
|
|
||||||
['clip' as const],
|
|
||||||
['recording' as const],
|
|
||||||
['snapshot' as const],
|
|
||||||
['live' as const],
|
|
||||||
])('%s', (viewName: FrigateCardView) => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
|
||||||
vi.mocked(api.getConfigManager()).getConfig.mockReturnValue(
|
|
||||||
createConfig({
|
|
||||||
media_viewer: {
|
|
||||||
display: {
|
|
||||||
mode: 'grid',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
live: {
|
|
||||||
display: {
|
|
||||||
mode: 'grid',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
|
|
||||||
manager.setViewByParameters({
|
|
||||||
cameraID: 'camera',
|
|
||||||
viewName: viewName,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(manager.getView()?.displayMode).toBe('grid');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should set view by parameters and leave display mode unset for view', () => {
|
|
||||||
it.each([
|
|
||||||
['media' as const],
|
|
||||||
['clip' as const],
|
|
||||||
['recording' as const],
|
|
||||||
['snapshot' as const],
|
|
||||||
['live' as const],
|
|
||||||
])('%s', (viewName: FrigateCardView) => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
|
|
||||||
manager.setViewByParameters({
|
|
||||||
cameraID: 'camera',
|
|
||||||
viewName: viewName,
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(manager.getView()?.displayMode).toBe('single');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set view by parameters using config as fallback', () => {
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(getAllDependentCameras).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_1_hd']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
manager.setViewByParameters({
|
|
||||||
cameraID: 'camera_1',
|
|
||||||
viewName: 'live',
|
|
||||||
substream: 'camera_1_hd',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(manager.getView()?.view).toBe('live');
|
|
||||||
expect(manager.getView()?.camera).toBe('camera_1');
|
|
||||||
expect(manager.getView()?.context?.live?.overrides).toEqual(
|
|
||||||
new Map([['camera_1', 'camera_1_hd']]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
|
||||||
describe('ViewManager.setViewWithNewDisplayMode', () => {
|
|
||||||
it('should set display mode', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
manager.setView(createView());
|
|
||||||
|
|
||||||
await manager.setViewWithNewDisplayMode('grid');
|
|
||||||
|
|
||||||
expect(manager.getView()?.displayMode).toBe('grid');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set display mode without view', async () => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager());
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
|
|
||||||
manager.setViewWithNewDisplayMode('grid');
|
|
||||||
|
|
||||||
expect(manager.getView()).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set display mode to grid and create new query', async () => {
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2);
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
|
|
||||||
const hass = createHASS();
|
|
||||||
vi.mocked(api.getHASSManager()).getHASS.mockReturnValue(hass);
|
|
||||||
|
|
||||||
const media = generateViewMediaArray({ count: 5 });
|
|
||||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(media);
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
const query = new EventMediaQueries([
|
|
||||||
{ type: QueryType.Event, cameraIDs: new Set(['camera_1']), hasClip: true },
|
|
||||||
]);
|
|
||||||
|
|
||||||
manager.setView(
|
|
||||||
createView({
|
|
||||||
camera: 'camera_1',
|
|
||||||
view: 'clip',
|
|
||||||
query: query,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await manager.setViewWithNewDisplayMode('grid');
|
|
||||||
|
|
||||||
expect(manager.getView()?.queryResults?.getResults()).toBe(media);
|
|
||||||
expect(cameraManager.executeMediaQueries).toBeCalledWith(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'event-query',
|
|
||||||
cameraIDs: new Set(['camera_1', 'camera_2']),
|
|
||||||
hasClip: true,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set display mode to single and create new query', async () => {
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2);
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
|
|
||||||
const hass = createHASS();
|
|
||||||
vi.mocked(api.getHASSManager()).getHASS.mockReturnValue(hass);
|
|
||||||
|
|
||||||
const media = generateViewMediaArray({ count: 5 });
|
|
||||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(media);
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
const query = new EventMediaQueries([
|
|
||||||
{
|
|
||||||
type: QueryType.Event,
|
|
||||||
cameraIDs: new Set(['camera_1', 'camera_2']),
|
|
||||||
hasClip: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
manager.setView(
|
|
||||||
createView({
|
|
||||||
view: 'clip',
|
|
||||||
camera: 'camera_2',
|
|
||||||
query: query,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
await manager.setViewWithNewDisplayMode('single');
|
|
||||||
|
|
||||||
expect(manager.getView()?.queryResults?.getResults()).toBe(media);
|
|
||||||
expect(cameraManager.executeMediaQueries).toBeCalledWith(
|
|
||||||
expect.arrayContaining([
|
|
||||||
expect.objectContaining({
|
|
||||||
type: 'event-query',
|
|
||||||
cameraIDs: new Set(['camera_2']),
|
|
||||||
hasClip: true,
|
|
||||||
}),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set display mode to single and handle failed new query', async () => {
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2);
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
const query = new EventMediaQueries([
|
|
||||||
{
|
|
||||||
type: QueryType.Event,
|
|
||||||
cameraIDs: new Set(['camera_1', 'camera_2']),
|
|
||||||
hasClip: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const originalView = createView({
|
|
||||||
view: 'clip',
|
|
||||||
camera: 'camera_2',
|
|
||||||
query: query,
|
|
||||||
});
|
|
||||||
manager.setView(originalView);
|
|
||||||
|
|
||||||
// Query execution fails / returns null.
|
|
||||||
vi.mocked(cameraManager.executeMediaQueries).mockRejectedValue(null);
|
|
||||||
|
|
||||||
await manager.setViewWithNewDisplayMode('single');
|
|
||||||
|
|
||||||
expect(manager.getView()).toBe(originalView);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set display mode and handle empty new query results', async () => {
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraCount).mockReturnValue(2);
|
|
||||||
vi.mocked(cameraManager.getStore().getVisibleCameraIDs).mockReturnValue(
|
|
||||||
new Set(['camera_1', 'camera_2']),
|
|
||||||
);
|
|
||||||
|
|
||||||
const api = createCardAPI();
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
|
||||||
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
|
|
||||||
const query = new EventMediaQueries([
|
|
||||||
{
|
|
||||||
type: QueryType.Event,
|
|
||||||
cameraIDs: new Set(['camera_1']),
|
|
||||||
hasClip: true,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
const originalView = createView({
|
|
||||||
view: 'clip',
|
|
||||||
camera: 'camera_2',
|
|
||||||
query: query,
|
|
||||||
});
|
|
||||||
manager.setView(originalView);
|
|
||||||
|
|
||||||
await manager.setViewWithNewDisplayMode('grid');
|
|
||||||
|
|
||||||
vi.mocked(cameraManager.executeMediaQueries).mockResolvedValue(null);
|
|
||||||
|
|
||||||
// Empty queries will not be executed, so view will not be changed.
|
|
||||||
expect(manager.getView()?.displayMode).toBeNull();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ViewManager.setViewWithSubstream', () => {
|
|
||||||
it('should set new equal view with no dependencies', () => {
|
|
||||||
const view = createView({
|
|
||||||
view: 'live',
|
|
||||||
camera: 'camera',
|
|
||||||
});
|
|
||||||
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera']));
|
|
||||||
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
manager.setView(view);
|
|
||||||
manager.setViewWithSubstream();
|
|
||||||
|
|
||||||
expect(manager.getView()?.camera).toBe(view.camera);
|
|
||||||
expect(manager.getView()?.view).toBe(view.view);
|
|
||||||
expect(manager.getView()?.context).toEqual(view.context);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set new view with next substream', () => {
|
|
||||||
const view = createView({
|
|
||||||
view: 'live',
|
|
||||||
camera: 'camera',
|
|
||||||
});
|
|
||||||
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2']));
|
|
||||||
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
manager.setView(view);
|
|
||||||
manager.setViewWithSubstream();
|
|
||||||
|
|
||||||
expect(manager.getView()?.context?.live?.overrides).toEqual(
|
|
||||||
new Map([['camera', 'camera2']]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set new view with next substream when view has invalid substream', () => {
|
|
||||||
const view = createView({
|
|
||||||
view: 'live',
|
|
||||||
camera: 'camera',
|
|
||||||
context: {
|
|
||||||
live: {
|
|
||||||
overrides: new Map([['camera', 'camera-that-does-not-exist']]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
vi.mocked(getAllDependentCameras).mockReturnValue(new Set(['camera', 'camera2']));
|
|
||||||
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
manager.setView(view);
|
|
||||||
manager.setViewWithSubstream();
|
|
||||||
|
|
||||||
expect(manager.getView()?.context?.live?.overrides).toEqual(
|
|
||||||
new Map([['camera', 'camera']]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set new view with selected substream', () => {
|
|
||||||
const view = createView({
|
|
||||||
view: 'live',
|
|
||||||
camera: 'camera',
|
|
||||||
});
|
|
||||||
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
manager.setView(view);
|
|
||||||
manager.setViewWithSubstream('substream');
|
|
||||||
|
|
||||||
expect(manager.getView()?.context?.live?.overrides).toEqual(
|
|
||||||
new Map([['camera', 'substream']]),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set view with next substream without an existing view', () => {
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
manager.setViewWithSubstream();
|
|
||||||
expect(manager.getView()).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set view with selected substream without an existing view', () => {
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
manager.setViewWithSubstream('substream');
|
|
||||||
expect(manager.getView()).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set view without substream without an existing view', () => {
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
manager.setViewWithoutSubstream();
|
|
||||||
expect(manager.getView()).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set new view without substream', () => {
|
|
||||||
const view = createView({
|
|
||||||
view: 'live',
|
|
||||||
camera: 'camera',
|
|
||||||
context: {
|
|
||||||
live: {
|
|
||||||
overrides: new Map([['camera', 'camera']]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
manager.setView(view);
|
|
||||||
manager.setViewWithoutSubstream();
|
|
||||||
|
|
||||||
expect(manager.getView()?.context?.live?.overrides).toEqual(new Map());
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set new view without substream', () => {
|
|
||||||
const view = createView({
|
|
||||||
view: 'live',
|
|
||||||
camera: 'camera',
|
|
||||||
context: {
|
|
||||||
live: {
|
|
||||||
overrides: new Map([['camera-2', 'camera-3']]),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const manager = new ViewManager(createCardAPI());
|
|
||||||
manager.setView(view);
|
|
||||||
manager.setViewWithoutSubstream();
|
|
||||||
|
|
||||||
expect(manager.getView()?.context?.live?.overrides).toEqual(
|
|
||||||
view.context?.live?.overrides,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ViewManager.isViewSupportedByCamera', () => {
|
|
||||||
it.each([
|
|
||||||
['live' as const, true],
|
|
||||||
['image' as const, true],
|
|
||||||
['diagnostics' as const, true],
|
|
||||||
['clip' as const, false],
|
|
||||||
['clips' as const, false],
|
|
||||||
['snapshot' as const, false],
|
|
||||||
['snapshots' as const, false],
|
|
||||||
['recording' as const, false],
|
|
||||||
['recordings' as const, false],
|
|
||||||
['timeline' as const, false],
|
|
||||||
['media' as const, false],
|
|
||||||
])('%s', (viewName: FrigateCardView, expected: boolean) => {
|
|
||||||
const api = createCardAPI();
|
|
||||||
const cameraManager = createCameraManager();
|
|
||||||
vi.mocked(cameraManager.getCameraCapabilities).mockReturnValue({
|
|
||||||
canFavoriteEvents: false,
|
|
||||||
canFavoriteRecordings: false,
|
|
||||||
canSeek: false,
|
|
||||||
supportsClips: false,
|
|
||||||
supportsRecordings: false,
|
|
||||||
supportsSnapshots: false,
|
|
||||||
supportsTimeline: false,
|
|
||||||
});
|
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
|
||||||
const manager = new ViewManager(api);
|
|
||||||
|
|
||||||
expect(manager.isViewSupportedByCamera('camera', viewName)).toBe(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -7,8 +7,8 @@ import { CameraManagerCameraMetadata } from '../../src/camera-manager/types';
|
|||||||
import { FrigateCardConfig, MenuItem, ViewDisplayMode } from '../../src/config/types';
|
import { FrigateCardConfig, MenuItem, ViewDisplayMode } from '../../src/config/types';
|
||||||
import { FrigateCardMediaPlayer } from '../../src/types';
|
import { FrigateCardMediaPlayer } from '../../src/types';
|
||||||
import { createFrigateCardCustomAction } from '../../src/utils/action';
|
import { createFrigateCardCustomAction } from '../../src/utils/action';
|
||||||
import { MediaPlayerManager } from '../../src/utils/card-controller/media-player-manager';
|
import { MediaPlayerManager } from '../../src/card-controller/media-player-manager';
|
||||||
import { MicrophoneManager } from '../../src/utils/card-controller/microphone-manager';
|
import { MicrophoneManager } from '../../src/card-controller/microphone-manager';
|
||||||
import {
|
import {
|
||||||
MenuButtonController,
|
MenuButtonController,
|
||||||
MenuButtonControllerOptions,
|
MenuButtonControllerOptions,
|
||||||
@@ -31,7 +31,7 @@ import {
|
|||||||
|
|
||||||
vi.mock('../../src/camera-manager/manager.js');
|
vi.mock('../../src/camera-manager/manager.js');
|
||||||
vi.mock('../../src/utils/media-player-controller.js');
|
vi.mock('../../src/utils/media-player-controller.js');
|
||||||
vi.mock('../../src/utils/card-controller/microphone-manager.js');
|
vi.mock('../../src/card-controller/microphone-manager.js');
|
||||||
|
|
||||||
const calculateButtons = (
|
const calculateButtons = (
|
||||||
controller: MenuButtonController,
|
controller: MenuButtonController,
|
||||||
|
|||||||
Reference in New Issue
Block a user