generated from custom-cards/boilerplate-card
-
-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
913355a
commit ed91c87
Showing
45 changed files
with
8,008 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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}`); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
} |
Oops, something went wrong.