Skip to content

Commit

Permalink
Move card-controller out of utils.
Browse files Browse the repository at this point in the history
  • Loading branch information
dermotduffy committed Oct 4, 2023
1 parent 913355a commit ed91c87
Show file tree
Hide file tree
Showing 45 changed files with 8,008 additions and 0 deletions.
223 changes: 223 additions & 0 deletions src/card-controller/actions-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
import {
Actions,
ActionsConfig,
FrigateCardCustomAction,
FRIGATE_CARD_VIEW_DEFAULT,
} from '../config/types.js';
import {
convertActionToFrigateCardCustomAction,
frigateCardHandleActionConfig,
getActionConfigGivenAction,
} from '../utils/action.js';
import { getStreamCameraID } from '../utils/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}`);
}
}
}
43 changes: 43 additions & 0 deletions src/card-controller/auto-update-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Timer } from '../utils/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())
);
}
}
69 changes: 69 additions & 0 deletions src/card-controller/automations-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Automation, AutomationActions, Automations } from '../config/types.js';
import { localize } from '../localize/localize.js';
import { frigateCardHandleAction } from '../utils/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;
}
}
32 changes: 32 additions & 0 deletions src/card-controller/camera-url-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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;
}
}
Loading

0 comments on commit ed91c87

Please sign in to comment.