fix: Confirmations should apply to all actions (#1959)

This also refactors how generic HA actions are handled, and improves
typing of actions.


[skip ci]
This commit is contained in:
Dermot Duffy
2025-03-15 14:32:48 -07:00
committed by GitHub
parent a79ffa6edc
commit 75ae7720d3
90 changed files with 1332 additions and 854 deletions
+28 -13
View File
@@ -1,12 +1,19 @@
import { ActionContext } from 'action';
import { z } from 'zod';
import { ConditionsTriggerData } from '../../conditions/types.js';
import { Actions, ActionsConfig, ActionType } from '../../config/types.js';
import {
ActionConfig,
Actions,
ActionsConfig,
AuxillaryActionConfig,
} from '../../config/types.js';
import { forwardHaptic } from '../../ha/haptic.js';
import { getActionConfigGivenAction } from '../../utils/action.js';
import { allPromises } from '../../utils/basic.js';
import { TemplateRenderer } from '../templates/index.js';
import { CardActionsManagerAPI } from '../types.js';
import { ActionSet } from './actions/set.js';
import { ActionExecutionRequest, AuxillaryActionConfig } from './types.js';
import { ActionExecutionRequest } from './types.js';
const INTERACTIONS = ['tap', 'double_tap', 'hold', 'start_tap', 'end_tap'] as const;
export type InteractionName = (typeof INTERACTIONS)[number];
@@ -60,7 +67,7 @@ export class ActionsManager {
/**
* Handle an human interaction called on an element (e.g. 'tap').
*/
public handleInteractionEvent = (ev: Event): void => {
public handleInteractionEvent = async (ev: Event): Promise<void> => {
const result = interactionEventSchema.safeParse(ev);
if (!result.success) {
return;
@@ -76,7 +83,7 @@ export class ActionsManager {
// actions).
actionConfig
) {
this.executeActions(actionConfig, { config });
await this.executeActions(actionConfig, { config });
}
};
@@ -85,14 +92,16 @@ export class ActionsManager {
* cards to fire custom actions. This card itself should not call this, but
* embedded elements may.
*/
public handleCustomActionEvent = (ev: Event): void => {
public handleCustomActionEvent = async (
ev: Event | CustomEvent<ActionConfig>,
): Promise<void> => {
if (!('detail' in ev)) {
// The event may or may not be a CustomEvent object. For example, whilst
// this card doesn't use custom-card-helpers, embedded elements may:
// https://github.com/custom-cards/custom-card-helpers/blob/master/src/fire-event.ts#L70
return;
}
this.executeActions(ev.detail as ActionType);
await this.executeActions(ev.detail as ActionConfig);
};
/**
@@ -107,25 +116,25 @@ export class ActionsManager {
});
};
public uninitialize(): void {
public async uninitialize(): Promise<void> {
// If there are any long-running actions, ensure they are stopped.
this._actionsInFlight.forEach((actionSet) => actionSet.stop());
await allPromises(this._actionsInFlight, (actionSet) => actionSet.stop());
}
public async executeActions(
action: ActionType | ActionType[],
action: ActionConfig | ActionConfig[],
options?: {
config?: AuxillaryActionConfig;
triggerData?: ConditionsTriggerData;
},
): Promise<void> {
const hass = this._api.getHASSManager().getHASS();
const renderedAction =
const renderedAction: ActionConfig | ActionConfig[] =
hass && this._templateRenderer
? this._templateRenderer.renderRecursively(hass, action, {
? (this._templateRenderer.renderRecursively(hass, action, {
conditionState: this._api.getConditionStateManager().getState(),
triggerData: options?.triggerData,
})
}) as ActionConfig | ActionConfig[])
: action;
const actionSet = new ActionSet(this._actionContext, renderedAction, {
@@ -134,7 +143,13 @@ export class ActionsManager {
});
this._actionsInFlight.push(actionSet);
await actionSet.execute(this._api);
try {
await actionSet.execute(this._api);
forwardHaptic('success');
} catch (e) {
forwardHaptic('warning');
}
this._actionsInFlight = this._actionsInFlight.filter((a) => a !== actionSet);
}
}
+32 -9
View File
@@ -1,9 +1,11 @@
import { ActionContext } from 'action';
import { AdvancedCameraCardCustomAction } from '../../../config/types';
import { ActionConfig, AuxillaryActionConfig } from '../../../config/types';
import { localize } from '../../../localize/localize.js';
import { isAdvancedCameraCardCustomAction } from '../../../utils/action';
import { CardActionsAPI } from '../../types';
import { Action, AuxillaryActionConfig } from '../types';
import { Action, ActionAbortError } from '../types';
export class BaseAction<T> implements Action {
export class BaseAction<T extends ActionConfig> implements Action {
protected _context: ActionContext;
protected _action: T;
protected _config?: AuxillaryActionConfig;
@@ -14,9 +16,32 @@ export class BaseAction<T> implements Action {
this._config = config;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async execute(_api: CardActionsAPI): Promise<void> {
// Pass.
protected _shouldSeekConfirmation(api: CardActionsAPI): boolean {
const hass = api.getHASSManager().getHASS();
return (
(typeof this._action.confirmation === 'boolean' && this._action.confirmation) ||
(typeof this._action.confirmation === 'object' &&
(!this._action.confirmation.exemptions ||
!this._action.confirmation.exemptions.some(
(entry) => entry.user === hass?.user.id,
)))
);
}
public async execute(api: CardActionsAPI): Promise<void> {
if (this._shouldSeekConfirmation(api)) {
const actionName = isAdvancedCameraCardCustomAction(this._action)
? this._action.advanced_camera_card_action
: this._action.action;
const text =
(typeof this._action.confirmation === 'object'
? this._action.confirmation.text
: null) ?? `${localize('actions.confirmation')}: ${actionName}`;
if (!confirm(text)) {
throw new ActionAbortError(localize('actions.abort'));
}
}
}
public async stop(): Promise<void> {
@@ -24,6 +49,4 @@ export class BaseAction<T> implements Action {
}
}
export class AdvancedCameraCardAction<
T extends AdvancedCameraCardCustomAction,
> extends BaseAction<T> {}
export class AdvancedCameraCardAction<T extends ActionConfig> extends BaseAction<T> {}
@@ -0,0 +1,17 @@
import { CallServiceActionConfig } from '../../../config/types';
import { CardActionsAPI } from '../../types';
import { AdvancedCameraCardAction } from './base';
export class CallServiceAction extends AdvancedCameraCardAction<CallServiceActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
const hass = api.getHASSManager().getHASS();
if (!hass) {
return;
}
const [domain, service] = this._action.service.split('.', 2);
await hass.callService(domain, service, this._action.data, this._action.target);
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class CameraSelectAction extends AdvancedCameraCardAction<CameraSelectActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
const selectCameraID =
this._action.camera ??
(this._action.triggered
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class CameraUIAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
api.getCameraURLManager().openURL();
}
}
@@ -0,0 +1,12 @@
import { CustomActionConfig } from '../../../config/types';
import { fireHASSEvent } from '../../../ha/fire-hass-event';
import { CardActionsAPI } from '../../types';
import { AdvancedCameraCardAction } from './base';
export class CustomAction extends AdvancedCameraCardAction<CustomActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
fireHASSEvent(api.getCardElementManager().getElement(), 'll-custom', this._action);
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class DefaultAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
await api.getViewManager().setViewDefaultWithNewQuery();
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class DisplayModeSelectAction extends AdvancedCameraCardAction<DisplayModeActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
await api.getViewManager().setViewByParametersWithNewQuery({
params: {
displayMode: this._action.display_mode,
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class DownloadAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
await api.getDownloadManager().downloadViewerMedia();
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class ExpandAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
api.getExpandManager().toggleExpanded();
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class FullscreenAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
api.getFullscreenManager().toggleFullscreen();
}
}
@@ -1,21 +0,0 @@
import { handleActionConfig } from '../../../ha/handle-action';
import { ActionConfig } from '../../../ha/types';
import { CardActionsAPI } from '../../types';
import { BaseAction } from './base';
/**
* Handles generic HA actions (e.g. 'more-info')
*/
export class GenericAction extends BaseAction<ActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
const hass = api.getHASSManager().getHASS();
if (hass) {
handleActionConfig(
api.getCardElementManager().getElement(),
hass,
this._config ?? {},
this._action,
);
}
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class InternalCallbackAction extends AdvancedCameraCardAction<InternalCallbackActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
await this._action.callback(api);
}
}
+3 -2
View File
@@ -3,8 +3,9 @@ import { CardActionsAPI } from '../../types';
import { AdvancedCameraCardAction } from './base';
export class LogAction extends AdvancedCameraCardAction<LogActionConfig> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async execute(_api: CardActionsAPI): Promise<void> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
console[this._action.level](this._action.message);
}
}
@@ -5,6 +5,8 @@ import { AdvancedCameraCardAction } from './base';
export class MediaPlayerAction extends AdvancedCameraCardAction<MediaPlayerActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
const mediaPlayer = this._action.media_player;
const mediaPlayerController = api.getMediaPlayerManager();
const view = api.getViewManager().getView();
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class MenuToggleAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
api.getCardElementManager().toggleMenu();
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class MicrophoneConnectAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
await api.getMicrophoneManager().connect();
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class MicrophoneDisconnectAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
api.getMicrophoneManager().disconnect();
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class MicrophoneMuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
api.getMicrophoneManager().mute();
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class MicrophoneUnmuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
await api.getMicrophoneManager().unmute();
}
}
@@ -0,0 +1,19 @@
import { MoreInfoActionConfig } from '../../../config/types';
import { fireHASSEvent } from '../../../ha/fire-hass-event';
import { CardActionsAPI } from '../../types';
import { AdvancedCameraCardAction } from './base';
export class MoreInfoAction extends AdvancedCameraCardAction<MoreInfoActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
const entityID = this._action.entity ?? this._config?.entity ?? null;
if (!entityID) {
return;
}
fireHASSEvent(api.getCardElementManager().getElement(), 'hass-more-info', {
entityId: entityID,
});
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class MuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
await api.getMediaLoadedInfoManager().get()?.mediaPlayerController?.mute();
}
}
@@ -0,0 +1,19 @@
import { NavigateActionConfig } from '../../../config/types';
import { fireHASSEvent } from '../../../ha/fire-hass-event';
import { CardActionsAPI } from '../../types';
import { AdvancedCameraCardAction } from './base';
export class NavigateAction extends AdvancedCameraCardAction<NavigateActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
if (!!this._action.navigation_replace) {
history.replaceState(null, '', this._action.navigation_path);
} else {
history.pushState(null, '', this._action.navigation_path);
}
fireHASSEvent(window, 'location-changed', {
replace: !!this._action.navigation_replace,
});
}
}
@@ -0,0 +1,9 @@
import { NoneActionConfig } from '../../../config/types';
import { CardActionsAPI } from '../../types';
import { AdvancedCameraCardAction } from './base';
export class NoneAction extends AdvancedCameraCardAction<NoneActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class PauseAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
await api.getMediaLoadedInfoManager().get()?.mediaPlayerController?.pause();
}
}
@@ -0,0 +1,17 @@
import { PerformActionActionConfig } from '../../../config/types';
import { CardActionsAPI } from '../../types';
import { AdvancedCameraCardAction } from './base';
export class PerformActionAction extends AdvancedCameraCardAction<PerformActionActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
const hass = api.getHASSManager().getHASS();
if (!hass) {
return;
}
const [domain, service] = this._action.perform_action.split('.', 2);
await hass.callService(domain, service, this._action.data, this._action.target);
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class PlayAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
await api.getMediaLoadedInfoManager().get()?.mediaPlayerController?.play();
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class PTZControlsAction extends AdvancedCameraCardAction<PTZControlsActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
api.getViewManager().setViewWithMergedContext({
ptzControls: { enabled: this._action.enabled },
});
@@ -45,6 +45,8 @@ export class PTZDigitalAction extends AdvancedCameraCardAction<PTZDigitialAction
}
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
const view = api.getViewManager().getView();
if (!view) {
return;
@@ -8,6 +8,8 @@ import { PTZDigitalAction } from './ptz-digital';
export class PTZMultiAction extends AdvancedCameraCardAction<PTZMultiActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
const view = api.getViewManager().getView();
let targetID: string | null = null;
let type: PTZType | null = null;
@@ -28,6 +28,8 @@ export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
}
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
const view = api.getViewManager().getView();
if (!view) {
return;
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class ScreenshotAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
await api.getDownloadManager().downloadScreenshot();
}
}
+3 -3
View File
@@ -1,9 +1,9 @@
import { ActionContext } from 'action';
import { ActionType } from '../../../config/types';
import { ActionConfig, AuxillaryActionConfig } from '../../../config/types';
import { arrayify } from '../../../utils/basic';
import { CardActionsAPI } from '../../types';
import { ActionFactory } from '../factory';
import { Action, AuxillaryActionConfig } from '../types';
import { Action } from '../types';
export class ActionSet implements Action {
protected _context: ActionContext;
@@ -13,7 +13,7 @@ export class ActionSet implements Action {
constructor(
context: ActionContext,
actions: ActionType | ActionType[],
actions: ActionConfig | ActionConfig[],
options?: {
config?: AuxillaryActionConfig;
cardID?: string;
+3 -2
View File
@@ -5,8 +5,9 @@ import { timeDeltaToSeconds } from '../utils/time-delta';
import { AdvancedCameraCardAction } from './base';
export class SleepAction extends AdvancedCameraCardAction<SleepActionConfig> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async execute(_api: CardActionsAPI): Promise<void> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
await sleep(timeDeltaToSeconds(this._action.duration));
}
}
@@ -5,6 +5,8 @@ import { AdvancedCameraCardAction } from './base';
export class StatusBarAction extends AdvancedCameraCardAction<StatusBarActionConfig> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
switch (this._action.status_bar_action) {
case 'reset':
api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
@@ -5,6 +5,8 @@ import { AdvancedCameraCardAction } from './base';
export class SubstreamOffAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
api.getViewManager().setViewByParameters({
modifiers: [new SubstreamOffViewModifier()],
});
@@ -5,6 +5,8 @@ import { AdvancedCameraCardAction } from './base';
export class SubstreamOnAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
api.getViewManager().setViewByParameters({
modifiers: [new SubstreamOnViewModifier(api)],
});
@@ -5,6 +5,8 @@ import { AdvancedCameraCardAction } from './base';
export class SubstreamSelectAction extends AdvancedCameraCardAction<SubstreamSelectActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
api.getViewManager().setViewByParameters({
modifiers: [new SubstreamSelectViewModifier(this._action.camera)],
});
@@ -0,0 +1,38 @@
import { ToggleActionConfig } from '../../../config/types';
import { computeDomain } from '../../../ha/compute-domain';
import { STATES_OFF } from '../../../ha/const';
import { CardActionsAPI } from '../../types';
import { AdvancedCameraCardAction } from './base';
export class ToggleAction extends AdvancedCameraCardAction<ToggleActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
const hass = api.getHASSManager().getHASS();
const entityID = this._config?.entity;
if (!hass || !entityID) {
return;
}
const entityState = hass.states[entityID]?.state;
if (!entityState) {
return;
}
const turnOn = STATES_OFF.includes(entityState);
const stateDomain = computeDomain(entityID);
const serviceDomain = stateDomain === 'group' ? 'homeassistant' : stateDomain;
const service =
stateDomain === 'lock'
? turnOn
? 'unlock'
: 'lock'
: stateDomain === 'cover'
? turnOn
? 'open_cover'
: 'close_cover'
: turnOn
? 'turn_on'
: 'turn_off';
await hass.callService(serviceDomain, service, { entity_id: entityID });
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class UnmuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
await api.getMediaLoadedInfoManager().get()?.mediaPlayerController?.unmute();
}
}
@@ -0,0 +1,11 @@
import { URLActionConfig } from '../../../config/types';
import { CardActionsAPI } from '../../types';
import { AdvancedCameraCardAction } from './base';
export class URLAction extends AdvancedCameraCardAction<URLActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
window.open(this._action.url_path);
}
}
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
export class ViewAction extends AdvancedCameraCardAction<ViewActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await super.execute(api);
await api.getViewManager().setViewByParametersWithNewQuery({
params: {
view: this._action.advanced_camera_card_action,
+71 -53
View File
@@ -1,15 +1,19 @@
import { ActionContext } from 'action';
import { ActionType, INTERNAL_CALLBACK_ACTION } from '../../config/types';
import { ActionConfig } from '../../ha/types';
import { convertActionToCardCustomAction } from '../../utils/action';
import {
ActionConfig,
AuxillaryActionConfig,
INTERNAL_CALLBACK_ACTION,
} from '../../config/types';
import { isAdvancedCameraCardCustomAction } from '../../utils/action';
import { CallServiceAction } from './actions/call-service';
import { CameraSelectAction } from './actions/camera-select';
import { CameraUIAction } from './actions/camera-ui';
import { CustomAction } from './actions/custom';
import { DefaultAction } from './actions/default';
import { DisplayModeSelectAction } from './actions/display-mode-select';
import { DownloadAction } from './actions/download';
import { ExpandAction } from './actions/expand';
import { FullscreenAction } from './actions/fullscreen';
import { GenericAction } from './actions/generic';
import { InternalCallbackAction } from './actions/internal-callback';
import { LogAction } from './actions/log';
import { MediaPlayerAction } from './actions/media-player';
@@ -18,8 +22,12 @@ import { MicrophoneConnectAction } from './actions/microphone-connect';
import { MicrophoneDisconnectAction } from './actions/microphone-disconnect';
import { MicrophoneMuteAction } from './actions/microphone-mute';
import { MicrophoneUnmuteAction } from './actions/microphone-unmute';
import { MoreInfoAction } from './actions/more-info';
import { MuteAction } from './actions/mute';
import { NavigateAction } from './actions/navigate';
import { NoneAction } from './actions/none';
import { PauseAction } from './actions/pause';
import { PerformActionAction } from './actions/perform-action';
import { PlayAction } from './actions/play';
import { PTZAction } from './actions/ptz';
import { PTZControlsAction } from './actions/ptz-controls';
@@ -31,39 +39,53 @@ import { StatusBarAction } from './actions/status-bar';
import { SubstreamOffAction } from './actions/substream-off';
import { SubstreamOnAction } from './actions/substream-on';
import { SubstreamSelectAction } from './actions/substream-select';
import { ToggleAction } from './actions/toggle';
import { UnmuteAction } from './actions/unmute';
import { URLAction } from './actions/url';
import { ViewAction } from './actions/view';
import { Action, AuxillaryActionConfig } from './types';
import { Action } from './types';
export class ActionFactory {
public createAction(
context: ActionContext,
action: ActionType,
action: ActionConfig,
options?: {
config?: AuxillaryActionConfig;
cardID?: string;
},
): Action | null {
const cardCustomAction = convertActionToCardCustomAction(action);
if (action.action !== 'fire-dom-event' || !cardCustomAction) {
// * There is a slight typing (but not functional) difference between
// ActionType in this card and ActionConfig in `custom-card-helpers`. See
// `ExtendedConfirmationRestrictionConfig` in `types.ts` for the source and
// reason behind this difference.
return new GenericAction(context, action as ActionConfig, options?.config);
}
if (
// Command not intended for this card (e.g. query string command).
cardCustomAction.card_id &&
cardCustomAction.card_id !== options?.cardID
action.card_id &&
action.card_id !== options?.cardID
) {
return null;
}
switch (cardCustomAction.advanced_camera_card_action) {
switch (action.action) {
case 'more-info':
return new MoreInfoAction(context, action, options?.config);
case 'toggle':
return new ToggleAction(context, action, options?.config);
case 'navigate':
return new NavigateAction(context, action, options?.config);
case 'url':
return new URLAction(context, action, options?.config);
case 'perform-action':
return new PerformActionAction(context, action, options?.config);
case 'call-service':
return new CallServiceAction(context, action, options?.config);
case 'none':
return new NoneAction(context, action, options?.config);
}
if (!isAdvancedCameraCardCustomAction(action)) {
return new CustomAction(context, action, options?.config);
}
switch (action.advanced_camera_card_action) {
case 'default':
return new DefaultAction(context, cardCustomAction, options?.config);
return new DefaultAction(context, action, options?.config);
case 'clip':
case 'clips':
case 'image':
@@ -74,72 +96,68 @@ export class ActionFactory {
case 'snapshots':
case 'timeline':
case 'diagnostics':
return new ViewAction(context, cardCustomAction, options?.config);
return new ViewAction(context, action, options?.config);
case 'sleep':
return new SleepAction(context, cardCustomAction, options?.config);
return new SleepAction(context, action, options?.config);
case 'download':
return new DownloadAction(context, cardCustomAction, options?.config);
return new DownloadAction(context, action, options?.config);
case 'camera_ui':
return new CameraUIAction(context, cardCustomAction, options?.config);
return new CameraUIAction(context, action, options?.config);
case 'expand':
return new ExpandAction(context, cardCustomAction, options?.config);
return new ExpandAction(context, action, options?.config);
case 'fullscreen':
return new FullscreenAction(context, cardCustomAction, options?.config);
return new FullscreenAction(context, action, options?.config);
case 'menu_toggle':
return new MenuToggleAction(context, cardCustomAction, options?.config);
return new MenuToggleAction(context, action, options?.config);
case 'camera_select':
return new CameraSelectAction(context, cardCustomAction, options?.config);
return new CameraSelectAction(context, action, options?.config);
case 'live_substream_select':
return new SubstreamSelectAction(context, cardCustomAction, options?.config);
return new SubstreamSelectAction(context, action, options?.config);
case 'live_substream_off':
return new SubstreamOffAction(context, cardCustomAction, options?.config);
return new SubstreamOffAction(context, action, options?.config);
case 'live_substream_on':
return new SubstreamOnAction(context, cardCustomAction, options?.config);
return new SubstreamOnAction(context, action, options?.config);
case 'media_player':
return new MediaPlayerAction(context, cardCustomAction, options?.config);
return new MediaPlayerAction(context, action, options?.config);
case 'microphone_connect':
return new MicrophoneConnectAction(context, cardCustomAction, options?.config);
return new MicrophoneConnectAction(context, action, options?.config);
case 'microphone_disconnect':
return new MicrophoneDisconnectAction(
context,
cardCustomAction,
options?.config,
);
return new MicrophoneDisconnectAction(context, action, options?.config);
case 'microphone_mute':
return new MicrophoneMuteAction(context, cardCustomAction, options?.config);
return new MicrophoneMuteAction(context, action, options?.config);
case 'microphone_unmute':
return new MicrophoneUnmuteAction(context, cardCustomAction, options?.config);
return new MicrophoneUnmuteAction(context, action, options?.config);
case 'mute':
return new MuteAction(context, cardCustomAction, options?.config);
return new MuteAction(context, action, options?.config);
case 'unmute':
return new UnmuteAction(context, cardCustomAction, options?.config);
return new UnmuteAction(context, action, options?.config);
case 'play':
return new PlayAction(context, cardCustomAction, options?.config);
return new PlayAction(context, action, options?.config);
case 'pause':
return new PauseAction(context, cardCustomAction, options?.config);
return new PauseAction(context, action, options?.config);
case 'screenshot':
return new ScreenshotAction(context, cardCustomAction, options?.config);
return new ScreenshotAction(context, action, options?.config);
case 'display_mode_select':
return new DisplayModeSelectAction(context, cardCustomAction, options?.config);
return new DisplayModeSelectAction(context, action, options?.config);
case 'ptz':
return new PTZAction(context, cardCustomAction, options?.config);
return new PTZAction(context, action, options?.config);
case 'ptz_digital':
return new PTZDigitalAction(context, cardCustomAction, options?.config);
return new PTZDigitalAction(context, action, options?.config);
case 'ptz_multi':
return new PTZMultiAction(context, cardCustomAction, options?.config);
return new PTZMultiAction(context, action, options?.config);
case 'ptz_controls':
return new PTZControlsAction(context, cardCustomAction, options?.config);
return new PTZControlsAction(context, action, options?.config);
case 'log':
return new LogAction(context, cardCustomAction, options?.config);
return new LogAction(context, action, options?.config);
case 'status_bar':
return new StatusBarAction(context, cardCustomAction, options?.config);
return new StatusBarAction(context, action, options?.config);
case INTERNAL_CALLBACK_ACTION:
return new InternalCallbackAction(context, cardCustomAction, options?.config);
return new InternalCallbackAction(context, action, options?.config);
}
/* istanbul ignore next: this path cannot be reached -- @preserve */
console.warn(
`Advanced Camera Card received unknown card action: ${cardCustomAction['advanced_camera_card_action']}`,
`Advanced Camera Card received unknown card action: ${action['advanced_camera_card_action']}`,
);
/* istanbul ignore next: this path cannot be reached -- @preserve */
return null;
+5 -7
View File
@@ -1,18 +1,14 @@
import { ActionType } from '../../config/types';
import { AdvancedCameraCardError } from '../../types.js';
import { ActionConfig, AuxillaryActionConfig } from '../../config/types';
import { CardActionsAPI } from '../types';
export interface AuxillaryActionConfig {
camera_image?: string;
entity?: string;
}
export interface Action {
execute(api: CardActionsAPI): Promise<void>;
stop(): Promise<void>;
}
export interface ActionExecutionRequest {
action: ActionType[] | ActionType;
action: ActionConfig[] | ActionConfig;
config?: AuxillaryActionConfig;
}
@@ -21,3 +17,5 @@ export interface TargetedActionContext {
inProgressAction?: Action;
};
}
export class ActionAbortError extends AdvancedCameraCardError {}
+14 -14
View File
@@ -1,4 +1,4 @@
import { AdvancedCameraCardCustomAction, ViewActionConfig } from '../config/types';
import { AdvancedCameraCardCustomActionConfig, ViewActionConfig } from '../config/types';
import {
createCameraAction,
createGeneralAction,
@@ -13,7 +13,7 @@ interface QueryStringViewIntent {
default?: boolean;
substream?: string;
};
other?: AdvancedCameraCardCustomAction[];
other?: AdvancedCameraCardCustomActionConfig[];
}
export class QueryStringManager {
@@ -92,9 +92,9 @@ export class QueryStringManager {
return result;
}
protected _getActions(): AdvancedCameraCardCustomAction[] {
protected _getActions(): AdvancedCameraCardCustomActionConfig[] {
const params = new URLSearchParams(window.location.search);
const actions: AdvancedCameraCardCustomAction[] = [];
const actions: AdvancedCameraCardCustomActionConfig[] = [];
const actionRE = new RegExp(
/^(advanced-camera-card|frigate-card)-action([.:](?<cardID>\w+))?[.:](?<action>\w+)/,
);
@@ -104,14 +104,14 @@ export class QueryStringManager {
continue;
}
const cardID: string | undefined = match.groups['cardID'];
const action = match.groups['action'];
const actionName = match.groups['action'];
let customAction: AdvancedCameraCardCustomAction | null = null;
switch (action) {
let action: AdvancedCameraCardCustomActionConfig | null = null;
switch (actionName) {
case 'camera_select':
case 'live_substream_select':
if (value) {
customAction = createCameraAction(action, value, {
action = createCameraAction(actionName, value, {
cardID: cardID,
});
}
@@ -121,7 +121,7 @@ export class QueryStringManager {
case 'download':
case 'expand':
case 'menu_toggle':
customAction = createGeneralAction(action, {
action = createGeneralAction(actionName, {
cardID: cardID,
});
break;
@@ -135,24 +135,24 @@ export class QueryStringManager {
case 'snapshot':
case 'snapshots':
case 'timeline':
customAction = createViewAction(action, {
action = createViewAction(actionName, {
cardID: cardID,
});
break;
default:
console.warn(
`Advanced Camera Card received unknown card action in query string: ${action}`,
`Advanced Camera Card received unknown card action in query string: ${actionName}`,
);
}
if (customAction) {
actions.push(customAction);
if (action) {
actions.push(action);
}
}
return actions;
}
protected _isViewAction = (
action: AdvancedCameraCardCustomAction,
action: AdvancedCameraCardCustomActionConfig,
): action is ViewActionConfig => {
switch (action.advanced_camera_card_action) {
case 'clip':
+9 -6
View File
@@ -1,6 +1,5 @@
import { HASS, renderTemplate } from 'ha-nunjucks/dist';
import { ConditionState, ConditionsTriggerData } from '../../conditions/types';
import { ActionType } from '../../config/types';
import { HomeAssistant } from '../../ha/types';
interface TemplateContextInternal {
@@ -24,7 +23,7 @@ export class TemplateRenderer {
conditionState?: ConditionState;
triggerData?: ConditionsTriggerData;
},
): ActionType => {
): unknown => {
return this._renderTemplateRecursively(
hass,
data,
@@ -59,11 +58,15 @@ export class TemplateRenderer {
hass: HomeAssistant,
data: unknown,
templateContext?: TemplateContext,
): ActionType {
): unknown {
if (typeof data === 'string') {
// ha-nunjucks has a more complete model of the Home Assistant object, but
// does not export it as a type.
return renderTemplate(hass as unknown as typeof HASS, data, templateContext);
return renderTemplate(
// ha-nunjucks has a more complete model of the Home Assistant object, but
// does not export it as a type.
hass as unknown as typeof HASS,
data,
templateContext,
);
} else if (Array.isArray(data)) {
return data.map((item) =>
this._renderTemplateRecursively(hass, item, templateContext),