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:
@@ -1,5 +1,5 @@
|
||||
import { PTZAction, PTZBaseAction } from '../../config/ptz';
|
||||
import { ActionPhase, ActionType, CameraConfig } from '../../config/types';
|
||||
import { ActionConfig, ActionPhase, CameraConfig } from '../../config/types';
|
||||
import { PTZCapabilities, PTZMovementType } from '../../types';
|
||||
|
||||
export const getConfiguredPTZAction = (
|
||||
@@ -9,7 +9,7 @@ export const getConfiguredPTZAction = (
|
||||
phase?: ActionPhase;
|
||||
preset?: string;
|
||||
},
|
||||
): ActionType | ActionType[] | null => {
|
||||
): ActionConfig | ActionConfig[] | null => {
|
||||
if (action === 'preset') {
|
||||
return (options?.preset ? cameraConfig.ptz.presets?.[options.preset] : null) ?? null;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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':
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -6,7 +6,6 @@ import { MicrophoneManager } from '../card-controller/microphone-manager';
|
||||
import { ViewManager } from '../card-controller/view/view-manager';
|
||||
import {
|
||||
AdvancedCameraCardConfig,
|
||||
AdvancedCameraCardCustomAction,
|
||||
MenuItem,
|
||||
VIEWS_USER_SPECIFIED,
|
||||
} from '../config/types';
|
||||
@@ -21,8 +20,9 @@ import {
|
||||
createPTZControlsAction,
|
||||
createPTZMultiAction,
|
||||
createViewAction,
|
||||
isAdvancedCameraCardCustomAction,
|
||||
} from '../utils/action';
|
||||
import { isTruthy } from '../utils/basic';
|
||||
import { arrayify, isTruthy } from '../utils/basic';
|
||||
import { isBeingCasted } from '../utils/casting';
|
||||
import { getEntityTitle } from '../utils/ha';
|
||||
import { getPTZTarget } from '../utils/ptz';
|
||||
@@ -118,9 +118,9 @@ export class MenuButtonController {
|
||||
permanent: true,
|
||||
tap_action:
|
||||
config.menu?.style === 'hidden'
|
||||
? (createGeneralAction('menu_toggle') as AdvancedCameraCardCustomAction)
|
||||
: (createGeneralAction('default') as AdvancedCameraCardCustomAction),
|
||||
hold_action: createViewAction('diagnostics') as AdvancedCameraCardCustomAction,
|
||||
? createGeneralAction('menu_toggle')
|
||||
: createGeneralAction('default'),
|
||||
hold_action: createViewAction('diagnostics'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ export class MenuButtonController {
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
tap_action: createGeneralAction(
|
||||
hasSubstream(view) ? 'live_substream_off' : 'live_substream_on',
|
||||
) as AdvancedCameraCardCustomAction,
|
||||
),
|
||||
};
|
||||
} else if (streams.length > 2) {
|
||||
const menuItems = Array.from(streams, (streamID) => {
|
||||
@@ -234,7 +234,7 @@ export class MenuButtonController {
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.view.views.live'),
|
||||
style: view.is('live') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createViewAction('live') as AdvancedCameraCardCustomAction,
|
||||
tap_action: createViewAction('live'),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -251,8 +251,8 @@ export class MenuButtonController {
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.view.views.clips'),
|
||||
style: view?.is('clips') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createViewAction('clips') as AdvancedCameraCardCustomAction,
|
||||
hold_action: createViewAction('clip') as AdvancedCameraCardCustomAction,
|
||||
tap_action: createViewAction('clips'),
|
||||
hold_action: createViewAction('clip'),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -269,8 +269,8 @@ export class MenuButtonController {
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.view.views.snapshots'),
|
||||
style: view?.is('snapshots') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createViewAction('snapshots') as AdvancedCameraCardCustomAction,
|
||||
hold_action: createViewAction('snapshot') as AdvancedCameraCardCustomAction,
|
||||
tap_action: createViewAction('snapshots'),
|
||||
hold_action: createViewAction('snapshot'),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -287,8 +287,8 @@ export class MenuButtonController {
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.view.views.recordings'),
|
||||
style: view.is('recordings') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createViewAction('recordings') as AdvancedCameraCardCustomAction,
|
||||
hold_action: createViewAction('recording') as AdvancedCameraCardCustomAction,
|
||||
tap_action: createViewAction('recordings'),
|
||||
hold_action: createViewAction('recording'),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -305,7 +305,7 @@ export class MenuButtonController {
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.view.views.image'),
|
||||
style: view?.is('image') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createViewAction('image') as AdvancedCameraCardCustomAction,
|
||||
tap_action: createViewAction('image'),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -322,7 +322,7 @@ export class MenuButtonController {
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.view.views.timeline'),
|
||||
style: view.is('timeline') ? this._getEmphasizedStyle() : {},
|
||||
tap_action: createViewAction('timeline') as AdvancedCameraCardCustomAction,
|
||||
tap_action: createViewAction('timeline'),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -342,7 +342,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.download,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.menu.buttons.download'),
|
||||
tap_action: createGeneralAction('download') as AdvancedCameraCardCustomAction,
|
||||
tap_action: createGeneralAction('download'),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -358,7 +358,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.camera_ui,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.menu.buttons.camera_ui'),
|
||||
tap_action: createGeneralAction('camera_ui') as AdvancedCameraCardCustomAction,
|
||||
tap_action: createGeneralAction('camera_ui'),
|
||||
}
|
||||
: null;
|
||||
}
|
||||
@@ -385,18 +385,14 @@ export class MenuButtonController {
|
||||
style: unavailable || muted ? {} : this._getEmphasizedStyle(true),
|
||||
...(!unavailable &&
|
||||
buttonType === 'momentary' && {
|
||||
start_tap_action: createGeneralAction(
|
||||
'microphone_unmute',
|
||||
) as AdvancedCameraCardCustomAction,
|
||||
end_tap_action: createGeneralAction(
|
||||
'microphone_mute',
|
||||
) as AdvancedCameraCardCustomAction,
|
||||
start_tap_action: createGeneralAction('microphone_unmute'),
|
||||
end_tap_action: createGeneralAction('microphone_mute'),
|
||||
}),
|
||||
...(!unavailable &&
|
||||
buttonType === 'toggle' && {
|
||||
tap_action: createGeneralAction(
|
||||
muted ? 'microphone_unmute' : 'microphone_mute',
|
||||
) as AdvancedCameraCardCustomAction,
|
||||
),
|
||||
}),
|
||||
};
|
||||
}
|
||||
@@ -412,7 +408,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.expand,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.menu.buttons.expand'),
|
||||
tap_action: createGeneralAction('expand') as AdvancedCameraCardCustomAction,
|
||||
tap_action: createGeneralAction('expand'),
|
||||
style: inExpandedMode ? this._getEmphasizedStyle() : {},
|
||||
};
|
||||
}
|
||||
@@ -428,9 +424,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.fullscreen,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.menu.buttons.fullscreen'),
|
||||
tap_action: createGeneralAction(
|
||||
'fullscreen',
|
||||
) as AdvancedCameraCardCustomAction,
|
||||
tap_action: createGeneralAction('fullscreen'),
|
||||
style: inFullscreen ? this._getEmphasizedStyle() : {},
|
||||
}
|
||||
: null;
|
||||
@@ -498,9 +492,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.play,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.menu.buttons.play'),
|
||||
tap_action: createGeneralAction(
|
||||
paused ? 'play' : 'pause',
|
||||
) as AdvancedCameraCardCustomAction,
|
||||
tap_action: createGeneralAction(paused ? 'play' : 'pause'),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -521,9 +513,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.mute,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.menu.buttons.mute'),
|
||||
tap_action: createGeneralAction(
|
||||
muted ? 'unmute' : 'mute',
|
||||
) as AdvancedCameraCardCustomAction,
|
||||
tap_action: createGeneralAction(muted ? 'unmute' : 'mute'),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -539,7 +529,7 @@ export class MenuButtonController {
|
||||
...config.menu.buttons.screenshot,
|
||||
type: 'custom:advanced-camera-card-menu-icon',
|
||||
title: localize('config.menu.buttons.screenshot'),
|
||||
tap_action: createGeneralAction('screenshot') as AdvancedCameraCardCustomAction,
|
||||
tap_action: createGeneralAction('screenshot'),
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -637,7 +627,7 @@ export class MenuButtonController {
|
||||
title: localize('config.menu.buttons.ptz_home'),
|
||||
tap_action: createPTZMultiAction({
|
||||
targetID: target.targetID,
|
||||
}) as AdvancedCameraCardCustomAction,
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -675,30 +665,23 @@ export class MenuButtonController {
|
||||
button.start_tap_action,
|
||||
button.end_tap_action,
|
||||
]) {
|
||||
const actions = Array.isArray(actionSet) ? actionSet : [actionSet];
|
||||
for (const action of actions) {
|
||||
// All advanced camera card actions will have action of 'fire-dom-event' and
|
||||
// styling only applies to those.
|
||||
if (
|
||||
!action ||
|
||||
action.action !== 'fire-dom-event' ||
|
||||
!('advanced_camera_card_action' in action)
|
||||
) {
|
||||
for (const action of arrayify(actionSet)) {
|
||||
if (!isAdvancedCameraCardCustomAction(action)) {
|
||||
continue;
|
||||
}
|
||||
const customCardAction = action as AdvancedCameraCardCustomAction;
|
||||
|
||||
if (
|
||||
VIEWS_USER_SPECIFIED.some(
|
||||
(viewName) =>
|
||||
viewName === customCardAction.advanced_camera_card_action &&
|
||||
options?.view?.is(customCardAction.advanced_camera_card_action),
|
||||
viewName === action.advanced_camera_card_action &&
|
||||
options?.view?.is(action.advanced_camera_card_action),
|
||||
) ||
|
||||
(customCardAction.advanced_camera_card_action === 'default' &&
|
||||
(action.advanced_camera_card_action === 'default' &&
|
||||
options?.view?.is(config.view.default)) ||
|
||||
(customCardAction.advanced_camera_card_action === 'fullscreen' &&
|
||||
(action.advanced_camera_card_action === 'fullscreen' &&
|
||||
!!options?.fullscreenManager?.isInFullscreen()) ||
|
||||
(customCardAction.advanced_camera_card_action === 'camera_select' &&
|
||||
options?.view?.camera === customCardAction.camera)
|
||||
(action.advanced_camera_card_action === 'camera_select' &&
|
||||
options?.view?.camera === action.camera)
|
||||
) {
|
||||
return this._getEmphasizedStyle();
|
||||
}
|
||||
|
||||
@@ -3,17 +3,14 @@ import { orderBy } from 'lodash-es';
|
||||
import { dispatchActionExecutionRequest } from '../card-controller/actions/utils/execution-request.js';
|
||||
import { SubmenuInteraction } from '../components/submenu/types.js';
|
||||
import {
|
||||
ActionConfig,
|
||||
MENU_PRIORITY_MAX,
|
||||
type ActionType,
|
||||
type ActionsConfig,
|
||||
type MenuConfig,
|
||||
type MenuItem,
|
||||
} from '../config/types.js';
|
||||
import { Interaction } from '../types.js';
|
||||
import {
|
||||
convertActionToCardCustomAction,
|
||||
getActionConfigGivenAction,
|
||||
} from '../utils/action';
|
||||
import { getActionConfigGivenAction } from '../utils/action';
|
||||
import { arrayify, isTruthy, setOrRemoveAttribute } from '../utils/basic.js';
|
||||
|
||||
export class MenuController {
|
||||
@@ -114,7 +111,7 @@ export class MenuController {
|
||||
let menuToggle = false;
|
||||
|
||||
const toggleLessActions = actions.filter(
|
||||
(item) => isTruthy(item) && !this._isUnknownActionMenuToggleAction(item),
|
||||
(item) => isTruthy(item) && !this._isMenuToggleAction(item),
|
||||
);
|
||||
if (toggleLessActions.length != actions.length) {
|
||||
menuToggle = true;
|
||||
@@ -171,8 +168,10 @@ export class MenuController {
|
||||
return this._config?.style === 'hidden';
|
||||
}
|
||||
|
||||
protected _isUnknownActionMenuToggleAction(action: ActionType): boolean {
|
||||
const parsedAction = convertActionToCardCustomAction(action);
|
||||
return !!parsedAction && parsedAction.advanced_camera_card_action == 'menu_toggle';
|
||||
protected _isMenuToggleAction(action: ActionConfig): boolean {
|
||||
return (
|
||||
action.action === 'fire-dom-event' &&
|
||||
action.advanced_camera_card_action === 'menu_toggle'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,9 +8,10 @@ import {
|
||||
} from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { MenuSubmenu, MenuSubmenuItem, MenuSubmenuSelect } from '../../config/types.js';
|
||||
import { MenuSubmenuItem, MenuSubmenuSelect } from '../../config/types.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import menuButtonStyle from '../../scss/menu-button.scss';
|
||||
import { Icon } from '../../types.js';
|
||||
import { getEntityTitle, isHassDifferent } from '../../utils/ha';
|
||||
import { getEntityStateTranslation } from '../../utils/ha/entity-state-translation.js';
|
||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity/index.js';
|
||||
@@ -31,7 +32,8 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
|
||||
@state()
|
||||
protected _optionTitles?: Record<string, string>;
|
||||
|
||||
protected _generatedSubmenu?: MenuSubmenu;
|
||||
protected _generatedSubmenuItems?: MenuSubmenuItem[];
|
||||
protected _generatedIcon?: Icon;
|
||||
|
||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||
// No need to update the submenu unless the select entity has changed.
|
||||
@@ -86,29 +88,7 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
|
||||
return;
|
||||
}
|
||||
|
||||
const title = getEntityTitle(this.hass, entityID);
|
||||
const submenu: MenuSubmenu = {
|
||||
...(title && { title }),
|
||||
|
||||
// Override it with anything explicitly set in the submenuSelect.
|
||||
...this.submenuSelect,
|
||||
|
||||
icon: {
|
||||
icon: this.submenuSelect.icon,
|
||||
entity: entityID,
|
||||
fallback: 'mdi:format-list-bulleted',
|
||||
},
|
||||
|
||||
type: 'custom:advanced-camera-card-menu-submenu',
|
||||
items: [],
|
||||
};
|
||||
|
||||
// For cleanliness remove the options parameter which is unused by the
|
||||
// submenu rendering itself (above). It is only in this method to populate
|
||||
// the items correctly (below).
|
||||
delete submenu['options'];
|
||||
|
||||
const items = submenu.items as MenuSubmenuItem[];
|
||||
const items: MenuSubmenuItem[] = [];
|
||||
|
||||
for (const option of options) {
|
||||
const title = this._optionTitles?.[option] ?? option;
|
||||
@@ -136,31 +116,32 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
this._generatedSubmenu = submenu;
|
||||
this._generatedSubmenuItems = items;
|
||||
this._generatedIcon = {
|
||||
icon: this.submenuSelect.icon,
|
||||
entity: entityID,
|
||||
fallback: 'mdi:format-list-bulleted',
|
||||
};
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const submenu = this._generatedSubmenu;
|
||||
if (!submenu) {
|
||||
if (!this._generatedSubmenuItems || !this._generatedIcon || !this.submenuSelect) {
|
||||
return html``;
|
||||
}
|
||||
|
||||
const style = styleMap(submenu.style || {});
|
||||
const title = getEntityTitle(this.hass, this.submenuSelect.entity);
|
||||
const style = styleMap(this.submenuSelect.style || {});
|
||||
return html` <advanced-camera-card-submenu
|
||||
.hass=${this.hass}
|
||||
.items=${submenu?.items}
|
||||
.items=${this._generatedSubmenuItems}
|
||||
>
|
||||
<ha-icon-button style="${style}" .label=${submenu.title || ''}>
|
||||
<ha-icon-button style="${style}" .label=${title || ''}>
|
||||
<advanced-camera-card-icon
|
||||
?allow-override-non-active-styles=${true}
|
||||
style="${style}"
|
||||
title=${submenu.title || ''}
|
||||
title=${title || ''}
|
||||
.hass=${this.hass}
|
||||
.icon=${typeof submenu.icon === 'string'
|
||||
? {
|
||||
icon: submenu.icon,
|
||||
}
|
||||
: submenu.icon}
|
||||
.icon=${this._generatedIcon}
|
||||
></advanced-camera-card-icon>
|
||||
</ha-icon-button>
|
||||
</advanced-camera-card-submenu>`;
|
||||
|
||||
@@ -3,9 +3,9 @@ import { customElement, property } from 'lit/decorators.js';
|
||||
import { styleMap } from 'lit/directives/style-map.js';
|
||||
import { actionHandler } from '../../action-handler-directive.js';
|
||||
import { MenuSubmenu } from '../../config/types.js';
|
||||
import { hasAction } from '../../ha/has-action.js';
|
||||
import { HomeAssistant } from '../../ha/types.js';
|
||||
import menuButtonStyle from '../../scss/menu-button.scss';
|
||||
import { hasAction } from '../../utils/action.js';
|
||||
import '../icon.js';
|
||||
import './index.js';
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Actions } from '../../config/types';
|
||||
import { Interaction } from '../../types';
|
||||
|
||||
export interface SubmenuItem {
|
||||
export interface SubmenuItem extends Actions {
|
||||
title?: string;
|
||||
subtitle?: string;
|
||||
icon?: string;
|
||||
@@ -8,10 +9,6 @@ export interface SubmenuItem {
|
||||
style?: Record<string, string>;
|
||||
enabled?: boolean;
|
||||
selected?: boolean;
|
||||
|
||||
hold_action?: unknown;
|
||||
double_tap_action?: unknown;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface SubmenuInteraction extends Interaction {
|
||||
|
||||
+126
-154
@@ -1,16 +1,6 @@
|
||||
import { HassServiceTarget } from 'home-assistant-js-websocket';
|
||||
import { z } from 'zod';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT, MEDIA_CHUNK_SIZE_MAX } from '../const.js';
|
||||
import {
|
||||
CallServiceActionConfig,
|
||||
ConfirmationRestrictionConfig,
|
||||
MoreInfoActionConfig,
|
||||
NavigateActionConfig,
|
||||
NoActionConfig,
|
||||
PerformActionActionConfig,
|
||||
ToggleActionConfig,
|
||||
UrlActionConfig,
|
||||
} from '../ha/types.js';
|
||||
import { capabilityKeys } from '../types.js';
|
||||
import { deepRemoveDefaults } from '../utils/zod.js';
|
||||
import {
|
||||
@@ -123,12 +113,11 @@ const viewDisplaySchema = z
|
||||
export type ViewDisplayConfig = z.infer<typeof viewDisplaySchema>;
|
||||
|
||||
// *************************************************************************
|
||||
// Actions
|
||||
//
|
||||
// Declare schemas to existing types:
|
||||
// - https://github.com/colinhacks/zod/issues/372#issuecomment-826380330
|
||||
// Stock Actions
|
||||
// *************************************************************************
|
||||
|
||||
// Declare schemas for existing types.
|
||||
// See: https://github.com/colinhacks/zod/issues/372#issuecomment-826380330
|
||||
const schemaForType =
|
||||
<T>() =>
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@@ -152,108 +141,86 @@ const actionBaseSchema = z.object({
|
||||
}),
|
||||
)
|
||||
.optional(),
|
||||
|
||||
card_id: z
|
||||
.string()
|
||||
.regex(cardIDRegex, 'card_id parameter can only contain [a-z][A-Z][0-9_]-')
|
||||
.optional(),
|
||||
});
|
||||
|
||||
// HA accepts either a boolean or a ConfirmationRestrictionConfig object.
|
||||
// `custom-card-helpers` currently only supports the latter. For maximum
|
||||
// compatibility, this card supports what HA supports.
|
||||
interface ExtendedConfirmationRestrictionConfig {
|
||||
confirmation?: boolean | ConfirmationRestrictionConfig;
|
||||
}
|
||||
|
||||
const toggleActionSchema = schemaForType<
|
||||
ToggleActionConfig & ExtendedConfirmationRestrictionConfig
|
||||
>()(
|
||||
actionBaseSchema.extend({
|
||||
action: z.literal('toggle'),
|
||||
}),
|
||||
);
|
||||
const toggleActionSchema = actionBaseSchema.extend({
|
||||
action: z.literal('toggle'),
|
||||
});
|
||||
export type ToggleActionConfig = z.infer<typeof toggleActionSchema>;
|
||||
|
||||
const targetSchema = schemaForType<HassServiceTarget>()(
|
||||
z.object({
|
||||
entity_id: z.string().optional(),
|
||||
device_id: z.string().optional(),
|
||||
area_id: z.string().optional(),
|
||||
entity_id: z.string().or(z.string().array()).optional(),
|
||||
device_id: z.string().or(z.string().array()).optional(),
|
||||
area_id: z.string().or(z.string().array()).optional(),
|
||||
floor_id: z.string().or(z.string().array()).optional(),
|
||||
label_id: z.string().or(z.string().array()).optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
const performActionActionSchema = schemaForType<
|
||||
PerformActionActionConfig & ExtendedConfirmationRestrictionConfig
|
||||
>()(
|
||||
actionBaseSchema.extend({
|
||||
action: z.literal('perform-action'),
|
||||
perform_action: z.string(),
|
||||
data: z.object({}).passthrough().optional(),
|
||||
target: targetSchema.optional(),
|
||||
}),
|
||||
);
|
||||
const performActionActionSchema = actionBaseSchema.extend({
|
||||
action: z.literal('perform-action'),
|
||||
perform_action: z.string(),
|
||||
data: z.object({}).passthrough().optional(),
|
||||
target: targetSchema.optional(),
|
||||
});
|
||||
export type PerformActionActionConfig = z.infer<typeof performActionActionSchema>;
|
||||
|
||||
// Note: call-service is deprecated and will eventually go away. Please use
|
||||
// perform-action instead.
|
||||
// See: https://www.home-assistant.io/blog/2024/08/07/release-20248/#goodbye-service-calls-hello-actions-
|
||||
const callServiceActionSchema = schemaForType<
|
||||
CallServiceActionConfig & ExtendedConfirmationRestrictionConfig
|
||||
>()(
|
||||
actionBaseSchema.extend({
|
||||
action: z.literal('call-service'),
|
||||
service: z.string(),
|
||||
data: z.object({}).passthrough().optional(),
|
||||
target: targetSchema.optional(),
|
||||
}),
|
||||
);
|
||||
const callServiceActionSchema = actionBaseSchema.extend({
|
||||
action: z.literal('call-service'),
|
||||
service: z.string(),
|
||||
data: z.object({}).passthrough().optional(),
|
||||
target: targetSchema.optional(),
|
||||
});
|
||||
export type CallServiceActionConfig = z.infer<typeof callServiceActionSchema>;
|
||||
|
||||
const navigateActionSchema = schemaForType<
|
||||
NavigateActionConfig & ExtendedConfirmationRestrictionConfig
|
||||
>()(
|
||||
actionBaseSchema.extend({
|
||||
action: z.literal('navigate'),
|
||||
navigation_path: z.string(),
|
||||
}),
|
||||
);
|
||||
const navigateActionSchema = actionBaseSchema.extend({
|
||||
action: z.literal('navigate'),
|
||||
navigation_path: z.string(),
|
||||
navigation_replace: z.boolean().optional(),
|
||||
});
|
||||
export type NavigateActionConfig = z.infer<typeof navigateActionSchema>;
|
||||
|
||||
const urlActionSchema = schemaForType<
|
||||
UrlActionConfig & ExtendedConfirmationRestrictionConfig
|
||||
>()(
|
||||
actionBaseSchema.extend({
|
||||
action: z.literal('url'),
|
||||
url_path: z.string(),
|
||||
}),
|
||||
);
|
||||
const urlActionSchema = actionBaseSchema.extend({
|
||||
action: z.literal('url'),
|
||||
url_path: z.string(),
|
||||
});
|
||||
export type URLActionConfig = z.infer<typeof urlActionSchema>;
|
||||
|
||||
const moreInfoActionSchema = schemaForType<
|
||||
MoreInfoActionConfig & ExtendedConfirmationRestrictionConfig
|
||||
>()(
|
||||
actionBaseSchema.extend({
|
||||
action: z.literal('more-info'),
|
||||
}),
|
||||
);
|
||||
const moreInfoActionSchema = actionBaseSchema.extend({
|
||||
action: z.literal('more-info'),
|
||||
entity: z.string().optional(),
|
||||
});
|
||||
export type MoreInfoActionConfig = z.infer<typeof moreInfoActionSchema>;
|
||||
|
||||
const customActionSchema = actionBaseSchema
|
||||
.extend({
|
||||
action: z.literal('fire-dom-event'),
|
||||
})
|
||||
.passthrough();
|
||||
export type CustomActionConfig = z.infer<typeof customActionSchema>;
|
||||
|
||||
const noActionSchema = schemaForType<
|
||||
NoActionConfig & ExtendedConfirmationRestrictionConfig
|
||||
>()(
|
||||
actionBaseSchema.extend({
|
||||
action: z.literal('none'),
|
||||
}),
|
||||
);
|
||||
const noneActionSchema = actionBaseSchema.extend({
|
||||
action: z.literal('none'),
|
||||
});
|
||||
export type NoneActionConfig = z.infer<typeof noneActionSchema>;
|
||||
|
||||
export const advancedCameraCardCustomActionsBaseSchema = customActionSchema.extend({
|
||||
export const advancedCameraCardCustomActionsBaseSchema = actionBaseSchema.extend({
|
||||
action: z
|
||||
.literal('custom:advanced-camera-card-action')
|
||||
// Syntactic sugar to avoid 'fire-dom-event' as part of an external API.
|
||||
.transform((): 'fire-dom-event' => 'fire-dom-event')
|
||||
.or(z.literal('fire-dom-event')),
|
||||
|
||||
// Card this command is intended for.
|
||||
card_id: z
|
||||
.string()
|
||||
.regex(cardIDRegex, 'card_id parameter can only contain [a-z][A-Z][0-9_]-')
|
||||
.optional(),
|
||||
.literal('fire-dom-event')
|
||||
.or(
|
||||
z
|
||||
.literal('custom:advanced-camera-card-action')
|
||||
.transform((): 'fire-dom-event' => 'fire-dom-event'),
|
||||
),
|
||||
});
|
||||
|
||||
// *************************************************************************
|
||||
@@ -374,18 +341,28 @@ const sleepActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend
|
||||
});
|
||||
export type SleepActionConfig = z.infer<typeof sleepActionConfigSchema>;
|
||||
|
||||
const statusBarActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
|
||||
advanced_camera_card_action: z.literal('status_bar'),
|
||||
status_bar_action: z.enum(['add', 'remove', 'reset']),
|
||||
|
||||
// This needs to be lazily evaluated since statusBarItemSchema may itself
|
||||
// contain actions.
|
||||
// For StatusBarActionConfig provide a manual type definition to avoid the `any`
|
||||
// that would be created by the lazy() evaluation below.
|
||||
// See: https://zod.dev/?id=recursive-types
|
||||
const statusBarActionConfigSchemaBase = advancedCameraCardCustomActionsBaseSchema.extend(
|
||||
{
|
||||
advanced_camera_card_action: z.literal('status_bar'),
|
||||
status_bar_action: z.enum(['add', 'remove', 'reset']),
|
||||
},
|
||||
);
|
||||
export type StatusBarActionConfig = z.infer<typeof statusBarActionConfigSchemaBase> & {
|
||||
items?: StatusBarItem[];
|
||||
};
|
||||
export const statusBarActionConfigSchema: z.ZodSchema<
|
||||
StatusBarActionConfig,
|
||||
z.ZodTypeDef,
|
||||
unknown
|
||||
> = statusBarActionConfigSchemaBase.extend({
|
||||
items: z
|
||||
.lazy(() => statusBarItemSchema)
|
||||
.array()
|
||||
.optional(),
|
||||
});
|
||||
export type StatusBarActionConfig = z.infer<typeof statusBarActionConfigSchema>;
|
||||
|
||||
const LOG_ACTIONS_LEVELS = ['debug', 'info', 'warn', 'error'] as const;
|
||||
export type LogActionLevel = (typeof LOG_ACTIONS_LEVELS)[number];
|
||||
@@ -397,27 +374,8 @@ const logActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
|
||||
});
|
||||
export type LogActionConfig = z.infer<typeof logActionConfigSchema>;
|
||||
|
||||
const advancedCameraCardCustomActionSchema = z.union([
|
||||
cameraSelectActionConfigSchema,
|
||||
generalActionConfigSchema,
|
||||
substreamSelectActionConfigSchema,
|
||||
logActionConfigSchema,
|
||||
mediaPlayerActionConfigSchema,
|
||||
ptzActionConfigSchema,
|
||||
ptzDigitalActionConfigSchema,
|
||||
ptzMultiActionSchema,
|
||||
ptzControlsActionConfigSchema,
|
||||
viewActionConfigSchema,
|
||||
viewDisplayModeActionConfigSchema,
|
||||
sleepActionConfigSchema,
|
||||
statusBarActionConfigSchema,
|
||||
]);
|
||||
export type AdvancedCameraCardCustomAction = z.infer<
|
||||
typeof advancedCameraCardCustomActionSchema
|
||||
>;
|
||||
|
||||
// An action that can be used internally to call a callback.
|
||||
// Note: The internal callback action is kept out of schemas that can be user-specified.
|
||||
// An action that can be used internally to call a callback (it is not possible
|
||||
// for the user to pass this through via the configuration).
|
||||
export const INTERNAL_CALLBACK_ACTION = '__INTERNAL_CALLBACK_ACTION__';
|
||||
const internalCallbackActionConfigSchema =
|
||||
advancedCameraCardCustomActionsBaseSchema.extend({
|
||||
@@ -430,57 +388,66 @@ export type InternalCallbackActionConfig = z.infer<
|
||||
typeof internalCallbackActionConfigSchema
|
||||
>;
|
||||
|
||||
export const internalAdvancedCameraCardCustomActionSchema =
|
||||
advancedCameraCardCustomActionSchema.or(internalCallbackActionConfigSchema);
|
||||
export type InternalAdvancedCameraCardCustomAction = z.infer<
|
||||
typeof internalAdvancedCameraCardCustomActionSchema
|
||||
>;
|
||||
|
||||
// Cannot use discriminatedUnion since advancedCameraCardCustomActionSchema uses
|
||||
// a transform on the discriminated union key.
|
||||
export const actionSchema = z.union([
|
||||
toggleActionSchema,
|
||||
const stockActionSchema = z.union([
|
||||
callServiceActionSchema,
|
||||
performActionActionSchema,
|
||||
navigateActionSchema,
|
||||
urlActionSchema,
|
||||
moreInfoActionSchema,
|
||||
noActionSchema,
|
||||
customActionSchema,
|
||||
advancedCameraCardCustomActionSchema,
|
||||
moreInfoActionSchema,
|
||||
navigateActionSchema,
|
||||
noneActionSchema,
|
||||
performActionActionSchema,
|
||||
toggleActionSchema,
|
||||
urlActionSchema,
|
||||
]);
|
||||
|
||||
const internalActionSchema = actionSchema.or(internalCallbackActionConfigSchema);
|
||||
export type ActionType = z.infer<typeof internalActionSchema>;
|
||||
const advancedCameraCardCustomActionSchema = z.union([
|
||||
cameraSelectActionConfigSchema,
|
||||
generalActionConfigSchema,
|
||||
internalCallbackActionConfigSchema,
|
||||
logActionConfigSchema,
|
||||
mediaPlayerActionConfigSchema,
|
||||
ptzActionConfigSchema,
|
||||
ptzControlsActionConfigSchema,
|
||||
ptzDigitalActionConfigSchema,
|
||||
ptzMultiActionSchema,
|
||||
sleepActionConfigSchema,
|
||||
statusBarActionConfigSchema,
|
||||
substreamSelectActionConfigSchema,
|
||||
viewActionConfigSchema,
|
||||
viewDisplayModeActionConfigSchema,
|
||||
]);
|
||||
export type AdvancedCameraCardCustomActionConfig = z.infer<
|
||||
typeof advancedCameraCardCustomActionSchema
|
||||
>;
|
||||
|
||||
const actionConfigSchema = z.union([
|
||||
stockActionSchema,
|
||||
advancedCameraCardCustomActionSchema,
|
||||
]);
|
||||
export type ActionConfig = z.infer<typeof actionConfigSchema>;
|
||||
|
||||
export interface AuxillaryActionConfig {
|
||||
entity?: string;
|
||||
}
|
||||
|
||||
const actionsBaseSchema = z
|
||||
.object({
|
||||
tap_action: actionSchema.or(actionSchema.array()).optional(),
|
||||
hold_action: actionSchema.or(actionSchema.array()).optional(),
|
||||
double_tap_action: actionSchema.or(actionSchema.array()).optional(),
|
||||
start_tap_action: actionSchema.or(actionSchema.array()).optional(),
|
||||
end_tap_action: actionSchema.or(actionSchema.array()).optional(),
|
||||
tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
|
||||
hold_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
|
||||
double_tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
|
||||
start_tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
|
||||
end_tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
|
||||
})
|
||||
// Passthrough to allow (at least) entity/camera_image to go through. This
|
||||
// card doesn't need these attributes, but handleAction() in
|
||||
// custom_card_helpers may depending on how the action is configured.
|
||||
.passthrough();
|
||||
export type Actions = z.infer<typeof actionsBaseSchema>;
|
||||
|
||||
export type ActionsConfig = Actions & {
|
||||
camera_image?: string;
|
||||
entity?: string;
|
||||
};
|
||||
export type ActionsConfig = Actions & AuxillaryActionConfig;
|
||||
|
||||
const actionsSchema = z.object({
|
||||
actions: actionsBaseSchema.optional(),
|
||||
});
|
||||
|
||||
const elementsBaseSchema = actionsBaseSchema.extend({
|
||||
style: z.record(z.string().nullable().or(z.undefined()).or(z.number())).optional(),
|
||||
title: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
// *************************************************************************
|
||||
// Picture Elements
|
||||
//
|
||||
@@ -489,6 +456,11 @@ const elementsBaseSchema = actionsBaseSchema.extend({
|
||||
// display up-front regardless of where they made their error.
|
||||
// *************************************************************************
|
||||
|
||||
const elementsBaseSchema = actionsBaseSchema.extend({
|
||||
style: z.record(z.string().nullable().or(z.undefined()).or(z.number())).optional(),
|
||||
title: z.string().nullable().optional(),
|
||||
});
|
||||
|
||||
// https://www.home-assistant.io/lovelace/picture-elements/#state-badge
|
||||
const stateBadgeIconSchema = elementsBaseSchema.extend({
|
||||
type: z.literal('state-badge'),
|
||||
@@ -2009,7 +1981,7 @@ export type Overrides = z.infer<typeof overridesSchema>;
|
||||
// Automation Configuration
|
||||
// *************************************************************************
|
||||
|
||||
const automationActionSchema = actionSchema.array();
|
||||
const automationActionSchema = actionConfigSchema.array();
|
||||
export type AutomationActions = z.infer<typeof automationActionSchema>;
|
||||
|
||||
const automationSchema = z
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
import { fireHASSEvent } from './fire-hass-event.js';
|
||||
import { forwardHaptic } from './haptic.js';
|
||||
import { navigate } from './navigate.js';
|
||||
import { toggleEntity } from './toggle-entity.js';
|
||||
import { ActionConfig, HomeAssistant } from './types.js';
|
||||
|
||||
export const handleActionConfig = (
|
||||
node: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
config: {
|
||||
entity?: string;
|
||||
camera_image?: string;
|
||||
hold_action?: ActionConfig;
|
||||
tap_action?: ActionConfig;
|
||||
double_tap_action?: ActionConfig;
|
||||
},
|
||||
actionConfig: ActionConfig | undefined,
|
||||
): void => {
|
||||
if (!actionConfig) {
|
||||
actionConfig = {
|
||||
action: 'more-info',
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
actionConfig.confirmation &&
|
||||
(!actionConfig.confirmation.exemptions ||
|
||||
!actionConfig.confirmation.exemptions.some((e) => e.user === hass!.user!.id))
|
||||
) {
|
||||
forwardHaptic('warning');
|
||||
|
||||
if (
|
||||
!confirm(
|
||||
actionConfig.confirmation.text ||
|
||||
`Are you sure you want to ${actionConfig.action}?`,
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
switch (actionConfig.action) {
|
||||
case 'more-info':
|
||||
if (config.entity || config.camera_image) {
|
||||
fireHASSEvent(node, 'hass-more-info', {
|
||||
entityId: config.entity ? config.entity : config.camera_image!,
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'navigate':
|
||||
if (actionConfig.navigation_path) {
|
||||
navigate(node, actionConfig.navigation_path);
|
||||
}
|
||||
break;
|
||||
case 'url':
|
||||
if (actionConfig.url_path) {
|
||||
window.open(actionConfig.url_path);
|
||||
}
|
||||
break;
|
||||
case 'toggle':
|
||||
if (config.entity) {
|
||||
toggleEntity(hass, config.entity!);
|
||||
forwardHaptic('success');
|
||||
}
|
||||
break;
|
||||
case 'perform-action': {
|
||||
if (!actionConfig.perform_action) {
|
||||
forwardHaptic('failure');
|
||||
return;
|
||||
}
|
||||
const [domain, service] = actionConfig.perform_action.split('.', 2);
|
||||
hass.callService(domain, service, actionConfig.data, actionConfig.target);
|
||||
forwardHaptic('success');
|
||||
break;
|
||||
}
|
||||
case 'call-service': {
|
||||
if (!actionConfig.service) {
|
||||
forwardHaptic('failure');
|
||||
return;
|
||||
}
|
||||
const [domain, service] = actionConfig.service.split('.', 2);
|
||||
hass.callService(domain, service, actionConfig.data, actionConfig.target);
|
||||
forwardHaptic('success');
|
||||
break;
|
||||
}
|
||||
case 'fire-dom-event': {
|
||||
fireHASSEvent(node, 'll-custom', actionConfig);
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
import { ActionConfig } from './types.js';
|
||||
|
||||
export function hasAction(config?: ActionConfig): boolean {
|
||||
return config !== undefined && config.action !== 'none';
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { fireHASSEvent } from './fire-hass-event.js';
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
'location-changed': {
|
||||
replace: boolean;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const navigate = (_node: unknown, path: string, replace: boolean = false) => {
|
||||
if (replace) {
|
||||
history.replaceState(null, '', path);
|
||||
} else {
|
||||
history.pushState(null, '', path);
|
||||
}
|
||||
fireHASSEvent(window, 'location-changed', {
|
||||
replace,
|
||||
});
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
import { STATES_OFF } from './const.js';
|
||||
import { turnOnOffEntity } from './turn-on-off-entity.js';
|
||||
import { HomeAssistant } from './types.js';
|
||||
|
||||
export const toggleEntity = (hass: HomeAssistant, entityId: string): Promise<void> => {
|
||||
const turnOn = STATES_OFF.includes(hass.states[entityId].state);
|
||||
return turnOnOffEntity(hass, entityId, turnOn);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import { HomeAssistant } from './types.js';
|
||||
import { computeDomain } from './compute-domain.js';
|
||||
|
||||
export const turnOnOffEntity = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
turnOn = true,
|
||||
): Promise<void> => {
|
||||
const stateDomain = computeDomain(entityId);
|
||||
const serviceDomain = stateDomain === 'group' ? 'homeassistant' : stateDomain;
|
||||
|
||||
let service;
|
||||
switch (stateDomain) {
|
||||
case 'lock':
|
||||
service = turnOn ? 'unlock' : 'lock';
|
||||
break;
|
||||
case 'cover':
|
||||
service = turnOn ? 'open_cover' : 'close_cover';
|
||||
break;
|
||||
default:
|
||||
service = turnOn ? 'turn_on' : 'turn_off';
|
||||
}
|
||||
|
||||
return hass.callService(serviceDomain, service, { entity_id: entityId });
|
||||
};
|
||||
@@ -7,87 +7,6 @@ import {
|
||||
HassServiceTarget,
|
||||
MessageBase,
|
||||
} from 'home-assistant-js-websocket';
|
||||
import { HapticType } from './haptic.js';
|
||||
|
||||
interface ToggleMenuActionConfig extends BaseActionConfig {
|
||||
action: 'toggle-menu';
|
||||
}
|
||||
|
||||
export interface ToggleActionConfig extends BaseActionConfig {
|
||||
action: 'toggle';
|
||||
}
|
||||
|
||||
export interface CallServiceActionConfig extends BaseActionConfig {
|
||||
action: 'call-service';
|
||||
service: string;
|
||||
data?: {
|
||||
entity_id?: string | [string];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
target?: HassServiceTarget;
|
||||
repeat?: number;
|
||||
haptic?: HapticType;
|
||||
}
|
||||
|
||||
export interface PerformActionActionConfig extends BaseActionConfig {
|
||||
action: 'perform-action';
|
||||
perform_action: string;
|
||||
data?: {
|
||||
entity_id?: string | [string];
|
||||
[key: string]: unknown;
|
||||
};
|
||||
target?: HassServiceTarget;
|
||||
repeat?: number;
|
||||
haptic?: HapticType;
|
||||
}
|
||||
|
||||
export interface NavigateActionConfig extends BaseActionConfig {
|
||||
action: 'navigate';
|
||||
navigation_path: string;
|
||||
}
|
||||
|
||||
export interface UrlActionConfig extends BaseActionConfig {
|
||||
action: 'url';
|
||||
url_path: string;
|
||||
}
|
||||
export interface MoreInfoActionConfig extends BaseActionConfig {
|
||||
action: 'more-info';
|
||||
entity?: string;
|
||||
}
|
||||
export interface NoActionConfig extends BaseActionConfig {
|
||||
action: 'none';
|
||||
}
|
||||
interface CustomActionConfig extends BaseActionConfig {
|
||||
action: 'fire-dom-event';
|
||||
}
|
||||
/**
|
||||
* `repeat` and `haptic` are specifically for use in custom cards like the Button-Card
|
||||
*/
|
||||
interface BaseActionConfig {
|
||||
confirmation?: ConfirmationRestrictionConfig;
|
||||
repeat?: number;
|
||||
haptic?: HapticType;
|
||||
}
|
||||
|
||||
export interface ConfirmationRestrictionConfig {
|
||||
text?: string;
|
||||
exemptions?: RestrictionConfig[];
|
||||
}
|
||||
|
||||
interface RestrictionConfig {
|
||||
user: string;
|
||||
}
|
||||
|
||||
export declare type ActionConfig =
|
||||
| ToggleActionConfig
|
||||
| CallServiceActionConfig
|
||||
| PerformActionActionConfig
|
||||
| NavigateActionConfig
|
||||
| UrlActionConfig
|
||||
| MoreInfoActionConfig
|
||||
| NoActionConfig
|
||||
| CustomActionConfig
|
||||
| ToggleMenuActionConfig;
|
||||
|
||||
declare global {
|
||||
interface HASSDomEvents {
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"actions": {
|
||||
"abort": "",
|
||||
"confirmation": ""
|
||||
},
|
||||
"common": {
|
||||
"advanced_camera_card": "",
|
||||
"advanced_camera_card_description": "",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"actions": {
|
||||
"abort": "Aborted action",
|
||||
"confirmation": "Are you sure you want to perform this action"
|
||||
},
|
||||
"common": {
|
||||
"advanced_camera_card": "Advanced Camera Card",
|
||||
"advanced_camera_card_description": "An Advanced Camera Card",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"actions": {
|
||||
"abort": "",
|
||||
"confirmation": ""
|
||||
},
|
||||
"common": {
|
||||
"advanced_camera_card": "",
|
||||
"advanced_camera_card_description": "",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"actions": {
|
||||
"abort": "",
|
||||
"confirmation": ""
|
||||
},
|
||||
"common": {
|
||||
"advanced_camera_card": "",
|
||||
"advanced_camera_card_description": "",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"actions": {
|
||||
"abort": "",
|
||||
"confirmation": ""
|
||||
},
|
||||
"common": {
|
||||
"advanced_camera_card": "",
|
||||
"advanced_camera_card_description": "",
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"actions": {
|
||||
"abort": "",
|
||||
"confirmation": ""
|
||||
},
|
||||
"common": {
|
||||
"advanced_camera_card": "",
|
||||
"advanced_camera_card_description": "",
|
||||
|
||||
+17
-29
@@ -2,49 +2,31 @@ import { CardActionsAPI } from '../card-controller/types.js';
|
||||
import { ZoomSettingsBase } from '../components-lib/zoom/types.js';
|
||||
import { PTZAction } from '../config/ptz.js';
|
||||
import {
|
||||
ActionConfig,
|
||||
ActionPhase,
|
||||
ActionType,
|
||||
ActionsConfig,
|
||||
AdvancedCameraCardCustomActionConfig,
|
||||
AdvancedCameraCardGeneralAction,
|
||||
AdvancedCameraCardUserSpecifiedView,
|
||||
CameraSelectActionConfig,
|
||||
DisplayModeActionConfig,
|
||||
GeneralActionConfig,
|
||||
INTERNAL_CALLBACK_ACTION,
|
||||
InternalAdvancedCameraCardCustomAction,
|
||||
InternalCallbackActionConfig,
|
||||
LogActionConfig,
|
||||
LogActionLevel,
|
||||
MediaPlayerActionConfig,
|
||||
PerformActionActionConfig,
|
||||
PTZActionConfig,
|
||||
PTZControlsActionConfig,
|
||||
PTZDigitialActionConfig,
|
||||
PTZMultiActionConfig,
|
||||
SubstreamSelectActionConfig,
|
||||
ViewActionConfig,
|
||||
internalAdvancedCameraCardCustomActionSchema,
|
||||
} from '../config/types.js';
|
||||
import { hasAction as customCardHasAction } from '../ha/has-action.js';
|
||||
import { ActionConfig, ServiceCallRequest } from '../ha/types.js';
|
||||
import { ServiceCallRequest } from '../ha/types.js';
|
||||
import { arrayify } from './basic.js';
|
||||
|
||||
/**
|
||||
* Convert a generic Action to a AdvancedCameraCardCustomAction if it parses correctly.
|
||||
* @param action The generic action configuration.
|
||||
* @returns A AdvancedCameraCardCustomAction or null if it cannot be converted.
|
||||
*/
|
||||
export function convertActionToCardCustomAction(
|
||||
action: unknown,
|
||||
): InternalAdvancedCameraCardCustomAction | null {
|
||||
if (!action) {
|
||||
return null;
|
||||
}
|
||||
// Parse a custom event as other things could generate ll-custom events that
|
||||
// are not related to Advanced Camera Card.
|
||||
const parseResult = internalAdvancedCameraCardCustomActionSchema.safeParse(action);
|
||||
return parseResult.success ? parseResult.data : null;
|
||||
}
|
||||
|
||||
export function createGeneralAction(
|
||||
action: AdvancedCameraCardGeneralAction,
|
||||
options?: {
|
||||
@@ -221,7 +203,7 @@ export function createPerformAction(
|
||||
data?: ServiceCallRequest['serviceData'];
|
||||
target?: ServiceCallRequest['target'];
|
||||
},
|
||||
): ActionType {
|
||||
): PerformActionActionConfig {
|
||||
return {
|
||||
action: 'perform-action' as const,
|
||||
perform_action: perform_action,
|
||||
@@ -240,7 +222,7 @@ export function createPerformAction(
|
||||
export function getActionConfigGivenAction(
|
||||
interaction?: string,
|
||||
config?: ActionsConfig | null,
|
||||
): ActionType | ActionType[] | null {
|
||||
): ActionConfig | ActionConfig[] | null {
|
||||
if (!interaction || !config) {
|
||||
return null;
|
||||
}
|
||||
@@ -270,11 +252,17 @@ export function getActionConfigGivenAction(
|
||||
* @param config The action config in question.
|
||||
* @returns `true` if there's a real action defined, `false` otherwise.
|
||||
*/
|
||||
export const hasAction = (config?: ActionType | ActionType[]): boolean => {
|
||||
// See note above on 'ActionConfig vs ActionType' for why this cast is
|
||||
// necessary and harmless.
|
||||
return arrayify(config).some((item) =>
|
||||
customCardHasAction(item as ActionConfig | undefined),
|
||||
export const hasAction = (config?: ActionConfig | ActionConfig[]): boolean => {
|
||||
return arrayify(config).some((item) => item.action !== 'none');
|
||||
};
|
||||
|
||||
export const isAdvancedCameraCardCustomAction = (
|
||||
action: ActionConfig,
|
||||
): action is AdvancedCameraCardCustomActionConfig => {
|
||||
return (
|
||||
action.action === 'fire-dom-event' &&
|
||||
'advanced_camera_card_action' in action &&
|
||||
typeof action.advanced_camera_card_action === 'string'
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -50,8 +50,8 @@ export function arrayMove(target: unknown[], from: number, to: number): unknown[
|
||||
* @param value: A value (which may be an array).
|
||||
* @returns An array.
|
||||
*/
|
||||
export const arrayify = <T>(value: T | T[]): T[] => {
|
||||
return Array.isArray(value) ? value : [value];
|
||||
export const arrayify = <T>(value?: T | T[]): T[] => {
|
||||
return value ? (Array.isArray(value) ? value : [value]) : [];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user