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]) : [];
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,4 +1,13 @@
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import {
|
||||
afterAll,
|
||||
afterEach,
|
||||
beforeAll,
|
||||
beforeEach,
|
||||
describe,
|
||||
expect,
|
||||
it,
|
||||
vi,
|
||||
} from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import {
|
||||
ActionsManager,
|
||||
@@ -8,13 +17,7 @@ import {
|
||||
import { TemplateRenderer } from '../../../src/card-controller/templates';
|
||||
import { AdvancedCameraCardView } from '../../../src/config/types';
|
||||
import { createLogAction } from '../../../src/utils/action';
|
||||
import {
|
||||
createAction,
|
||||
createCardAPI,
|
||||
createConfig,
|
||||
createHASS,
|
||||
createView,
|
||||
} from '../../test-utils';
|
||||
import { createCardAPI, createConfig, createHASS, createView } from '../../test-utils';
|
||||
|
||||
describe('ActionsManager', () => {
|
||||
describe('getMergedActions', () => {
|
||||
@@ -138,7 +141,7 @@ describe('ActionsManager', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should handle interaction', () => {
|
||||
it('should handle interaction', async () => {
|
||||
const api = createCardAPI();
|
||||
const element = document.createElement('div');
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
@@ -158,7 +161,7 @@ describe('ActionsManager', () => {
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||
manager.handleInteractionEvent(
|
||||
await manager.handleInteractionEvent(
|
||||
new CustomEvent<Interaction>('event', { detail: { action: 'tap' } }),
|
||||
);
|
||||
expect(consoleSpy).toBeCalled();
|
||||
@@ -203,7 +206,7 @@ describe('ActionsManager', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should handle event', () => {
|
||||
it('should handle event', async () => {
|
||||
const action = createLogAction('Hello, world!');
|
||||
const event = new CustomEvent('ll-custom', {
|
||||
detail: action,
|
||||
@@ -213,15 +216,15 @@ describe('ActionsManager', () => {
|
||||
const manager = new ActionsManager(api);
|
||||
|
||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||
manager.handleCustomActionEvent(event);
|
||||
await manager.handleCustomActionEvent(event);
|
||||
expect(consoleSpy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should not handle event without detail', () => {
|
||||
it('should not handle event without detail', async () => {
|
||||
const manager = new ActionsManager(createCardAPI());
|
||||
|
||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||
manager.handleCustomActionEvent(new Event('ll-custom'));
|
||||
await manager.handleCustomActionEvent(new Event('ll-custom'));
|
||||
expect(consoleSpy).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -250,6 +253,77 @@ describe('ActionsManager', () => {
|
||||
await manager.executeActions(createLogAction('Hello, world!'));
|
||||
expect(consoleSpy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should execute actions', async () => {
|
||||
const api = createCardAPI();
|
||||
const manager = new ActionsManager(api);
|
||||
|
||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||
await manager.executeActions(createLogAction('Hello, world!'));
|
||||
expect(consoleSpy).toBeCalled();
|
||||
});
|
||||
|
||||
it('should render templates', async () => {
|
||||
const action = createLogAction('{{ acc.camera }}');
|
||||
|
||||
const templateRenderer = mock<TemplateRenderer>();
|
||||
templateRenderer.renderRecursively.mockReturnValue(action);
|
||||
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const conditionState = {
|
||||
camera: 'camera',
|
||||
};
|
||||
vi.mocked(api.getConditionStateManager().getState).mockReturnValue(conditionState);
|
||||
|
||||
const manager = new ActionsManager(api, templateRenderer);
|
||||
const config = { entity: 'light.office' };
|
||||
const triggerData = { view: { from: 'previous-view', to: 'view' } };
|
||||
|
||||
await manager.executeActions(action, {
|
||||
config,
|
||||
triggerData,
|
||||
});
|
||||
|
||||
expect(templateRenderer.renderRecursively).toBeCalledWith(hass, action, {
|
||||
conditionState,
|
||||
triggerData,
|
||||
});
|
||||
});
|
||||
|
||||
describe('should forward haptics', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('should forward success haptic', async () => {
|
||||
const handler = vi.fn();
|
||||
window.addEventListener('haptic', handler);
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ActionsManager(api);
|
||||
|
||||
await manager.executeActions({ action: 'none' });
|
||||
|
||||
expect(handler).toBeCalledWith(expect.objectContaining({ detail: 'success' }));
|
||||
});
|
||||
|
||||
it('should forward warning haptic', async () => {
|
||||
const handler = vi.fn();
|
||||
window.addEventListener('haptic', handler);
|
||||
|
||||
const api = createCardAPI();
|
||||
const manager = new ActionsManager(api);
|
||||
|
||||
vi.stubGlobal('confirm', vi.fn().mockReturnValue(false));
|
||||
|
||||
await manager.executeActions({ action: 'none', confirmation: true });
|
||||
|
||||
expect(handler).toBeCalledWith(expect.objectContaining({ detail: 'warning' }));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('uninitialize', () => {
|
||||
@@ -266,18 +340,18 @@ describe('ActionsManager', () => {
|
||||
|
||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||
const promise = manager.executeActions([
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
createAction({
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'sleep',
|
||||
duration: {
|
||||
m: 1,
|
||||
},
|
||||
})!,
|
||||
},
|
||||
createLogAction('Hello, world!'),
|
||||
]);
|
||||
|
||||
// Stop inflight actions.
|
||||
manager.uninitialize();
|
||||
await manager.uninitialize();
|
||||
|
||||
// Advance timers (causes the sleep to end).
|
||||
vi.runOnlyPendingTimers();
|
||||
@@ -288,34 +362,4 @@ describe('ActionsManager', () => {
|
||||
expect(consoleSpy).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render templates', async () => {
|
||||
const action = createLogAction('{{ acc.camera }}');
|
||||
|
||||
const templateRenderer = mock<TemplateRenderer>();
|
||||
templateRenderer.renderRecursively.mockReturnValue(action);
|
||||
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const conditionState = {
|
||||
camera: 'camera',
|
||||
};
|
||||
vi.mocked(api.getConditionStateManager().getState).mockReturnValue(conditionState);
|
||||
|
||||
const manager = new ActionsManager(api, templateRenderer);
|
||||
const config = { camera_image: 'camera-image' };
|
||||
const triggerData = { view: { from: 'previous-view', to: 'view' } };
|
||||
|
||||
await manager.executeActions(action, {
|
||||
config,
|
||||
triggerData,
|
||||
});
|
||||
|
||||
expect(templateRenderer.renderRecursively).toBeCalledWith(hass, action, {
|
||||
conditionState,
|
||||
triggerData,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,172 @@
|
||||
import { it } from 'vitest';
|
||||
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { BaseAction } from '../../../../src/card-controller/actions/actions/base';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
import { createViewAction } from '../../../../src/utils/action';
|
||||
import { createCardAPI, createHASS, createUser } from '../../../test-utils';
|
||||
|
||||
it('should construct', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
},
|
||||
);
|
||||
describe('should handle base action', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
await action.execute(api);
|
||||
await action.stop();
|
||||
beforeAll(() => {
|
||||
vi.stubGlobal('confirm', vi.fn());
|
||||
});
|
||||
|
||||
// These methods have no observable effect on the base class, so this test is
|
||||
// currently only providing coverage and proof of no exceptions!
|
||||
afterAll(() => {
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it('should construct', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
await action.stop();
|
||||
|
||||
// These methods have no observable effect on the base class, so this test is
|
||||
// currently only providing coverage and proof of no exceptions!
|
||||
});
|
||||
|
||||
it('should not confirm when not necessary', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(confirm).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should continue execution when confirmed', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
confirmation: true,
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(confirm).mockReturnValue(true);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(confirm).toBeCalled();
|
||||
});
|
||||
|
||||
it('should abort execution when not confirmed', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
confirmation: true,
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(confirm).mockReturnValue(false);
|
||||
|
||||
expect(async () => await action.execute(api)).rejects.toThrowError(/Aborted action/);
|
||||
});
|
||||
|
||||
it('should not confirm when exempted', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS({}, createUser({ id: 'user-id' }));
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
confirmation: {
|
||||
exemptions: [
|
||||
{
|
||||
user: 'user-id',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(confirm).not.toBeCalled();
|
||||
});
|
||||
|
||||
describe('should show correct confirmation text', () => {
|
||||
it('should show action name in confirmation text', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS({}, createUser({ id: 'user-id' }));
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'more-info',
|
||||
confirmation: true,
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(confirm).mockReturnValue(true);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(confirm).toBeCalledWith(
|
||||
'Are you sure you want to perform this action: more-info',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show advanced camera card action name in confirmation text', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS({}, createUser({ id: 'user-id' }));
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
...createViewAction('clips'),
|
||||
confirmation: true,
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(confirm).mockReturnValue(true);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(confirm).toBeCalledWith(
|
||||
'Are you sure you want to perform this action: clips',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show configured confirmation text', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS({}, createUser({ id: 'user-id' }));
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new BaseAction(
|
||||
{},
|
||||
{
|
||||
action: 'more-info',
|
||||
confirmation: {
|
||||
text: 'Test text',
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
vi.mocked(confirm).mockReturnValue(true);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(confirm).toBeCalledWith('Test text');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { CallServiceAction } from '../../../../src/card-controller/actions/actions/call-service';
|
||||
import { createCardAPI, createHASS } from '../../../test-utils';
|
||||
|
||||
describe('CallServiceAction', () => {
|
||||
it('should call service', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new CallServiceAction(
|
||||
{},
|
||||
{
|
||||
action: 'call-service',
|
||||
service: 'light.turn_on',
|
||||
data: { brightness_pct: 80 },
|
||||
target: { entity_id: 'light.office' },
|
||||
},
|
||||
);
|
||||
await action.execute(api);
|
||||
|
||||
expect(hass.callService).toBeCalledWith(
|
||||
'light',
|
||||
'turn_on',
|
||||
{
|
||||
brightness_pct: 80,
|
||||
},
|
||||
{ entity_id: 'light.office' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call service without hass', async () => {
|
||||
const api = createCardAPI();
|
||||
|
||||
const action = new CallServiceAction(
|
||||
{},
|
||||
{
|
||||
action: 'call-service',
|
||||
service: 'light.turn_on',
|
||||
data: { brightness_pct: 80 },
|
||||
target: { entity_id: 'light.office' },
|
||||
},
|
||||
);
|
||||
await action.execute(api);
|
||||
|
||||
// No observable effect.
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { CustomAction } from '../../../../src/card-controller/actions/actions/custom';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('CustomAction', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should open the URL in a new window', async () => {
|
||||
const handler = vi.fn();
|
||||
const element = document.createElement('div');
|
||||
element.addEventListener('ll-custom', handler);
|
||||
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
|
||||
const action = new CustomAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event' as const,
|
||||
foo: 'bar',
|
||||
1: 2,
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { action: 'fire-dom-event', foo: 'bar', 1: 2 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,46 +0,0 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createCardAPI, createHASS, createLitElement } from '../../../test-utils.js';
|
||||
import { GenericAction } from '../../../../src/card-controller/actions/actions/generic.js';
|
||||
import { handleActionConfig } from '../../../../src/ha/handle-action.js';
|
||||
|
||||
vi.mock('../../../../src/ha/handle-action.js');
|
||||
|
||||
describe('should handle generic action', () => {
|
||||
it('without hass', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new GenericAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(handleActionConfig).not.toBeCalled();
|
||||
});
|
||||
|
||||
// @vitest-environment jsdom
|
||||
it('with hass', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS();
|
||||
const element = createLitElement();
|
||||
vi.mocked(api.getCardElementManager()).getElement.mockReturnValue(element);
|
||||
vi.mocked(api.getHASSManager()).getHASS.mockReturnValue(hass);
|
||||
const action = new GenericAction(
|
||||
{},
|
||||
{
|
||||
action: 'fire-dom-event',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(handleActionConfig).toBeCalledWith(
|
||||
element,
|
||||
hass,
|
||||
{},
|
||||
{ action: 'fire-dom-event' },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,80 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { MoreInfoAction } from '../../../../src/card-controller/actions/actions/more-info';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('should handle more-info action', () => {
|
||||
it('should handle more-info with entity in action', async () => {
|
||||
const handler = vi.fn();
|
||||
const element = document.createElement('div');
|
||||
element.addEventListener('hass-more-info', handler);
|
||||
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
|
||||
const action = new MoreInfoAction(
|
||||
{},
|
||||
{
|
||||
action: 'more-info',
|
||||
entity: 'light.office',
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { entityId: 'light.office' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle more-info with entity in auxilliary config', async () => {
|
||||
const handler = vi.fn();
|
||||
const element = document.createElement('div');
|
||||
element.addEventListener('hass-more-info', handler);
|
||||
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
|
||||
const action = new MoreInfoAction(
|
||||
{},
|
||||
{
|
||||
action: 'more-info',
|
||||
},
|
||||
{
|
||||
entity: 'light.office',
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { entityId: 'light.office' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should take no action with any entity', async () => {
|
||||
const handler = vi.fn();
|
||||
const element = document.createElement('div');
|
||||
element.addEventListener('hass-more-info', handler);
|
||||
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||
|
||||
const action = new MoreInfoAction(
|
||||
{},
|
||||
{
|
||||
action: 'more-info',
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(handler).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { NavigateAction } from '../../../../src/card-controller/actions/actions/navigate';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('should handle navigate action', () => {
|
||||
it('should handle navigate action', async () => {
|
||||
const handler = vi.fn();
|
||||
window.addEventListener('location-changed', handler);
|
||||
|
||||
const action = new NavigateAction(
|
||||
{},
|
||||
{
|
||||
action: 'navigate',
|
||||
navigation_path: '/path',
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const historyLength = history.length;
|
||||
|
||||
await action.execute(createCardAPI());
|
||||
|
||||
expect(history.length).toBe(historyLength + 1);
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { replace: false },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle navigate action that replaces', async () => {
|
||||
const handler = vi.fn();
|
||||
window.addEventListener('location-changed', handler);
|
||||
|
||||
const action = new NavigateAction(
|
||||
{},
|
||||
{
|
||||
action: 'navigate',
|
||||
navigation_path: '/path',
|
||||
navigation_replace: true,
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
const historyLength = history.length;
|
||||
|
||||
await action.execute(createCardAPI());
|
||||
|
||||
expect(history.length).toBe(historyLength);
|
||||
expect(handler).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
detail: { replace: true },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,17 @@
|
||||
import { it } from 'vitest';
|
||||
import { NoneAction } from '../../../../src/card-controller/actions/actions/none';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
|
||||
it('should handle none action', async () => {
|
||||
const api = createCardAPI();
|
||||
const action = new NoneAction(
|
||||
{},
|
||||
{
|
||||
action: 'none' as const,
|
||||
},
|
||||
);
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
// No observable side effects.
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { createCardAPI, createHASS } from '../../../test-utils';
|
||||
import { PerformActionAction } from '../../../../src/card-controller/actions/actions/perform-action';
|
||||
|
||||
describe('PerformActionAction', () => {
|
||||
it('should perform action', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new PerformActionAction(
|
||||
{},
|
||||
{
|
||||
action: 'perform-action',
|
||||
perform_action: 'light.turn_on',
|
||||
data: { brightness_pct: 80 },
|
||||
target: { entity_id: 'light.office' },
|
||||
},
|
||||
);
|
||||
await action.execute(api);
|
||||
|
||||
expect(hass.callService).toBeCalledWith(
|
||||
'light',
|
||||
'turn_on',
|
||||
{
|
||||
brightness_pct: 80,
|
||||
},
|
||||
{ entity_id: 'light.office' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should not perform action without hass', async () => {
|
||||
const api = createCardAPI();
|
||||
|
||||
const action = new PerformActionAction(
|
||||
{},
|
||||
{
|
||||
action: 'perform-action',
|
||||
perform_action: 'light.turn_on',
|
||||
data: { brightness_pct: 80 },
|
||||
target: { entity_id: 'light.office' },
|
||||
},
|
||||
);
|
||||
await action.execute(api);
|
||||
|
||||
// No observable effect.
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,81 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ToggleAction } from '../../../../src/card-controller/actions/actions/toggle';
|
||||
import { createCardAPI, createHASS, createStateEntity } from '../../../test-utils';
|
||||
|
||||
describe('ToggleAction', () => {
|
||||
describe('should toggle entities', () => {
|
||||
it.each([
|
||||
['light.office' as const, 'off' as const, 'light' as const, 'turn_on' as const],
|
||||
['light.office' as const, 'on' as const, 'light' as const, 'turn_off' as const],
|
||||
[
|
||||
'cover.door' as const,
|
||||
'closed' as const,
|
||||
'cover' as const,
|
||||
'open_cover' as const,
|
||||
],
|
||||
['cover.door' as const, 'open' as const, 'cover' as const, 'close_cover' as const],
|
||||
['lock.door' as const, 'locked' as const, 'lock' as const, 'unlock' as const],
|
||||
['lock.door' as const, 'unlocked' as const, 'lock' as const, 'lock' as const],
|
||||
[
|
||||
'group.foo' as const,
|
||||
'off' as const,
|
||||
'homeassistant' as const,
|
||||
'turn_on' as const,
|
||||
],
|
||||
[
|
||||
'group.foo' as const,
|
||||
'on' as const,
|
||||
'homeassistant' as const,
|
||||
'turn_off' as const,
|
||||
],
|
||||
])(
|
||||
'%s %s',
|
||||
async (
|
||||
entityID: string,
|
||||
state: string,
|
||||
expectedServiceDomain: string,
|
||||
expectedService: string,
|
||||
) => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS({
|
||||
[entityID]: createStateEntity({ entity_id: entityID, state: state }),
|
||||
});
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new ToggleAction({}, { action: 'toggle' }, { entity: entityID });
|
||||
await action.execute(api);
|
||||
|
||||
expect(hass.callService).toBeCalledWith(expectedServiceDomain, expectedService, {
|
||||
entity_id: entityID,
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('should do nothing without an entity ID', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new ToggleAction({}, { action: 'toggle' }, {});
|
||||
|
||||
await action.execute(api);
|
||||
|
||||
expect(hass.callService).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should do nothing without an entity state', async () => {
|
||||
const api = createCardAPI();
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
|
||||
const action = new ToggleAction(
|
||||
{},
|
||||
{ action: 'toggle' },
|
||||
{ entity: 'light.NOT_FOUND' },
|
||||
);
|
||||
await action.execute(api);
|
||||
|
||||
expect(hass.callService).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { URLAction } from '../../../../src/card-controller/actions/actions/url';
|
||||
import { createCardAPI } from '../../../test-utils';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('URLAction', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should open the URL in a new window', async () => {
|
||||
const urlAction = new URLAction(
|
||||
{},
|
||||
{
|
||||
action: 'url',
|
||||
url_path: 'https://example.com',
|
||||
},
|
||||
);
|
||||
const windowOpenSpy = vi.spyOn(window, 'open').mockImplementation(() => null);
|
||||
|
||||
await urlAction.execute(createCardAPI());
|
||||
|
||||
expect(windowOpenSpy).toHaveBeenCalledWith('https://example.com');
|
||||
});
|
||||
});
|
||||
@@ -1,12 +1,13 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { CallServiceAction } from '../../../src/card-controller/actions/actions/call-service';
|
||||
import { CameraSelectAction } from '../../../src/card-controller/actions/actions/camera-select';
|
||||
import { CameraUIAction } from '../../../src/card-controller/actions/actions/camera-ui';
|
||||
import { CustomAction } from '../../../src/card-controller/actions/actions/custom';
|
||||
import { DefaultAction } from '../../../src/card-controller/actions/actions/default';
|
||||
import { DisplayModeSelectAction } from '../../../src/card-controller/actions/actions/display-mode-select';
|
||||
import { DownloadAction } from '../../../src/card-controller/actions/actions/download';
|
||||
import { ExpandAction } from '../../../src/card-controller/actions/actions/expand';
|
||||
import { FullscreenAction } from '../../../src/card-controller/actions/actions/fullscreen';
|
||||
import { GenericAction } from '../../../src/card-controller/actions/actions/generic';
|
||||
import { InternalCallbackAction } from '../../../src/card-controller/actions/actions/internal-callback';
|
||||
import { LogAction } from '../../../src/card-controller/actions/actions/log';
|
||||
import { MediaPlayerAction } from '../../../src/card-controller/actions/actions/media-player';
|
||||
@@ -15,8 +16,12 @@ import { MicrophoneConnectAction } from '../../../src/card-controller/actions/ac
|
||||
import { MicrophoneDisconnectAction } from '../../../src/card-controller/actions/actions/microphone-disconnect';
|
||||
import { MicrophoneMuteAction } from '../../../src/card-controller/actions/actions/microphone-mute';
|
||||
import { MicrophoneUnmuteAction } from '../../../src/card-controller/actions/actions/microphone-unmute';
|
||||
import { MoreInfoAction } from '../../../src/card-controller/actions/actions/more-info';
|
||||
import { MuteAction } from '../../../src/card-controller/actions/actions/mute';
|
||||
import { NavigateAction } from '../../../src/card-controller/actions/actions/navigate';
|
||||
import { NoneAction } from '../../../src/card-controller/actions/actions/none';
|
||||
import { PauseAction } from '../../../src/card-controller/actions/actions/pause';
|
||||
import { PerformActionAction } from '../../../src/card-controller/actions/actions/perform-action';
|
||||
import { PlayAction } from '../../../src/card-controller/actions/actions/play';
|
||||
import { PTZAction } from '../../../src/card-controller/actions/actions/ptz';
|
||||
import { PTZControlsAction } from '../../../src/card-controller/actions/actions/ptz-controls';
|
||||
@@ -28,13 +33,12 @@ import { StatusBarAction } from '../../../src/card-controller/actions/actions/st
|
||||
import { SubstreamOffAction } from '../../../src/card-controller/actions/actions/substream-off';
|
||||
import { SubstreamOnAction } from '../../../src/card-controller/actions/actions/substream-on';
|
||||
import { SubstreamSelectAction } from '../../../src/card-controller/actions/actions/substream-select';
|
||||
import { ToggleAction } from '../../../src/card-controller/actions/actions/toggle';
|
||||
import { UnmuteAction } from '../../../src/card-controller/actions/actions/unmute';
|
||||
import { URLAction } from '../../../src/card-controller/actions/actions/url';
|
||||
import { ViewAction } from '../../../src/card-controller/actions/actions/view';
|
||||
import { ActionFactory } from '../../../src/card-controller/actions/factory';
|
||||
import {
|
||||
AdvancedCameraCardCustomAction,
|
||||
INTERNAL_CALLBACK_ACTION,
|
||||
} from '../../../src/config/types';
|
||||
import { ActionConfig, INTERNAL_CALLBACK_ACTION } from '../../../src/config/types';
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('ActionFactory', () => {
|
||||
@@ -55,23 +59,26 @@ describe('ActionFactory', () => {
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
describe('generic', () => {
|
||||
it('non advanced camera card action', () => {
|
||||
describe('stock actions', () => {
|
||||
it.each([
|
||||
[{ action: 'more-info' as const }, MoreInfoAction],
|
||||
[{ action: 'toggle' as const }, ToggleAction],
|
||||
[{ action: 'navigate' as const, navigation_path: '/foo' }, NavigateAction],
|
||||
[{ action: 'url' as const, url_path: 'https://card.camera' }, URLAction],
|
||||
[
|
||||
{ action: 'perform-action' as const, perform_action: 'action' },
|
||||
PerformActionAction,
|
||||
],
|
||||
[{ action: 'call-service' as const, service: 'service' }, CallServiceAction],
|
||||
[{ action: 'none' as const }, NoneAction],
|
||||
[{ action: 'fire-dom-event' as const }, CustomAction],
|
||||
])('action: $action', (action: ActionConfig, classObject: object) => {
|
||||
const factory = new ActionFactory();
|
||||
expect(factory.createAction({}, { action: 'fire-dom-event' })).toBeInstanceOf(
|
||||
GenericAction,
|
||||
);
|
||||
});
|
||||
|
||||
it('non fire-dom-event', () => {
|
||||
const factory = new ActionFactory();
|
||||
expect(factory.createAction({}, { action: 'more-info' })).toBeInstanceOf(
|
||||
GenericAction,
|
||||
);
|
||||
expect(factory.createAction({}, action)).toBeInstanceOf(classObject);
|
||||
});
|
||||
});
|
||||
|
||||
describe('actions', () => {
|
||||
describe('custom actions', () => {
|
||||
it.each([
|
||||
[{ advanced_camera_card_action: 'camera_select' as const }, CameraSelectAction],
|
||||
[{ advanced_camera_card_action: 'camera_ui' as const }, CameraUIAction],
|
||||
@@ -178,10 +185,10 @@ describe('ActionFactory', () => {
|
||||
],
|
||||
])(
|
||||
'advanced_camera_card_action: $advanced_camera_card_action',
|
||||
(action: Partial<AdvancedCameraCardCustomAction>, classObject: object) => {
|
||||
(action: Partial<ActionConfig>, classObject: object) => {
|
||||
const factory = new ActionFactory();
|
||||
expect(
|
||||
factory.createAction({}, { action: 'fire-dom-event', ...action }),
|
||||
factory.createAction({}, { ...action, action: 'fire-dom-event' }),
|
||||
).toBeInstanceOf(classObject);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { AuxillaryActionConfig } from '../../src/card-controller/actions/types.js';
|
||||
import { AutomationsManager } from '../../src/card-controller/automations-manager.js';
|
||||
import { ConditionStateManager } from '../../src/conditions/state-manager.js';
|
||||
import { ActionType } from '../../src/config/types.js';
|
||||
import { ActionConfig } from '../../src/config/types.js';
|
||||
import { createCardAPI } from '../test-utils.js';
|
||||
|
||||
describe('AutomationsManager', () => {
|
||||
@@ -151,9 +150,9 @@ describe('AutomationsManager', () => {
|
||||
vi.mocked(api.getActionsManager().executeActions).mockImplementation(
|
||||
async (
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_action: ActionType | ActionType[],
|
||||
_action: ActionConfig | ActionConfig[],
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_config?: AuxillaryActionConfig,
|
||||
_options?: unknown,
|
||||
): Promise<void> => {
|
||||
fullscreen = !fullscreen;
|
||||
stateManager.setState({ fullscreen: fullscreen });
|
||||
|
||||
@@ -14,7 +14,6 @@ import {
|
||||
describe('CardElementManager', () => {
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals();
|
||||
global.window.location = mock<Location>();
|
||||
});
|
||||
|
||||
it('should get element', () => {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
|
||||
import { SubstreamSelectViewModifier } from '../../src/card-controller/view/modifiers/substream-select';
|
||||
@@ -7,13 +7,14 @@ import { createCardAPI } from '../test-utils';
|
||||
const setQueryString = (qs: string): void => {
|
||||
const location: Location = mock<Location>();
|
||||
location.search = qs;
|
||||
global.window.location = location;
|
||||
|
||||
vi.spyOn(window, 'location', 'get').mockReturnValue(location);
|
||||
};
|
||||
|
||||
// @vitest-environment jsdom
|
||||
describe('QueryStringManager', () => {
|
||||
beforeEach(() => {
|
||||
global.window.location = mock<Location>();
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should reject malformed query string', async () => {
|
||||
|
||||
@@ -2,16 +2,12 @@ import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { MenuController } from '../../src/components-lib/menu-controller.js';
|
||||
import { SubmenuItem } from '../../src/components/submenu/types.js';
|
||||
import { MenuConfig, menuConfigSchema } from '../../src/config/types.js';
|
||||
import { handleActionConfig } from '../../src/ha/handle-action.js';
|
||||
import {
|
||||
createInteractionActionEvent,
|
||||
createLitElement,
|
||||
createSubmenuInteractionActionEvent,
|
||||
} from '../test-utils';
|
||||
|
||||
vi.mock('../../src/ha/handle-action.js');
|
||||
vi.mock('../../src/utils/ha');
|
||||
|
||||
const createMenuConfig = (config: unknown): MenuConfig => {
|
||||
return menuConfigSchema.parse(config);
|
||||
};
|
||||
@@ -369,9 +365,13 @@ describe('MenuController', () => {
|
||||
|
||||
describe('should handle actions', () => {
|
||||
it('should bail without config', () => {
|
||||
const controller = new MenuController(createLitElement());
|
||||
const host = createLitElement();
|
||||
const handler = vi.fn();
|
||||
host.addEventListener('advanced-camera-card:action:execution-request', handler);
|
||||
|
||||
const controller = new MenuController(host);
|
||||
controller.handleAction(createInteractionActionEvent('tap'));
|
||||
expect(vi.mocked(handleActionConfig)).not.toBeCalled();
|
||||
expect(handler).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should execute simple action in non-hidden menu', () => {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
advancedCameraCardConditionSchema,
|
||||
advancedCameraCardCustomActionsBaseSchema,
|
||||
cameraConfigSchema,
|
||||
conditionalSchema,
|
||||
customSchema,
|
||||
dimensionsConfigSchema,
|
||||
advancedCameraCardConditionSchema,
|
||||
advancedCameraCardCustomActionsBaseSchema,
|
||||
internalAdvancedCameraCardCustomActionSchema,
|
||||
statusBarActionConfigSchema,
|
||||
} from '../../src/config/types';
|
||||
import { createConfig } from '../test-utils';
|
||||
|
||||
@@ -641,7 +641,7 @@ describe('should lazy evaluate schemas', () => {
|
||||
},
|
||||
],
|
||||
};
|
||||
expect(internalAdvancedCameraCardCustomActionSchema.parse(input)).toEqual(input);
|
||||
expect(statusBarActionConfigSchema.parse(input)).toEqual(input);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -43,12 +43,10 @@ import { ConditionStateManager } from '../src/conditions/state-manager';
|
||||
import {
|
||||
AdvancedCameraCardConfig,
|
||||
CameraConfig,
|
||||
InternalAdvancedCameraCardCustomAction,
|
||||
PerformanceConfig,
|
||||
RawAdvancedCameraCardConfig,
|
||||
advancedCameraCardConfigSchema,
|
||||
cameraConfigSchema,
|
||||
internalAdvancedCameraCardCustomActionSchema,
|
||||
performanceConfigSchema,
|
||||
} from '../src/config/types';
|
||||
import { CurrentUser, HomeAssistant } from '../src/ha/types';
|
||||
@@ -61,16 +59,6 @@ import { ViewMedia, ViewMediaType } from '../src/view/media';
|
||||
import { MediaQueriesResults } from '../src/view/media-queries-results';
|
||||
import { View, ViewParameters } from '../src/view/view';
|
||||
|
||||
export const createAction = (
|
||||
action: Record<string, unknown>,
|
||||
): InternalAdvancedCameraCardCustomAction | null => {
|
||||
const result = internalAdvancedCameraCardCustomActionSchema.safeParse({
|
||||
action: 'custom:advanced-camera-card-action',
|
||||
...action,
|
||||
});
|
||||
return result.success ? result.data : null;
|
||||
};
|
||||
|
||||
export const createCameraConfig = (config?: unknown): CameraConfig => {
|
||||
return cameraConfigSchema.parse(config ?? {});
|
||||
};
|
||||
|
||||
+19
-40
@@ -1,9 +1,7 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { actionSchema, INTERNAL_CALLBACK_ACTION } from '../../src/config/types.js';
|
||||
import { hasAction as customCardHasAction } from '../../src/ha/has-action.js';
|
||||
import { ActionConfig, INTERNAL_CALLBACK_ACTION } from '../../src/config/types.js';
|
||||
import {
|
||||
convertActionToCardCustomAction,
|
||||
createCameraAction,
|
||||
createDisplayModeAction,
|
||||
createGeneralAction,
|
||||
@@ -21,30 +19,6 @@ import {
|
||||
stopEventFromActivatingCardWideActions,
|
||||
} from '../../src/utils/action.js';
|
||||
|
||||
vi.mock('../../src/ha/has-action.js');
|
||||
|
||||
describe('convertActionToAdvancedCameraCardCustomAction', () => {
|
||||
it('should skip null action', () => {
|
||||
expect(convertActionToCardCustomAction(null)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should parse valid', () => {
|
||||
expect(
|
||||
convertActionToCardCustomAction({
|
||||
action: 'custom:advanced-camera-card-action',
|
||||
advanced_camera_card_action: 'download',
|
||||
}),
|
||||
).toEqual({
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'download',
|
||||
});
|
||||
});
|
||||
|
||||
it('should not parse invalid', () => {
|
||||
expect(convertActionToCardCustomAction('this is garbage')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createGeneralAction', () => {
|
||||
it('should create general action', () => {
|
||||
expect(
|
||||
@@ -293,10 +267,7 @@ describe('createPerformAction', () => {
|
||||
});
|
||||
|
||||
describe('getActionConfigGivenAction', () => {
|
||||
const action = actionSchema.parse({
|
||||
action: 'fire-dom-event',
|
||||
advanced_camera_card_action: 'clips',
|
||||
});
|
||||
const action = createViewAction('clips');
|
||||
|
||||
it('should not handle undefined arguments', () => {
|
||||
expect(getActionConfigGivenAction()).toBeNull();
|
||||
@@ -348,21 +319,29 @@ describe('getActionConfigGivenAction', () => {
|
||||
});
|
||||
|
||||
describe('hasAction', () => {
|
||||
const action = actionSchema.parse({
|
||||
action: 'toggle',
|
||||
});
|
||||
const realAction = createViewAction('clips');
|
||||
const noneAction: ActionConfig = {
|
||||
action: 'none',
|
||||
};
|
||||
|
||||
afterEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should handle non-array case', () => {
|
||||
expect(hasAction(action)).toBeFalsy();
|
||||
expect(customCardHasAction).toBeCalledTimes(1);
|
||||
it('should return true for real action', () => {
|
||||
expect(hasAction(realAction)).toBeTruthy();
|
||||
});
|
||||
it('should handle array case', () => {
|
||||
expect(hasAction([action, action, action])).toBeFalsy();
|
||||
expect(customCardHasAction).toBeCalledTimes(3);
|
||||
|
||||
it('should return false for none action', () => {
|
||||
expect(hasAction(noneAction)).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should return true with an array of actions some real', () => {
|
||||
expect(hasAction([noneAction, noneAction, realAction])).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should return false with an array of actions none real', () => {
|
||||
expect(hasAction([noneAction, noneAction, noneAction])).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -54,6 +54,9 @@ describe('arrayify', () => {
|
||||
const data = [1, 2, 3];
|
||||
expect(arrayify(data)).toBe(data);
|
||||
});
|
||||
it('should handle undefined', () => {
|
||||
expect(arrayify()).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setify', () => {
|
||||
|
||||
@@ -18,13 +18,13 @@ const media = new ViewMedia('clip', 'camera.office');
|
||||
describe('downloadURL', () => {
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
global.window.location = mock<Location>();
|
||||
});
|
||||
|
||||
it('should download same origin via link', () => {
|
||||
const location: Location & { origin: string } = mock<Location>();
|
||||
location.origin = 'http://foo';
|
||||
global.window.location = location;
|
||||
|
||||
vi.spyOn(window, 'location', 'get').mockReturnValue(location);
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.click = vi.fn();
|
||||
@@ -55,7 +55,8 @@ describe('downloadURL', () => {
|
||||
// Set the origin to the same.
|
||||
const location: Location & { origin: string } = mock<Location>();
|
||||
location.origin = 'http://foo';
|
||||
global.window.location = location;
|
||||
|
||||
vi.spyOn(window, 'location', 'get').mockReturnValue(location);
|
||||
|
||||
const windowSpy = vi.spyOn(window, 'open').mockReturnValue(null);
|
||||
|
||||
@@ -66,8 +67,13 @@ describe('downloadURL', () => {
|
||||
|
||||
describe('downloadMedia', () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(window, 'location', 'get').mockReturnValue(
|
||||
mock<Location>({ origin: 'https://foo' }),
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
global.window.location = mock<Location>({ origin: 'https://foo' });
|
||||
});
|
||||
|
||||
it('should throw error when no media', () => {
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"moduleResolution": "node",
|
||||
"lib": ["es2021", "dom", "dom.iterable"],
|
||||
"noEmit": true,
|
||||
"noErrorTruncation": true,
|
||||
"noErrorTruncation": false,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
|
||||
@@ -16,6 +16,7 @@ const FULL_COVERAGE_FILES_RELATIVE = [
|
||||
'conditions/**/*.ts',
|
||||
'config/**/*.ts',
|
||||
'const.ts',
|
||||
'ha/**/*.ts',
|
||||
'types.ts',
|
||||
'utils/action.ts',
|
||||
'utils/audio.ts',
|
||||
|
||||
Reference in New Issue
Block a user