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 { PTZAction, PTZBaseAction } from '../../config/ptz';
|
||||||
import { ActionPhase, ActionType, CameraConfig } from '../../config/types';
|
import { ActionConfig, ActionPhase, CameraConfig } from '../../config/types';
|
||||||
import { PTZCapabilities, PTZMovementType } from '../../types';
|
import { PTZCapabilities, PTZMovementType } from '../../types';
|
||||||
|
|
||||||
export const getConfiguredPTZAction = (
|
export const getConfiguredPTZAction = (
|
||||||
@@ -9,7 +9,7 @@ export const getConfiguredPTZAction = (
|
|||||||
phase?: ActionPhase;
|
phase?: ActionPhase;
|
||||||
preset?: string;
|
preset?: string;
|
||||||
},
|
},
|
||||||
): ActionType | ActionType[] | null => {
|
): ActionConfig | ActionConfig[] | null => {
|
||||||
if (action === 'preset') {
|
if (action === 'preset') {
|
||||||
return (options?.preset ? cameraConfig.ptz.presets?.[options.preset] : null) ?? null;
|
return (options?.preset ? cameraConfig.ptz.presets?.[options.preset] : null) ?? null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,19 @@
|
|||||||
import { ActionContext } from 'action';
|
import { ActionContext } from 'action';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { ConditionsTriggerData } from '../../conditions/types.js';
|
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 { getActionConfigGivenAction } from '../../utils/action.js';
|
||||||
|
import { allPromises } from '../../utils/basic.js';
|
||||||
import { TemplateRenderer } from '../templates/index.js';
|
import { TemplateRenderer } from '../templates/index.js';
|
||||||
import { CardActionsManagerAPI } from '../types.js';
|
import { CardActionsManagerAPI } from '../types.js';
|
||||||
import { ActionSet } from './actions/set.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;
|
const INTERACTIONS = ['tap', 'double_tap', 'hold', 'start_tap', 'end_tap'] as const;
|
||||||
export type InteractionName = (typeof INTERACTIONS)[number];
|
export type InteractionName = (typeof INTERACTIONS)[number];
|
||||||
@@ -60,7 +67,7 @@ export class ActionsManager {
|
|||||||
/**
|
/**
|
||||||
* Handle an human interaction called on an element (e.g. 'tap').
|
* 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);
|
const result = interactionEventSchema.safeParse(ev);
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return;
|
return;
|
||||||
@@ -76,7 +83,7 @@ export class ActionsManager {
|
|||||||
// actions).
|
// actions).
|
||||||
actionConfig
|
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
|
* cards to fire custom actions. This card itself should not call this, but
|
||||||
* embedded elements may.
|
* embedded elements may.
|
||||||
*/
|
*/
|
||||||
public handleCustomActionEvent = (ev: Event): void => {
|
public handleCustomActionEvent = async (
|
||||||
|
ev: Event | CustomEvent<ActionConfig>,
|
||||||
|
): Promise<void> => {
|
||||||
if (!('detail' in ev)) {
|
if (!('detail' in ev)) {
|
||||||
// The event may or may not be a CustomEvent object. For example, whilst
|
// The event may or may not be a CustomEvent object. For example, whilst
|
||||||
// this card doesn't use custom-card-helpers, embedded elements may:
|
// 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
|
// https://github.com/custom-cards/custom-card-helpers/blob/master/src/fire-event.ts#L70
|
||||||
return;
|
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.
|
// 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(
|
public async executeActions(
|
||||||
action: ActionType | ActionType[],
|
action: ActionConfig | ActionConfig[],
|
||||||
options?: {
|
options?: {
|
||||||
config?: AuxillaryActionConfig;
|
config?: AuxillaryActionConfig;
|
||||||
triggerData?: ConditionsTriggerData;
|
triggerData?: ConditionsTriggerData;
|
||||||
},
|
},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const hass = this._api.getHASSManager().getHASS();
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
const renderedAction =
|
const renderedAction: ActionConfig | ActionConfig[] =
|
||||||
hass && this._templateRenderer
|
hass && this._templateRenderer
|
||||||
? this._templateRenderer.renderRecursively(hass, action, {
|
? (this._templateRenderer.renderRecursively(hass, action, {
|
||||||
conditionState: this._api.getConditionStateManager().getState(),
|
conditionState: this._api.getConditionStateManager().getState(),
|
||||||
triggerData: options?.triggerData,
|
triggerData: options?.triggerData,
|
||||||
})
|
}) as ActionConfig | ActionConfig[])
|
||||||
: action;
|
: action;
|
||||||
|
|
||||||
const actionSet = new ActionSet(this._actionContext, renderedAction, {
|
const actionSet = new ActionSet(this._actionContext, renderedAction, {
|
||||||
@@ -134,7 +143,13 @@ export class ActionsManager {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this._actionsInFlight.push(actionSet);
|
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);
|
this._actionsInFlight = this._actionsInFlight.filter((a) => a !== actionSet);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import { ActionContext } from 'action';
|
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 { 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 _context: ActionContext;
|
||||||
protected _action: T;
|
protected _action: T;
|
||||||
protected _config?: AuxillaryActionConfig;
|
protected _config?: AuxillaryActionConfig;
|
||||||
@@ -14,9 +16,32 @@ export class BaseAction<T> implements Action {
|
|||||||
this._config = config;
|
this._config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
protected _shouldSeekConfirmation(api: CardActionsAPI): boolean {
|
||||||
public async execute(_api: CardActionsAPI): Promise<void> {
|
const hass = api.getHASSManager().getHASS();
|
||||||
// Pass.
|
|
||||||
|
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> {
|
public async stop(): Promise<void> {
|
||||||
@@ -24,6 +49,4 @@ export class BaseAction<T> implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AdvancedCameraCardAction<
|
export class AdvancedCameraCardAction<T extends ActionConfig> extends BaseAction<T> {}
|
||||||
T extends AdvancedCameraCardCustomAction,
|
|
||||||
> 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> {
|
export class CameraSelectAction extends AdvancedCameraCardAction<CameraSelectActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
const selectCameraID =
|
const selectCameraID =
|
||||||
this._action.camera ??
|
this._action.camera ??
|
||||||
(this._action.triggered
|
(this._action.triggered
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class CameraUIAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class CameraUIAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
api.getCameraURLManager().openURL();
|
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> {
|
export class DefaultAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
await api.getViewManager().setViewDefaultWithNewQuery();
|
await api.getViewManager().setViewDefaultWithNewQuery();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class DisplayModeSelectAction extends AdvancedCameraCardAction<DisplayModeActionConfig> {
|
export class DisplayModeSelectAction extends AdvancedCameraCardAction<DisplayModeActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
await api.getViewManager().setViewByParametersWithNewQuery({
|
await api.getViewManager().setViewByParametersWithNewQuery({
|
||||||
params: {
|
params: {
|
||||||
displayMode: this._action.display_mode,
|
displayMode: this._action.display_mode,
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class DownloadAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class DownloadAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
await api.getDownloadManager().downloadViewerMedia();
|
await api.getDownloadManager().downloadViewerMedia();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class ExpandAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class ExpandAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
api.getExpandManager().toggleExpanded();
|
api.getExpandManager().toggleExpanded();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class FullscreenAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class FullscreenAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
api.getFullscreenManager().toggleFullscreen();
|
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> {
|
export class InternalCallbackAction extends AdvancedCameraCardAction<InternalCallbackActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
await this._action.callback(api);
|
await this._action.callback(api);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { CardActionsAPI } from '../../types';
|
|||||||
import { AdvancedCameraCardAction } from './base';
|
import { AdvancedCameraCardAction } from './base';
|
||||||
|
|
||||||
export class LogAction extends AdvancedCameraCardAction<LogActionConfig> {
|
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);
|
console[this._action.level](this._action.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class MediaPlayerAction extends AdvancedCameraCardAction<MediaPlayerActionConfig> {
|
export class MediaPlayerAction extends AdvancedCameraCardAction<MediaPlayerActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
const mediaPlayer = this._action.media_player;
|
const mediaPlayer = this._action.media_player;
|
||||||
const mediaPlayerController = api.getMediaPlayerManager();
|
const mediaPlayerController = api.getMediaPlayerManager();
|
||||||
const view = api.getViewManager().getView();
|
const view = api.getViewManager().getView();
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class MenuToggleAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class MenuToggleAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
api.getCardElementManager().toggleMenu();
|
api.getCardElementManager().toggleMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class MicrophoneConnectAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class MicrophoneConnectAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
await api.getMicrophoneManager().connect();
|
await api.getMicrophoneManager().connect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class MicrophoneDisconnectAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class MicrophoneDisconnectAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
api.getMicrophoneManager().disconnect();
|
api.getMicrophoneManager().disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class MicrophoneMuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class MicrophoneMuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
api.getMicrophoneManager().mute();
|
api.getMicrophoneManager().mute();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class MicrophoneUnmuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class MicrophoneUnmuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
await api.getMicrophoneManager().unmute();
|
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> {
|
export class MuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
await api.getMediaLoadedInfoManager().get()?.mediaPlayerController?.mute();
|
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> {
|
export class PauseAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
await api.getMediaLoadedInfoManager().get()?.mediaPlayerController?.pause();
|
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> {
|
export class PlayAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
await api.getMediaLoadedInfoManager().get()?.mediaPlayerController?.play();
|
await api.getMediaLoadedInfoManager().get()?.mediaPlayerController?.play();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class PTZControlsAction extends AdvancedCameraCardAction<PTZControlsActionConfig> {
|
export class PTZControlsAction extends AdvancedCameraCardAction<PTZControlsActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
api.getViewManager().setViewWithMergedContext({
|
api.getViewManager().setViewWithMergedContext({
|
||||||
ptzControls: { enabled: this._action.enabled },
|
ptzControls: { enabled: this._action.enabled },
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ export class PTZDigitalAction extends AdvancedCameraCardAction<PTZDigitialAction
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
const view = api.getViewManager().getView();
|
const view = api.getViewManager().getView();
|
||||||
if (!view) {
|
if (!view) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { PTZDigitalAction } from './ptz-digital';
|
|||||||
|
|
||||||
export class PTZMultiAction extends AdvancedCameraCardAction<PTZMultiActionConfig> {
|
export class PTZMultiAction extends AdvancedCameraCardAction<PTZMultiActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
const view = api.getViewManager().getView();
|
const view = api.getViewManager().getView();
|
||||||
let targetID: string | null = null;
|
let targetID: string | null = null;
|
||||||
let type: PTZType | null = null;
|
let type: PTZType | null = null;
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
const view = api.getViewManager().getView();
|
const view = api.getViewManager().getView();
|
||||||
if (!view) {
|
if (!view) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class ScreenshotAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class ScreenshotAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
await api.getDownloadManager().downloadScreenshot();
|
await api.getDownloadManager().downloadScreenshot();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { ActionContext } from 'action';
|
import { ActionContext } from 'action';
|
||||||
import { ActionType } from '../../../config/types';
|
import { ActionConfig, AuxillaryActionConfig } from '../../../config/types';
|
||||||
import { arrayify } from '../../../utils/basic';
|
import { arrayify } from '../../../utils/basic';
|
||||||
import { CardActionsAPI } from '../../types';
|
import { CardActionsAPI } from '../../types';
|
||||||
import { ActionFactory } from '../factory';
|
import { ActionFactory } from '../factory';
|
||||||
import { Action, AuxillaryActionConfig } from '../types';
|
import { Action } from '../types';
|
||||||
|
|
||||||
export class ActionSet implements Action {
|
export class ActionSet implements Action {
|
||||||
protected _context: ActionContext;
|
protected _context: ActionContext;
|
||||||
@@ -13,7 +13,7 @@ export class ActionSet implements Action {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
context: ActionContext,
|
context: ActionContext,
|
||||||
actions: ActionType | ActionType[],
|
actions: ActionConfig | ActionConfig[],
|
||||||
options?: {
|
options?: {
|
||||||
config?: AuxillaryActionConfig;
|
config?: AuxillaryActionConfig;
|
||||||
cardID?: string;
|
cardID?: string;
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import { timeDeltaToSeconds } from '../utils/time-delta';
|
|||||||
import { AdvancedCameraCardAction } from './base';
|
import { AdvancedCameraCardAction } from './base';
|
||||||
|
|
||||||
export class SleepAction extends AdvancedCameraCardAction<SleepActionConfig> {
|
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));
|
await sleep(timeDeltaToSeconds(this._action.duration));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
export class StatusBarAction extends AdvancedCameraCardAction<StatusBarActionConfig> {
|
export class StatusBarAction extends AdvancedCameraCardAction<StatusBarActionConfig> {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// 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);
|
||||||
|
|
||||||
switch (this._action.status_bar_action) {
|
switch (this._action.status_bar_action) {
|
||||||
case 'reset':
|
case 'reset':
|
||||||
api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
|
api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class SubstreamOffAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class SubstreamOffAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
api.getViewManager().setViewByParameters({
|
api.getViewManager().setViewByParameters({
|
||||||
modifiers: [new SubstreamOffViewModifier()],
|
modifiers: [new SubstreamOffViewModifier()],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class SubstreamOnAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
export class SubstreamOnAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
api.getViewManager().setViewByParameters({
|
api.getViewManager().setViewByParameters({
|
||||||
modifiers: [new SubstreamOnViewModifier(api)],
|
modifiers: [new SubstreamOnViewModifier(api)],
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import { AdvancedCameraCardAction } from './base';
|
|||||||
|
|
||||||
export class SubstreamSelectAction extends AdvancedCameraCardAction<SubstreamSelectActionConfig> {
|
export class SubstreamSelectAction extends AdvancedCameraCardAction<SubstreamSelectActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
api.getViewManager().setViewByParameters({
|
api.getViewManager().setViewByParameters({
|
||||||
modifiers: [new SubstreamSelectViewModifier(this._action.camera)],
|
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> {
|
export class UnmuteAction extends AdvancedCameraCardAction<GeneralActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
await api.getMediaLoadedInfoManager().get()?.mediaPlayerController?.unmute();
|
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> {
|
export class ViewAction extends AdvancedCameraCardAction<ViewActionConfig> {
|
||||||
public async execute(api: CardActionsAPI): Promise<void> {
|
public async execute(api: CardActionsAPI): Promise<void> {
|
||||||
|
await super.execute(api);
|
||||||
|
|
||||||
await api.getViewManager().setViewByParametersWithNewQuery({
|
await api.getViewManager().setViewByParametersWithNewQuery({
|
||||||
params: {
|
params: {
|
||||||
view: this._action.advanced_camera_card_action,
|
view: this._action.advanced_camera_card_action,
|
||||||
|
|||||||
@@ -1,15 +1,19 @@
|
|||||||
import { ActionContext } from 'action';
|
import { ActionContext } from 'action';
|
||||||
import { ActionType, INTERNAL_CALLBACK_ACTION } from '../../config/types';
|
import {
|
||||||
import { ActionConfig } from '../../ha/types';
|
ActionConfig,
|
||||||
import { convertActionToCardCustomAction } from '../../utils/action';
|
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 { CameraSelectAction } from './actions/camera-select';
|
||||||
import { CameraUIAction } from './actions/camera-ui';
|
import { CameraUIAction } from './actions/camera-ui';
|
||||||
|
import { CustomAction } from './actions/custom';
|
||||||
import { DefaultAction } from './actions/default';
|
import { DefaultAction } from './actions/default';
|
||||||
import { DisplayModeSelectAction } from './actions/display-mode-select';
|
import { DisplayModeSelectAction } from './actions/display-mode-select';
|
||||||
import { DownloadAction } from './actions/download';
|
import { DownloadAction } from './actions/download';
|
||||||
import { ExpandAction } from './actions/expand';
|
import { ExpandAction } from './actions/expand';
|
||||||
import { FullscreenAction } from './actions/fullscreen';
|
import { FullscreenAction } from './actions/fullscreen';
|
||||||
import { GenericAction } from './actions/generic';
|
|
||||||
import { InternalCallbackAction } from './actions/internal-callback';
|
import { InternalCallbackAction } from './actions/internal-callback';
|
||||||
import { LogAction } from './actions/log';
|
import { LogAction } from './actions/log';
|
||||||
import { MediaPlayerAction } from './actions/media-player';
|
import { MediaPlayerAction } from './actions/media-player';
|
||||||
@@ -18,8 +22,12 @@ import { MicrophoneConnectAction } from './actions/microphone-connect';
|
|||||||
import { MicrophoneDisconnectAction } from './actions/microphone-disconnect';
|
import { MicrophoneDisconnectAction } from './actions/microphone-disconnect';
|
||||||
import { MicrophoneMuteAction } from './actions/microphone-mute';
|
import { MicrophoneMuteAction } from './actions/microphone-mute';
|
||||||
import { MicrophoneUnmuteAction } from './actions/microphone-unmute';
|
import { MicrophoneUnmuteAction } from './actions/microphone-unmute';
|
||||||
|
import { MoreInfoAction } from './actions/more-info';
|
||||||
import { MuteAction } from './actions/mute';
|
import { MuteAction } from './actions/mute';
|
||||||
|
import { NavigateAction } from './actions/navigate';
|
||||||
|
import { NoneAction } from './actions/none';
|
||||||
import { PauseAction } from './actions/pause';
|
import { PauseAction } from './actions/pause';
|
||||||
|
import { PerformActionAction } from './actions/perform-action';
|
||||||
import { PlayAction } from './actions/play';
|
import { PlayAction } from './actions/play';
|
||||||
import { PTZAction } from './actions/ptz';
|
import { PTZAction } from './actions/ptz';
|
||||||
import { PTZControlsAction } from './actions/ptz-controls';
|
import { PTZControlsAction } from './actions/ptz-controls';
|
||||||
@@ -31,39 +39,53 @@ import { StatusBarAction } from './actions/status-bar';
|
|||||||
import { SubstreamOffAction } from './actions/substream-off';
|
import { SubstreamOffAction } from './actions/substream-off';
|
||||||
import { SubstreamOnAction } from './actions/substream-on';
|
import { SubstreamOnAction } from './actions/substream-on';
|
||||||
import { SubstreamSelectAction } from './actions/substream-select';
|
import { SubstreamSelectAction } from './actions/substream-select';
|
||||||
|
import { ToggleAction } from './actions/toggle';
|
||||||
import { UnmuteAction } from './actions/unmute';
|
import { UnmuteAction } from './actions/unmute';
|
||||||
|
import { URLAction } from './actions/url';
|
||||||
import { ViewAction } from './actions/view';
|
import { ViewAction } from './actions/view';
|
||||||
import { Action, AuxillaryActionConfig } from './types';
|
import { Action } from './types';
|
||||||
|
|
||||||
export class ActionFactory {
|
export class ActionFactory {
|
||||||
public createAction(
|
public createAction(
|
||||||
context: ActionContext,
|
context: ActionContext,
|
||||||
action: ActionType,
|
action: ActionConfig,
|
||||||
options?: {
|
options?: {
|
||||||
config?: AuxillaryActionConfig;
|
config?: AuxillaryActionConfig;
|
||||||
cardID?: string;
|
cardID?: string;
|
||||||
},
|
},
|
||||||
): Action | null {
|
): 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 (
|
if (
|
||||||
// Command not intended for this card (e.g. query string command).
|
// Command not intended for this card (e.g. query string command).
|
||||||
cardCustomAction.card_id &&
|
action.card_id &&
|
||||||
cardCustomAction.card_id !== options?.cardID
|
action.card_id !== options?.cardID
|
||||||
) {
|
) {
|
||||||
return null;
|
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':
|
case 'default':
|
||||||
return new DefaultAction(context, cardCustomAction, options?.config);
|
return new DefaultAction(context, action, options?.config);
|
||||||
case 'clip':
|
case 'clip':
|
||||||
case 'clips':
|
case 'clips':
|
||||||
case 'image':
|
case 'image':
|
||||||
@@ -74,72 +96,68 @@ export class ActionFactory {
|
|||||||
case 'snapshots':
|
case 'snapshots':
|
||||||
case 'timeline':
|
case 'timeline':
|
||||||
case 'diagnostics':
|
case 'diagnostics':
|
||||||
return new ViewAction(context, cardCustomAction, options?.config);
|
return new ViewAction(context, action, options?.config);
|
||||||
case 'sleep':
|
case 'sleep':
|
||||||
return new SleepAction(context, cardCustomAction, options?.config);
|
return new SleepAction(context, action, options?.config);
|
||||||
case 'download':
|
case 'download':
|
||||||
return new DownloadAction(context, cardCustomAction, options?.config);
|
return new DownloadAction(context, action, options?.config);
|
||||||
case 'camera_ui':
|
case 'camera_ui':
|
||||||
return new CameraUIAction(context, cardCustomAction, options?.config);
|
return new CameraUIAction(context, action, options?.config);
|
||||||
case 'expand':
|
case 'expand':
|
||||||
return new ExpandAction(context, cardCustomAction, options?.config);
|
return new ExpandAction(context, action, options?.config);
|
||||||
case 'fullscreen':
|
case 'fullscreen':
|
||||||
return new FullscreenAction(context, cardCustomAction, options?.config);
|
return new FullscreenAction(context, action, options?.config);
|
||||||
case 'menu_toggle':
|
case 'menu_toggle':
|
||||||
return new MenuToggleAction(context, cardCustomAction, options?.config);
|
return new MenuToggleAction(context, action, options?.config);
|
||||||
case 'camera_select':
|
case 'camera_select':
|
||||||
return new CameraSelectAction(context, cardCustomAction, options?.config);
|
return new CameraSelectAction(context, action, options?.config);
|
||||||
case 'live_substream_select':
|
case 'live_substream_select':
|
||||||
return new SubstreamSelectAction(context, cardCustomAction, options?.config);
|
return new SubstreamSelectAction(context, action, options?.config);
|
||||||
case 'live_substream_off':
|
case 'live_substream_off':
|
||||||
return new SubstreamOffAction(context, cardCustomAction, options?.config);
|
return new SubstreamOffAction(context, action, options?.config);
|
||||||
case 'live_substream_on':
|
case 'live_substream_on':
|
||||||
return new SubstreamOnAction(context, cardCustomAction, options?.config);
|
return new SubstreamOnAction(context, action, options?.config);
|
||||||
case 'media_player':
|
case 'media_player':
|
||||||
return new MediaPlayerAction(context, cardCustomAction, options?.config);
|
return new MediaPlayerAction(context, action, options?.config);
|
||||||
case 'microphone_connect':
|
case 'microphone_connect':
|
||||||
return new MicrophoneConnectAction(context, cardCustomAction, options?.config);
|
return new MicrophoneConnectAction(context, action, options?.config);
|
||||||
case 'microphone_disconnect':
|
case 'microphone_disconnect':
|
||||||
return new MicrophoneDisconnectAction(
|
return new MicrophoneDisconnectAction(context, action, options?.config);
|
||||||
context,
|
|
||||||
cardCustomAction,
|
|
||||||
options?.config,
|
|
||||||
);
|
|
||||||
case 'microphone_mute':
|
case 'microphone_mute':
|
||||||
return new MicrophoneMuteAction(context, cardCustomAction, options?.config);
|
return new MicrophoneMuteAction(context, action, options?.config);
|
||||||
case 'microphone_unmute':
|
case 'microphone_unmute':
|
||||||
return new MicrophoneUnmuteAction(context, cardCustomAction, options?.config);
|
return new MicrophoneUnmuteAction(context, action, options?.config);
|
||||||
case 'mute':
|
case 'mute':
|
||||||
return new MuteAction(context, cardCustomAction, options?.config);
|
return new MuteAction(context, action, options?.config);
|
||||||
case 'unmute':
|
case 'unmute':
|
||||||
return new UnmuteAction(context, cardCustomAction, options?.config);
|
return new UnmuteAction(context, action, options?.config);
|
||||||
case 'play':
|
case 'play':
|
||||||
return new PlayAction(context, cardCustomAction, options?.config);
|
return new PlayAction(context, action, options?.config);
|
||||||
case 'pause':
|
case 'pause':
|
||||||
return new PauseAction(context, cardCustomAction, options?.config);
|
return new PauseAction(context, action, options?.config);
|
||||||
case 'screenshot':
|
case 'screenshot':
|
||||||
return new ScreenshotAction(context, cardCustomAction, options?.config);
|
return new ScreenshotAction(context, action, options?.config);
|
||||||
case 'display_mode_select':
|
case 'display_mode_select':
|
||||||
return new DisplayModeSelectAction(context, cardCustomAction, options?.config);
|
return new DisplayModeSelectAction(context, action, options?.config);
|
||||||
case 'ptz':
|
case 'ptz':
|
||||||
return new PTZAction(context, cardCustomAction, options?.config);
|
return new PTZAction(context, action, options?.config);
|
||||||
case 'ptz_digital':
|
case 'ptz_digital':
|
||||||
return new PTZDigitalAction(context, cardCustomAction, options?.config);
|
return new PTZDigitalAction(context, action, options?.config);
|
||||||
case 'ptz_multi':
|
case 'ptz_multi':
|
||||||
return new PTZMultiAction(context, cardCustomAction, options?.config);
|
return new PTZMultiAction(context, action, options?.config);
|
||||||
case 'ptz_controls':
|
case 'ptz_controls':
|
||||||
return new PTZControlsAction(context, cardCustomAction, options?.config);
|
return new PTZControlsAction(context, action, options?.config);
|
||||||
case 'log':
|
case 'log':
|
||||||
return new LogAction(context, cardCustomAction, options?.config);
|
return new LogAction(context, action, options?.config);
|
||||||
case 'status_bar':
|
case 'status_bar':
|
||||||
return new StatusBarAction(context, cardCustomAction, options?.config);
|
return new StatusBarAction(context, action, options?.config);
|
||||||
case INTERNAL_CALLBACK_ACTION:
|
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 */
|
/* istanbul ignore next: this path cannot be reached -- @preserve */
|
||||||
console.warn(
|
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 */
|
/* istanbul ignore next: this path cannot be reached -- @preserve */
|
||||||
return null;
|
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';
|
import { CardActionsAPI } from '../types';
|
||||||
|
|
||||||
export interface AuxillaryActionConfig {
|
|
||||||
camera_image?: string;
|
|
||||||
entity?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Action {
|
export interface Action {
|
||||||
execute(api: CardActionsAPI): Promise<void>;
|
execute(api: CardActionsAPI): Promise<void>;
|
||||||
stop(): Promise<void>;
|
stop(): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ActionExecutionRequest {
|
export interface ActionExecutionRequest {
|
||||||
action: ActionType[] | ActionType;
|
action: ActionConfig[] | ActionConfig;
|
||||||
config?: AuxillaryActionConfig;
|
config?: AuxillaryActionConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -21,3 +17,5 @@ export interface TargetedActionContext {
|
|||||||
inProgressAction?: Action;
|
inProgressAction?: Action;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class ActionAbortError extends AdvancedCameraCardError {}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { AdvancedCameraCardCustomAction, ViewActionConfig } from '../config/types';
|
import { AdvancedCameraCardCustomActionConfig, ViewActionConfig } from '../config/types';
|
||||||
import {
|
import {
|
||||||
createCameraAction,
|
createCameraAction,
|
||||||
createGeneralAction,
|
createGeneralAction,
|
||||||
@@ -13,7 +13,7 @@ interface QueryStringViewIntent {
|
|||||||
default?: boolean;
|
default?: boolean;
|
||||||
substream?: string;
|
substream?: string;
|
||||||
};
|
};
|
||||||
other?: AdvancedCameraCardCustomAction[];
|
other?: AdvancedCameraCardCustomActionConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class QueryStringManager {
|
export class QueryStringManager {
|
||||||
@@ -92,9 +92,9 @@ export class QueryStringManager {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _getActions(): AdvancedCameraCardCustomAction[] {
|
protected _getActions(): AdvancedCameraCardCustomActionConfig[] {
|
||||||
const params = new URLSearchParams(window.location.search);
|
const params = new URLSearchParams(window.location.search);
|
||||||
const actions: AdvancedCameraCardCustomAction[] = [];
|
const actions: AdvancedCameraCardCustomActionConfig[] = [];
|
||||||
const actionRE = new RegExp(
|
const actionRE = new RegExp(
|
||||||
/^(advanced-camera-card|frigate-card)-action([.:](?<cardID>\w+))?[.:](?<action>\w+)/,
|
/^(advanced-camera-card|frigate-card)-action([.:](?<cardID>\w+))?[.:](?<action>\w+)/,
|
||||||
);
|
);
|
||||||
@@ -104,14 +104,14 @@ export class QueryStringManager {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const cardID: string | undefined = match.groups['cardID'];
|
const cardID: string | undefined = match.groups['cardID'];
|
||||||
const action = match.groups['action'];
|
const actionName = match.groups['action'];
|
||||||
|
|
||||||
let customAction: AdvancedCameraCardCustomAction | null = null;
|
let action: AdvancedCameraCardCustomActionConfig | null = null;
|
||||||
switch (action) {
|
switch (actionName) {
|
||||||
case 'camera_select':
|
case 'camera_select':
|
||||||
case 'live_substream_select':
|
case 'live_substream_select':
|
||||||
if (value) {
|
if (value) {
|
||||||
customAction = createCameraAction(action, value, {
|
action = createCameraAction(actionName, value, {
|
||||||
cardID: cardID,
|
cardID: cardID,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -121,7 +121,7 @@ export class QueryStringManager {
|
|||||||
case 'download':
|
case 'download':
|
||||||
case 'expand':
|
case 'expand':
|
||||||
case 'menu_toggle':
|
case 'menu_toggle':
|
||||||
customAction = createGeneralAction(action, {
|
action = createGeneralAction(actionName, {
|
||||||
cardID: cardID,
|
cardID: cardID,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
@@ -135,24 +135,24 @@ export class QueryStringManager {
|
|||||||
case 'snapshot':
|
case 'snapshot':
|
||||||
case 'snapshots':
|
case 'snapshots':
|
||||||
case 'timeline':
|
case 'timeline':
|
||||||
customAction = createViewAction(action, {
|
action = createViewAction(actionName, {
|
||||||
cardID: cardID,
|
cardID: cardID,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
console.warn(
|
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) {
|
if (action) {
|
||||||
actions.push(customAction);
|
actions.push(action);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return actions;
|
return actions;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _isViewAction = (
|
protected _isViewAction = (
|
||||||
action: AdvancedCameraCardCustomAction,
|
action: AdvancedCameraCardCustomActionConfig,
|
||||||
): action is ViewActionConfig => {
|
): action is ViewActionConfig => {
|
||||||
switch (action.advanced_camera_card_action) {
|
switch (action.advanced_camera_card_action) {
|
||||||
case 'clip':
|
case 'clip':
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { HASS, renderTemplate } from 'ha-nunjucks/dist';
|
import { HASS, renderTemplate } from 'ha-nunjucks/dist';
|
||||||
import { ConditionState, ConditionsTriggerData } from '../../conditions/types';
|
import { ConditionState, ConditionsTriggerData } from '../../conditions/types';
|
||||||
import { ActionType } from '../../config/types';
|
|
||||||
import { HomeAssistant } from '../../ha/types';
|
import { HomeAssistant } from '../../ha/types';
|
||||||
|
|
||||||
interface TemplateContextInternal {
|
interface TemplateContextInternal {
|
||||||
@@ -24,7 +23,7 @@ export class TemplateRenderer {
|
|||||||
conditionState?: ConditionState;
|
conditionState?: ConditionState;
|
||||||
triggerData?: ConditionsTriggerData;
|
triggerData?: ConditionsTriggerData;
|
||||||
},
|
},
|
||||||
): ActionType => {
|
): unknown => {
|
||||||
return this._renderTemplateRecursively(
|
return this._renderTemplateRecursively(
|
||||||
hass,
|
hass,
|
||||||
data,
|
data,
|
||||||
@@ -59,11 +58,15 @@ export class TemplateRenderer {
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
data: unknown,
|
data: unknown,
|
||||||
templateContext?: TemplateContext,
|
templateContext?: TemplateContext,
|
||||||
): ActionType {
|
): unknown {
|
||||||
if (typeof data === 'string') {
|
if (typeof data === 'string') {
|
||||||
// ha-nunjucks has a more complete model of the Home Assistant object, but
|
return renderTemplate(
|
||||||
// does not export it as a type.
|
// ha-nunjucks has a more complete model of the Home Assistant object, but
|
||||||
return renderTemplate(hass as unknown as typeof HASS, data, templateContext);
|
// does not export it as a type.
|
||||||
|
hass as unknown as typeof HASS,
|
||||||
|
data,
|
||||||
|
templateContext,
|
||||||
|
);
|
||||||
} else if (Array.isArray(data)) {
|
} else if (Array.isArray(data)) {
|
||||||
return data.map((item) =>
|
return data.map((item) =>
|
||||||
this._renderTemplateRecursively(hass, item, templateContext),
|
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 { ViewManager } from '../card-controller/view/view-manager';
|
||||||
import {
|
import {
|
||||||
AdvancedCameraCardConfig,
|
AdvancedCameraCardConfig,
|
||||||
AdvancedCameraCardCustomAction,
|
|
||||||
MenuItem,
|
MenuItem,
|
||||||
VIEWS_USER_SPECIFIED,
|
VIEWS_USER_SPECIFIED,
|
||||||
} from '../config/types';
|
} from '../config/types';
|
||||||
@@ -21,8 +20,9 @@ import {
|
|||||||
createPTZControlsAction,
|
createPTZControlsAction,
|
||||||
createPTZMultiAction,
|
createPTZMultiAction,
|
||||||
createViewAction,
|
createViewAction,
|
||||||
|
isAdvancedCameraCardCustomAction,
|
||||||
} from '../utils/action';
|
} from '../utils/action';
|
||||||
import { isTruthy } from '../utils/basic';
|
import { arrayify, isTruthy } from '../utils/basic';
|
||||||
import { isBeingCasted } from '../utils/casting';
|
import { isBeingCasted } from '../utils/casting';
|
||||||
import { getEntityTitle } from '../utils/ha';
|
import { getEntityTitle } from '../utils/ha';
|
||||||
import { getPTZTarget } from '../utils/ptz';
|
import { getPTZTarget } from '../utils/ptz';
|
||||||
@@ -118,9 +118,9 @@ export class MenuButtonController {
|
|||||||
permanent: true,
|
permanent: true,
|
||||||
tap_action:
|
tap_action:
|
||||||
config.menu?.style === 'hidden'
|
config.menu?.style === 'hidden'
|
||||||
? (createGeneralAction('menu_toggle') as AdvancedCameraCardCustomAction)
|
? createGeneralAction('menu_toggle')
|
||||||
: (createGeneralAction('default') as AdvancedCameraCardCustomAction),
|
: createGeneralAction('default'),
|
||||||
hold_action: createViewAction('diagnostics') as AdvancedCameraCardCustomAction,
|
hold_action: createViewAction('diagnostics'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,7 +191,7 @@ export class MenuButtonController {
|
|||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
tap_action: createGeneralAction(
|
tap_action: createGeneralAction(
|
||||||
hasSubstream(view) ? 'live_substream_off' : 'live_substream_on',
|
hasSubstream(view) ? 'live_substream_off' : 'live_substream_on',
|
||||||
) as AdvancedCameraCardCustomAction,
|
),
|
||||||
};
|
};
|
||||||
} else if (streams.length > 2) {
|
} else if (streams.length > 2) {
|
||||||
const menuItems = Array.from(streams, (streamID) => {
|
const menuItems = Array.from(streams, (streamID) => {
|
||||||
@@ -234,7 +234,7 @@ export class MenuButtonController {
|
|||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.view.views.live'),
|
title: localize('config.view.views.live'),
|
||||||
style: view.is('live') ? this._getEmphasizedStyle() : {},
|
style: view.is('live') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createViewAction('live') as AdvancedCameraCardCustomAction,
|
tap_action: createViewAction('live'),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -251,8 +251,8 @@ export class MenuButtonController {
|
|||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.view.views.clips'),
|
title: localize('config.view.views.clips'),
|
||||||
style: view?.is('clips') ? this._getEmphasizedStyle() : {},
|
style: view?.is('clips') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createViewAction('clips') as AdvancedCameraCardCustomAction,
|
tap_action: createViewAction('clips'),
|
||||||
hold_action: createViewAction('clip') as AdvancedCameraCardCustomAction,
|
hold_action: createViewAction('clip'),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -269,8 +269,8 @@ export class MenuButtonController {
|
|||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.view.views.snapshots'),
|
title: localize('config.view.views.snapshots'),
|
||||||
style: view?.is('snapshots') ? this._getEmphasizedStyle() : {},
|
style: view?.is('snapshots') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createViewAction('snapshots') as AdvancedCameraCardCustomAction,
|
tap_action: createViewAction('snapshots'),
|
||||||
hold_action: createViewAction('snapshot') as AdvancedCameraCardCustomAction,
|
hold_action: createViewAction('snapshot'),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -287,8 +287,8 @@ export class MenuButtonController {
|
|||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.view.views.recordings'),
|
title: localize('config.view.views.recordings'),
|
||||||
style: view.is('recordings') ? this._getEmphasizedStyle() : {},
|
style: view.is('recordings') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createViewAction('recordings') as AdvancedCameraCardCustomAction,
|
tap_action: createViewAction('recordings'),
|
||||||
hold_action: createViewAction('recording') as AdvancedCameraCardCustomAction,
|
hold_action: createViewAction('recording'),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -305,7 +305,7 @@ export class MenuButtonController {
|
|||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.view.views.image'),
|
title: localize('config.view.views.image'),
|
||||||
style: view?.is('image') ? this._getEmphasizedStyle() : {},
|
style: view?.is('image') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createViewAction('image') as AdvancedCameraCardCustomAction,
|
tap_action: createViewAction('image'),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -322,7 +322,7 @@ export class MenuButtonController {
|
|||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.view.views.timeline'),
|
title: localize('config.view.views.timeline'),
|
||||||
style: view.is('timeline') ? this._getEmphasizedStyle() : {},
|
style: view.is('timeline') ? this._getEmphasizedStyle() : {},
|
||||||
tap_action: createViewAction('timeline') as AdvancedCameraCardCustomAction,
|
tap_action: createViewAction('timeline'),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -342,7 +342,7 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.download,
|
...config.menu.buttons.download,
|
||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.download'),
|
title: localize('config.menu.buttons.download'),
|
||||||
tap_action: createGeneralAction('download') as AdvancedCameraCardCustomAction,
|
tap_action: createGeneralAction('download'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -358,7 +358,7 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.camera_ui,
|
...config.menu.buttons.camera_ui,
|
||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.camera_ui'),
|
title: localize('config.menu.buttons.camera_ui'),
|
||||||
tap_action: createGeneralAction('camera_ui') as AdvancedCameraCardCustomAction,
|
tap_action: createGeneralAction('camera_ui'),
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -385,18 +385,14 @@ export class MenuButtonController {
|
|||||||
style: unavailable || muted ? {} : this._getEmphasizedStyle(true),
|
style: unavailable || muted ? {} : this._getEmphasizedStyle(true),
|
||||||
...(!unavailable &&
|
...(!unavailable &&
|
||||||
buttonType === 'momentary' && {
|
buttonType === 'momentary' && {
|
||||||
start_tap_action: createGeneralAction(
|
start_tap_action: createGeneralAction('microphone_unmute'),
|
||||||
'microphone_unmute',
|
end_tap_action: createGeneralAction('microphone_mute'),
|
||||||
) as AdvancedCameraCardCustomAction,
|
|
||||||
end_tap_action: createGeneralAction(
|
|
||||||
'microphone_mute',
|
|
||||||
) as AdvancedCameraCardCustomAction,
|
|
||||||
}),
|
}),
|
||||||
...(!unavailable &&
|
...(!unavailable &&
|
||||||
buttonType === 'toggle' && {
|
buttonType === 'toggle' && {
|
||||||
tap_action: createGeneralAction(
|
tap_action: createGeneralAction(
|
||||||
muted ? 'microphone_unmute' : 'microphone_mute',
|
muted ? 'microphone_unmute' : 'microphone_mute',
|
||||||
) as AdvancedCameraCardCustomAction,
|
),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -412,7 +408,7 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.expand,
|
...config.menu.buttons.expand,
|
||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.expand'),
|
title: localize('config.menu.buttons.expand'),
|
||||||
tap_action: createGeneralAction('expand') as AdvancedCameraCardCustomAction,
|
tap_action: createGeneralAction('expand'),
|
||||||
style: inExpandedMode ? this._getEmphasizedStyle() : {},
|
style: inExpandedMode ? this._getEmphasizedStyle() : {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -428,9 +424,7 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.fullscreen,
|
...config.menu.buttons.fullscreen,
|
||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.fullscreen'),
|
title: localize('config.menu.buttons.fullscreen'),
|
||||||
tap_action: createGeneralAction(
|
tap_action: createGeneralAction('fullscreen'),
|
||||||
'fullscreen',
|
|
||||||
) as AdvancedCameraCardCustomAction,
|
|
||||||
style: inFullscreen ? this._getEmphasizedStyle() : {},
|
style: inFullscreen ? this._getEmphasizedStyle() : {},
|
||||||
}
|
}
|
||||||
: null;
|
: null;
|
||||||
@@ -498,9 +492,7 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.play,
|
...config.menu.buttons.play,
|
||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.play'),
|
title: localize('config.menu.buttons.play'),
|
||||||
tap_action: createGeneralAction(
|
tap_action: createGeneralAction(paused ? 'play' : 'pause'),
|
||||||
paused ? 'play' : 'pause',
|
|
||||||
) as AdvancedCameraCardCustomAction,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -521,9 +513,7 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.mute,
|
...config.menu.buttons.mute,
|
||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.mute'),
|
title: localize('config.menu.buttons.mute'),
|
||||||
tap_action: createGeneralAction(
|
tap_action: createGeneralAction(muted ? 'unmute' : 'mute'),
|
||||||
muted ? 'unmute' : 'mute',
|
|
||||||
) as AdvancedCameraCardCustomAction,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -539,7 +529,7 @@ export class MenuButtonController {
|
|||||||
...config.menu.buttons.screenshot,
|
...config.menu.buttons.screenshot,
|
||||||
type: 'custom:advanced-camera-card-menu-icon',
|
type: 'custom:advanced-camera-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.screenshot'),
|
title: localize('config.menu.buttons.screenshot'),
|
||||||
tap_action: createGeneralAction('screenshot') as AdvancedCameraCardCustomAction,
|
tap_action: createGeneralAction('screenshot'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
@@ -637,7 +627,7 @@ export class MenuButtonController {
|
|||||||
title: localize('config.menu.buttons.ptz_home'),
|
title: localize('config.menu.buttons.ptz_home'),
|
||||||
tap_action: createPTZMultiAction({
|
tap_action: createPTZMultiAction({
|
||||||
targetID: target.targetID,
|
targetID: target.targetID,
|
||||||
}) as AdvancedCameraCardCustomAction,
|
}),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -675,30 +665,23 @@ export class MenuButtonController {
|
|||||||
button.start_tap_action,
|
button.start_tap_action,
|
||||||
button.end_tap_action,
|
button.end_tap_action,
|
||||||
]) {
|
]) {
|
||||||
const actions = Array.isArray(actionSet) ? actionSet : [actionSet];
|
for (const action of arrayify(actionSet)) {
|
||||||
for (const action of actions) {
|
if (!isAdvancedCameraCardCustomAction(action)) {
|
||||||
// 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)
|
|
||||||
) {
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const customCardAction = action as AdvancedCameraCardCustomAction;
|
|
||||||
if (
|
if (
|
||||||
VIEWS_USER_SPECIFIED.some(
|
VIEWS_USER_SPECIFIED.some(
|
||||||
(viewName) =>
|
(viewName) =>
|
||||||
viewName === customCardAction.advanced_camera_card_action &&
|
viewName === action.advanced_camera_card_action &&
|
||||||
options?.view?.is(customCardAction.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)) ||
|
options?.view?.is(config.view.default)) ||
|
||||||
(customCardAction.advanced_camera_card_action === 'fullscreen' &&
|
(action.advanced_camera_card_action === 'fullscreen' &&
|
||||||
!!options?.fullscreenManager?.isInFullscreen()) ||
|
!!options?.fullscreenManager?.isInFullscreen()) ||
|
||||||
(customCardAction.advanced_camera_card_action === 'camera_select' &&
|
(action.advanced_camera_card_action === 'camera_select' &&
|
||||||
options?.view?.camera === customCardAction.camera)
|
options?.view?.camera === action.camera)
|
||||||
) {
|
) {
|
||||||
return this._getEmphasizedStyle();
|
return this._getEmphasizedStyle();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,14 @@ import { orderBy } from 'lodash-es';
|
|||||||
import { dispatchActionExecutionRequest } from '../card-controller/actions/utils/execution-request.js';
|
import { dispatchActionExecutionRequest } from '../card-controller/actions/utils/execution-request.js';
|
||||||
import { SubmenuInteraction } from '../components/submenu/types.js';
|
import { SubmenuInteraction } from '../components/submenu/types.js';
|
||||||
import {
|
import {
|
||||||
|
ActionConfig,
|
||||||
MENU_PRIORITY_MAX,
|
MENU_PRIORITY_MAX,
|
||||||
type ActionType,
|
|
||||||
type ActionsConfig,
|
type ActionsConfig,
|
||||||
type MenuConfig,
|
type MenuConfig,
|
||||||
type MenuItem,
|
type MenuItem,
|
||||||
} from '../config/types.js';
|
} from '../config/types.js';
|
||||||
import { Interaction } from '../types.js';
|
import { Interaction } from '../types.js';
|
||||||
import {
|
import { getActionConfigGivenAction } from '../utils/action';
|
||||||
convertActionToCardCustomAction,
|
|
||||||
getActionConfigGivenAction,
|
|
||||||
} from '../utils/action';
|
|
||||||
import { arrayify, isTruthy, setOrRemoveAttribute } from '../utils/basic.js';
|
import { arrayify, isTruthy, setOrRemoveAttribute } from '../utils/basic.js';
|
||||||
|
|
||||||
export class MenuController {
|
export class MenuController {
|
||||||
@@ -114,7 +111,7 @@ export class MenuController {
|
|||||||
let menuToggle = false;
|
let menuToggle = false;
|
||||||
|
|
||||||
const toggleLessActions = actions.filter(
|
const toggleLessActions = actions.filter(
|
||||||
(item) => isTruthy(item) && !this._isUnknownActionMenuToggleAction(item),
|
(item) => isTruthy(item) && !this._isMenuToggleAction(item),
|
||||||
);
|
);
|
||||||
if (toggleLessActions.length != actions.length) {
|
if (toggleLessActions.length != actions.length) {
|
||||||
menuToggle = true;
|
menuToggle = true;
|
||||||
@@ -171,8 +168,10 @@ export class MenuController {
|
|||||||
return this._config?.style === 'hidden';
|
return this._config?.style === 'hidden';
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _isUnknownActionMenuToggleAction(action: ActionType): boolean {
|
protected _isMenuToggleAction(action: ActionConfig): boolean {
|
||||||
const parsedAction = convertActionToCardCustomAction(action);
|
return (
|
||||||
return !!parsedAction && parsedAction.advanced_camera_card_action == 'menu_toggle';
|
action.action === 'fire-dom-event' &&
|
||||||
|
action.advanced_camera_card_action === 'menu_toggle'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.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 { HomeAssistant } from '../../ha/types.js';
|
||||||
import menuButtonStyle from '../../scss/menu-button.scss';
|
import menuButtonStyle from '../../scss/menu-button.scss';
|
||||||
|
import { Icon } from '../../types.js';
|
||||||
import { getEntityTitle, isHassDifferent } from '../../utils/ha';
|
import { getEntityTitle, isHassDifferent } from '../../utils/ha';
|
||||||
import { getEntityStateTranslation } from '../../utils/ha/entity-state-translation.js';
|
import { getEntityStateTranslation } from '../../utils/ha/entity-state-translation.js';
|
||||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity/index.js';
|
import { EntityRegistryManager } from '../../utils/ha/registry/entity/index.js';
|
||||||
@@ -31,7 +32,8 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
|
|||||||
@state()
|
@state()
|
||||||
protected _optionTitles?: Record<string, string>;
|
protected _optionTitles?: Record<string, string>;
|
||||||
|
|
||||||
protected _generatedSubmenu?: MenuSubmenu;
|
protected _generatedSubmenuItems?: MenuSubmenuItem[];
|
||||||
|
protected _generatedIcon?: Icon;
|
||||||
|
|
||||||
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
protected shouldUpdate(changedProps: PropertyValues): boolean {
|
||||||
// No need to update the submenu unless the select entity has changed.
|
// No need to update the submenu unless the select entity has changed.
|
||||||
@@ -86,29 +88,7 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const title = getEntityTitle(this.hass, entityID);
|
const items: MenuSubmenuItem[] = [];
|
||||||
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[];
|
|
||||||
|
|
||||||
for (const option of options) {
|
for (const option of options) {
|
||||||
const title = this._optionTitles?.[option] ?? option;
|
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 {
|
protected render(): TemplateResult {
|
||||||
const submenu = this._generatedSubmenu;
|
if (!this._generatedSubmenuItems || !this._generatedIcon || !this.submenuSelect) {
|
||||||
if (!submenu) {
|
|
||||||
return html``;
|
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
|
return html` <advanced-camera-card-submenu
|
||||||
.hass=${this.hass}
|
.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
|
<advanced-camera-card-icon
|
||||||
?allow-override-non-active-styles=${true}
|
?allow-override-non-active-styles=${true}
|
||||||
style="${style}"
|
style="${style}"
|
||||||
title=${submenu.title || ''}
|
title=${title || ''}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.icon=${typeof submenu.icon === 'string'
|
.icon=${this._generatedIcon}
|
||||||
? {
|
|
||||||
icon: submenu.icon,
|
|
||||||
}
|
|
||||||
: submenu.icon}
|
|
||||||
></advanced-camera-card-icon>
|
></advanced-camera-card-icon>
|
||||||
</ha-icon-button>
|
</ha-icon-button>
|
||||||
</advanced-camera-card-submenu>`;
|
</advanced-camera-card-submenu>`;
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { customElement, property } from 'lit/decorators.js';
|
|||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
import { actionHandler } from '../../action-handler-directive.js';
|
import { actionHandler } from '../../action-handler-directive.js';
|
||||||
import { MenuSubmenu } from '../../config/types.js';
|
import { MenuSubmenu } from '../../config/types.js';
|
||||||
import { hasAction } from '../../ha/has-action.js';
|
|
||||||
import { HomeAssistant } from '../../ha/types.js';
|
import { HomeAssistant } from '../../ha/types.js';
|
||||||
import menuButtonStyle from '../../scss/menu-button.scss';
|
import menuButtonStyle from '../../scss/menu-button.scss';
|
||||||
|
import { hasAction } from '../../utils/action.js';
|
||||||
import '../icon.js';
|
import '../icon.js';
|
||||||
import './index.js';
|
import './index.js';
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
import { Actions } from '../../config/types';
|
||||||
import { Interaction } from '../../types';
|
import { Interaction } from '../../types';
|
||||||
|
|
||||||
export interface SubmenuItem {
|
export interface SubmenuItem extends Actions {
|
||||||
title?: string;
|
title?: string;
|
||||||
subtitle?: string;
|
subtitle?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
@@ -8,10 +9,6 @@ export interface SubmenuItem {
|
|||||||
style?: Record<string, string>;
|
style?: Record<string, string>;
|
||||||
enabled?: boolean;
|
enabled?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
|
|
||||||
hold_action?: unknown;
|
|
||||||
double_tap_action?: unknown;
|
|
||||||
[key: string]: unknown;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SubmenuInteraction extends Interaction {
|
export interface SubmenuInteraction extends Interaction {
|
||||||
|
|||||||
+126
-154
@@ -1,16 +1,6 @@
|
|||||||
import { HassServiceTarget } from 'home-assistant-js-websocket';
|
import { HassServiceTarget } from 'home-assistant-js-websocket';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
import { MEDIA_CHUNK_SIZE_DEFAULT, MEDIA_CHUNK_SIZE_MAX } from '../const.js';
|
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 { capabilityKeys } from '../types.js';
|
||||||
import { deepRemoveDefaults } from '../utils/zod.js';
|
import { deepRemoveDefaults } from '../utils/zod.js';
|
||||||
import {
|
import {
|
||||||
@@ -123,12 +113,11 @@ const viewDisplaySchema = z
|
|||||||
export type ViewDisplayConfig = z.infer<typeof viewDisplaySchema>;
|
export type ViewDisplayConfig = z.infer<typeof viewDisplaySchema>;
|
||||||
|
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
// Actions
|
// Stock Actions
|
||||||
//
|
|
||||||
// Declare schemas to existing types:
|
|
||||||
// - https://github.com/colinhacks/zod/issues/372#issuecomment-826380330
|
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
|
|
||||||
|
// Declare schemas for existing types.
|
||||||
|
// See: https://github.com/colinhacks/zod/issues/372#issuecomment-826380330
|
||||||
const schemaForType =
|
const schemaForType =
|
||||||
<T>() =>
|
<T>() =>
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
@@ -152,108 +141,86 @@ const actionBaseSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.optional(),
|
.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.
|
const toggleActionSchema = actionBaseSchema.extend({
|
||||||
// `custom-card-helpers` currently only supports the latter. For maximum
|
action: z.literal('toggle'),
|
||||||
// compatibility, this card supports what HA supports.
|
});
|
||||||
interface ExtendedConfirmationRestrictionConfig {
|
export type ToggleActionConfig = z.infer<typeof toggleActionSchema>;
|
||||||
confirmation?: boolean | ConfirmationRestrictionConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
const toggleActionSchema = schemaForType<
|
|
||||||
ToggleActionConfig & ExtendedConfirmationRestrictionConfig
|
|
||||||
>()(
|
|
||||||
actionBaseSchema.extend({
|
|
||||||
action: z.literal('toggle'),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const targetSchema = schemaForType<HassServiceTarget>()(
|
const targetSchema = schemaForType<HassServiceTarget>()(
|
||||||
z.object({
|
z.object({
|
||||||
entity_id: z.string().optional(),
|
entity_id: z.string().or(z.string().array()).optional(),
|
||||||
device_id: z.string().optional(),
|
device_id: z.string().or(z.string().array()).optional(),
|
||||||
area_id: z.string().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<
|
const performActionActionSchema = actionBaseSchema.extend({
|
||||||
PerformActionActionConfig & ExtendedConfirmationRestrictionConfig
|
action: z.literal('perform-action'),
|
||||||
>()(
|
perform_action: z.string(),
|
||||||
actionBaseSchema.extend({
|
data: z.object({}).passthrough().optional(),
|
||||||
action: z.literal('perform-action'),
|
target: targetSchema.optional(),
|
||||||
perform_action: z.string(),
|
});
|
||||||
data: z.object({}).passthrough().optional(),
|
export type PerformActionActionConfig = z.infer<typeof performActionActionSchema>;
|
||||||
target: targetSchema.optional(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Note: call-service is deprecated and will eventually go away. Please use
|
// Note: call-service is deprecated and will eventually go away. Please use
|
||||||
// perform-action instead.
|
// perform-action instead.
|
||||||
// See: https://www.home-assistant.io/blog/2024/08/07/release-20248/#goodbye-service-calls-hello-actions-
|
// See: https://www.home-assistant.io/blog/2024/08/07/release-20248/#goodbye-service-calls-hello-actions-
|
||||||
const callServiceActionSchema = schemaForType<
|
const callServiceActionSchema = actionBaseSchema.extend({
|
||||||
CallServiceActionConfig & ExtendedConfirmationRestrictionConfig
|
action: z.literal('call-service'),
|
||||||
>()(
|
service: z.string(),
|
||||||
actionBaseSchema.extend({
|
data: z.object({}).passthrough().optional(),
|
||||||
action: z.literal('call-service'),
|
target: targetSchema.optional(),
|
||||||
service: z.string(),
|
});
|
||||||
data: z.object({}).passthrough().optional(),
|
export type CallServiceActionConfig = z.infer<typeof callServiceActionSchema>;
|
||||||
target: targetSchema.optional(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const navigateActionSchema = schemaForType<
|
const navigateActionSchema = actionBaseSchema.extend({
|
||||||
NavigateActionConfig & ExtendedConfirmationRestrictionConfig
|
action: z.literal('navigate'),
|
||||||
>()(
|
navigation_path: z.string(),
|
||||||
actionBaseSchema.extend({
|
navigation_replace: z.boolean().optional(),
|
||||||
action: z.literal('navigate'),
|
});
|
||||||
navigation_path: z.string(),
|
export type NavigateActionConfig = z.infer<typeof navigateActionSchema>;
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const urlActionSchema = schemaForType<
|
const urlActionSchema = actionBaseSchema.extend({
|
||||||
UrlActionConfig & ExtendedConfirmationRestrictionConfig
|
action: z.literal('url'),
|
||||||
>()(
|
url_path: z.string(),
|
||||||
actionBaseSchema.extend({
|
});
|
||||||
action: z.literal('url'),
|
export type URLActionConfig = z.infer<typeof urlActionSchema>;
|
||||||
url_path: z.string(),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const moreInfoActionSchema = schemaForType<
|
const moreInfoActionSchema = actionBaseSchema.extend({
|
||||||
MoreInfoActionConfig & ExtendedConfirmationRestrictionConfig
|
action: z.literal('more-info'),
|
||||||
>()(
|
entity: z.string().optional(),
|
||||||
actionBaseSchema.extend({
|
});
|
||||||
action: z.literal('more-info'),
|
export type MoreInfoActionConfig = z.infer<typeof moreInfoActionSchema>;
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
const customActionSchema = actionBaseSchema
|
const customActionSchema = actionBaseSchema
|
||||||
.extend({
|
.extend({
|
||||||
action: z.literal('fire-dom-event'),
|
action: z.literal('fire-dom-event'),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
export type CustomActionConfig = z.infer<typeof customActionSchema>;
|
||||||
|
|
||||||
const noActionSchema = schemaForType<
|
const noneActionSchema = actionBaseSchema.extend({
|
||||||
NoActionConfig & ExtendedConfirmationRestrictionConfig
|
action: z.literal('none'),
|
||||||
>()(
|
});
|
||||||
actionBaseSchema.extend({
|
export type NoneActionConfig = z.infer<typeof noneActionSchema>;
|
||||||
action: z.literal('none'),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
export const advancedCameraCardCustomActionsBaseSchema = customActionSchema.extend({
|
export const advancedCameraCardCustomActionsBaseSchema = actionBaseSchema.extend({
|
||||||
action: z
|
action: z
|
||||||
.literal('custom:advanced-camera-card-action')
|
.literal('fire-dom-event')
|
||||||
// Syntactic sugar to avoid 'fire-dom-event' as part of an external API.
|
.or(
|
||||||
.transform((): 'fire-dom-event' => 'fire-dom-event')
|
z
|
||||||
.or(z.literal('fire-dom-event')),
|
.literal('custom:advanced-camera-card-action')
|
||||||
|
.transform((): 'fire-dom-event' => '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(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
@@ -374,18 +341,28 @@ const sleepActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend
|
|||||||
});
|
});
|
||||||
export type SleepActionConfig = z.infer<typeof sleepActionConfigSchema>;
|
export type SleepActionConfig = z.infer<typeof sleepActionConfigSchema>;
|
||||||
|
|
||||||
const statusBarActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
|
// For StatusBarActionConfig provide a manual type definition to avoid the `any`
|
||||||
advanced_camera_card_action: z.literal('status_bar'),
|
// that would be created by the lazy() evaluation below.
|
||||||
status_bar_action: z.enum(['add', 'remove', 'reset']),
|
// See: https://zod.dev/?id=recursive-types
|
||||||
|
const statusBarActionConfigSchemaBase = advancedCameraCardCustomActionsBaseSchema.extend(
|
||||||
// This needs to be lazily evaluated since statusBarItemSchema may itself
|
{
|
||||||
// contain actions.
|
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
|
items: z
|
||||||
.lazy(() => statusBarItemSchema)
|
.lazy(() => statusBarItemSchema)
|
||||||
.array()
|
.array()
|
||||||
.optional(),
|
.optional(),
|
||||||
});
|
});
|
||||||
export type StatusBarActionConfig = z.infer<typeof statusBarActionConfigSchema>;
|
|
||||||
|
|
||||||
const LOG_ACTIONS_LEVELS = ['debug', 'info', 'warn', 'error'] as const;
|
const LOG_ACTIONS_LEVELS = ['debug', 'info', 'warn', 'error'] as const;
|
||||||
export type LogActionLevel = (typeof LOG_ACTIONS_LEVELS)[number];
|
export type LogActionLevel = (typeof LOG_ACTIONS_LEVELS)[number];
|
||||||
@@ -397,27 +374,8 @@ const logActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
|
|||||||
});
|
});
|
||||||
export type LogActionConfig = z.infer<typeof logActionConfigSchema>;
|
export type LogActionConfig = z.infer<typeof logActionConfigSchema>;
|
||||||
|
|
||||||
const advancedCameraCardCustomActionSchema = z.union([
|
// An action that can be used internally to call a callback (it is not possible
|
||||||
cameraSelectActionConfigSchema,
|
// for the user to pass this through via the configuration).
|
||||||
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.
|
|
||||||
export const INTERNAL_CALLBACK_ACTION = '__INTERNAL_CALLBACK_ACTION__';
|
export const INTERNAL_CALLBACK_ACTION = '__INTERNAL_CALLBACK_ACTION__';
|
||||||
const internalCallbackActionConfigSchema =
|
const internalCallbackActionConfigSchema =
|
||||||
advancedCameraCardCustomActionsBaseSchema.extend({
|
advancedCameraCardCustomActionsBaseSchema.extend({
|
||||||
@@ -430,57 +388,66 @@ export type InternalCallbackActionConfig = z.infer<
|
|||||||
typeof internalCallbackActionConfigSchema
|
typeof internalCallbackActionConfigSchema
|
||||||
>;
|
>;
|
||||||
|
|
||||||
export const internalAdvancedCameraCardCustomActionSchema =
|
const stockActionSchema = z.union([
|
||||||
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,
|
|
||||||
callServiceActionSchema,
|
callServiceActionSchema,
|
||||||
performActionActionSchema,
|
|
||||||
navigateActionSchema,
|
|
||||||
urlActionSchema,
|
|
||||||
moreInfoActionSchema,
|
|
||||||
noActionSchema,
|
|
||||||
customActionSchema,
|
customActionSchema,
|
||||||
advancedCameraCardCustomActionSchema,
|
moreInfoActionSchema,
|
||||||
|
navigateActionSchema,
|
||||||
|
noneActionSchema,
|
||||||
|
performActionActionSchema,
|
||||||
|
toggleActionSchema,
|
||||||
|
urlActionSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const internalActionSchema = actionSchema.or(internalCallbackActionConfigSchema);
|
const advancedCameraCardCustomActionSchema = z.union([
|
||||||
export type ActionType = z.infer<typeof internalActionSchema>;
|
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
|
const actionsBaseSchema = z
|
||||||
.object({
|
.object({
|
||||||
tap_action: actionSchema.or(actionSchema.array()).optional(),
|
tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
|
||||||
hold_action: actionSchema.or(actionSchema.array()).optional(),
|
hold_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
|
||||||
double_tap_action: actionSchema.or(actionSchema.array()).optional(),
|
double_tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
|
||||||
start_tap_action: actionSchema.or(actionSchema.array()).optional(),
|
start_tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
|
||||||
end_tap_action: actionSchema.or(actionSchema.array()).optional(),
|
end_tap_action: actionConfigSchema.or(actionConfigSchema.array()).optional(),
|
||||||
})
|
})
|
||||||
// Passthrough to allow (at least) entity/camera_image to go through. This
|
// Passthrough to allow (at least) entity/camera_image to go through. This
|
||||||
// card doesn't need these attributes, but handleAction() in
|
// card doesn't need these attributes, but handleAction() in
|
||||||
// custom_card_helpers may depending on how the action is configured.
|
// custom_card_helpers may depending on how the action is configured.
|
||||||
.passthrough();
|
.passthrough();
|
||||||
export type Actions = z.infer<typeof actionsBaseSchema>;
|
export type Actions = z.infer<typeof actionsBaseSchema>;
|
||||||
|
export type ActionsConfig = Actions & AuxillaryActionConfig;
|
||||||
export type ActionsConfig = Actions & {
|
|
||||||
camera_image?: string;
|
|
||||||
entity?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const actionsSchema = z.object({
|
const actionsSchema = z.object({
|
||||||
actions: actionsBaseSchema.optional(),
|
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
|
// Picture Elements
|
||||||
//
|
//
|
||||||
@@ -489,6 +456,11 @@ const elementsBaseSchema = actionsBaseSchema.extend({
|
|||||||
// display up-front regardless of where they made their error.
|
// 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
|
// https://www.home-assistant.io/lovelace/picture-elements/#state-badge
|
||||||
const stateBadgeIconSchema = elementsBaseSchema.extend({
|
const stateBadgeIconSchema = elementsBaseSchema.extend({
|
||||||
type: z.literal('state-badge'),
|
type: z.literal('state-badge'),
|
||||||
@@ -2009,7 +1981,7 @@ export type Overrides = z.infer<typeof overridesSchema>;
|
|||||||
// Automation Configuration
|
// Automation Configuration
|
||||||
// *************************************************************************
|
// *************************************************************************
|
||||||
|
|
||||||
const automationActionSchema = actionSchema.array();
|
const automationActionSchema = actionConfigSchema.array();
|
||||||
export type AutomationActions = z.infer<typeof automationActionSchema>;
|
export type AutomationActions = z.infer<typeof automationActionSchema>;
|
||||||
|
|
||||||
const automationSchema = z
|
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,
|
HassServiceTarget,
|
||||||
MessageBase,
|
MessageBase,
|
||||||
} from 'home-assistant-js-websocket';
|
} 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 {
|
declare global {
|
||||||
interface HASSDomEvents {
|
interface HASSDomEvents {
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"actions": {
|
||||||
|
"abort": "",
|
||||||
|
"confirmation": ""
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"advanced_camera_card": "",
|
"advanced_camera_card": "",
|
||||||
"advanced_camera_card_description": "",
|
"advanced_camera_card_description": "",
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"actions": {
|
||||||
|
"abort": "Aborted action",
|
||||||
|
"confirmation": "Are you sure you want to perform this action"
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"advanced_camera_card": "Advanced Camera Card",
|
"advanced_camera_card": "Advanced Camera Card",
|
||||||
"advanced_camera_card_description": "An Advanced Camera Card",
|
"advanced_camera_card_description": "An Advanced Camera Card",
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"actions": {
|
||||||
|
"abort": "",
|
||||||
|
"confirmation": ""
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"advanced_camera_card": "",
|
"advanced_camera_card": "",
|
||||||
"advanced_camera_card_description": "",
|
"advanced_camera_card_description": "",
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"actions": {
|
||||||
|
"abort": "",
|
||||||
|
"confirmation": ""
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"advanced_camera_card": "",
|
"advanced_camera_card": "",
|
||||||
"advanced_camera_card_description": "",
|
"advanced_camera_card_description": "",
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"actions": {
|
||||||
|
"abort": "",
|
||||||
|
"confirmation": ""
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"advanced_camera_card": "",
|
"advanced_camera_card": "",
|
||||||
"advanced_camera_card_description": "",
|
"advanced_camera_card_description": "",
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
{
|
{
|
||||||
|
"actions": {
|
||||||
|
"abort": "",
|
||||||
|
"confirmation": ""
|
||||||
|
},
|
||||||
"common": {
|
"common": {
|
||||||
"advanced_camera_card": "",
|
"advanced_camera_card": "",
|
||||||
"advanced_camera_card_description": "",
|
"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 { ZoomSettingsBase } from '../components-lib/zoom/types.js';
|
||||||
import { PTZAction } from '../config/ptz.js';
|
import { PTZAction } from '../config/ptz.js';
|
||||||
import {
|
import {
|
||||||
|
ActionConfig,
|
||||||
ActionPhase,
|
ActionPhase,
|
||||||
ActionType,
|
|
||||||
ActionsConfig,
|
ActionsConfig,
|
||||||
|
AdvancedCameraCardCustomActionConfig,
|
||||||
AdvancedCameraCardGeneralAction,
|
AdvancedCameraCardGeneralAction,
|
||||||
AdvancedCameraCardUserSpecifiedView,
|
AdvancedCameraCardUserSpecifiedView,
|
||||||
CameraSelectActionConfig,
|
CameraSelectActionConfig,
|
||||||
DisplayModeActionConfig,
|
DisplayModeActionConfig,
|
||||||
GeneralActionConfig,
|
GeneralActionConfig,
|
||||||
INTERNAL_CALLBACK_ACTION,
|
INTERNAL_CALLBACK_ACTION,
|
||||||
InternalAdvancedCameraCardCustomAction,
|
|
||||||
InternalCallbackActionConfig,
|
InternalCallbackActionConfig,
|
||||||
LogActionConfig,
|
LogActionConfig,
|
||||||
LogActionLevel,
|
LogActionLevel,
|
||||||
MediaPlayerActionConfig,
|
MediaPlayerActionConfig,
|
||||||
|
PerformActionActionConfig,
|
||||||
PTZActionConfig,
|
PTZActionConfig,
|
||||||
PTZControlsActionConfig,
|
PTZControlsActionConfig,
|
||||||
PTZDigitialActionConfig,
|
PTZDigitialActionConfig,
|
||||||
PTZMultiActionConfig,
|
PTZMultiActionConfig,
|
||||||
SubstreamSelectActionConfig,
|
SubstreamSelectActionConfig,
|
||||||
ViewActionConfig,
|
ViewActionConfig,
|
||||||
internalAdvancedCameraCardCustomActionSchema,
|
|
||||||
} from '../config/types.js';
|
} from '../config/types.js';
|
||||||
import { hasAction as customCardHasAction } from '../ha/has-action.js';
|
import { ServiceCallRequest } from '../ha/types.js';
|
||||||
import { ActionConfig, ServiceCallRequest } from '../ha/types.js';
|
|
||||||
import { arrayify } from './basic.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(
|
export function createGeneralAction(
|
||||||
action: AdvancedCameraCardGeneralAction,
|
action: AdvancedCameraCardGeneralAction,
|
||||||
options?: {
|
options?: {
|
||||||
@@ -221,7 +203,7 @@ export function createPerformAction(
|
|||||||
data?: ServiceCallRequest['serviceData'];
|
data?: ServiceCallRequest['serviceData'];
|
||||||
target?: ServiceCallRequest['target'];
|
target?: ServiceCallRequest['target'];
|
||||||
},
|
},
|
||||||
): ActionType {
|
): PerformActionActionConfig {
|
||||||
return {
|
return {
|
||||||
action: 'perform-action' as const,
|
action: 'perform-action' as const,
|
||||||
perform_action: perform_action,
|
perform_action: perform_action,
|
||||||
@@ -240,7 +222,7 @@ export function createPerformAction(
|
|||||||
export function getActionConfigGivenAction(
|
export function getActionConfigGivenAction(
|
||||||
interaction?: string,
|
interaction?: string,
|
||||||
config?: ActionsConfig | null,
|
config?: ActionsConfig | null,
|
||||||
): ActionType | ActionType[] | null {
|
): ActionConfig | ActionConfig[] | null {
|
||||||
if (!interaction || !config) {
|
if (!interaction || !config) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -270,11 +252,17 @@ export function getActionConfigGivenAction(
|
|||||||
* @param config The action config in question.
|
* @param config The action config in question.
|
||||||
* @returns `true` if there's a real action defined, `false` otherwise.
|
* @returns `true` if there's a real action defined, `false` otherwise.
|
||||||
*/
|
*/
|
||||||
export const hasAction = (config?: ActionType | ActionType[]): boolean => {
|
export const hasAction = (config?: ActionConfig | ActionConfig[]): boolean => {
|
||||||
// See note above on 'ActionConfig vs ActionType' for why this cast is
|
return arrayify(config).some((item) => item.action !== 'none');
|
||||||
// necessary and harmless.
|
};
|
||||||
return arrayify(config).some((item) =>
|
|
||||||
customCardHasAction(item as ActionConfig | undefined),
|
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).
|
* @param value: A value (which may be an array).
|
||||||
* @returns An array.
|
* @returns An array.
|
||||||
*/
|
*/
|
||||||
export const arrayify = <T>(value: T | T[]): T[] => {
|
export const arrayify = <T>(value?: T | T[]): T[] => {
|
||||||
return Array.isArray(value) ? value : [value];
|
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 { mock } from 'vitest-mock-extended';
|
||||||
import {
|
import {
|
||||||
ActionsManager,
|
ActionsManager,
|
||||||
@@ -8,13 +17,7 @@ import {
|
|||||||
import { TemplateRenderer } from '../../../src/card-controller/templates';
|
import { TemplateRenderer } from '../../../src/card-controller/templates';
|
||||||
import { AdvancedCameraCardView } from '../../../src/config/types';
|
import { AdvancedCameraCardView } from '../../../src/config/types';
|
||||||
import { createLogAction } from '../../../src/utils/action';
|
import { createLogAction } from '../../../src/utils/action';
|
||||||
import {
|
import { createCardAPI, createConfig, createHASS, createView } from '../../test-utils';
|
||||||
createAction,
|
|
||||||
createCardAPI,
|
|
||||||
createConfig,
|
|
||||||
createHASS,
|
|
||||||
createView,
|
|
||||||
} from '../../test-utils';
|
|
||||||
|
|
||||||
describe('ActionsManager', () => {
|
describe('ActionsManager', () => {
|
||||||
describe('getMergedActions', () => {
|
describe('getMergedActions', () => {
|
||||||
@@ -138,7 +141,7 @@ describe('ActionsManager', () => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle interaction', () => {
|
it('should handle interaction', async () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const element = document.createElement('div');
|
const element = document.createElement('div');
|
||||||
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||||
@@ -158,7 +161,7 @@ describe('ActionsManager', () => {
|
|||||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
|
|
||||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||||
manager.handleInteractionEvent(
|
await manager.handleInteractionEvent(
|
||||||
new CustomEvent<Interaction>('event', { detail: { action: 'tap' } }),
|
new CustomEvent<Interaction>('event', { detail: { action: 'tap' } }),
|
||||||
);
|
);
|
||||||
expect(consoleSpy).toBeCalled();
|
expect(consoleSpy).toBeCalled();
|
||||||
@@ -203,7 +206,7 @@ describe('ActionsManager', () => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle event', () => {
|
it('should handle event', async () => {
|
||||||
const action = createLogAction('Hello, world!');
|
const action = createLogAction('Hello, world!');
|
||||||
const event = new CustomEvent('ll-custom', {
|
const event = new CustomEvent('ll-custom', {
|
||||||
detail: action,
|
detail: action,
|
||||||
@@ -213,15 +216,15 @@ describe('ActionsManager', () => {
|
|||||||
const manager = new ActionsManager(api);
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||||
manager.handleCustomActionEvent(event);
|
await manager.handleCustomActionEvent(event);
|
||||||
expect(consoleSpy).toBeCalled();
|
expect(consoleSpy).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not handle event without detail', () => {
|
it('should not handle event without detail', async () => {
|
||||||
const manager = new ActionsManager(createCardAPI());
|
const manager = new ActionsManager(createCardAPI());
|
||||||
|
|
||||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
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();
|
expect(consoleSpy).not.toBeCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -250,6 +253,77 @@ describe('ActionsManager', () => {
|
|||||||
await manager.executeActions(createLogAction('Hello, world!'));
|
await manager.executeActions(createLogAction('Hello, world!'));
|
||||||
expect(consoleSpy).toBeCalled();
|
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', () => {
|
describe('uninitialize', () => {
|
||||||
@@ -266,18 +340,18 @@ describe('ActionsManager', () => {
|
|||||||
|
|
||||||
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
const consoleSpy = vi.spyOn(global.console, 'info').mockReturnValue(undefined);
|
||||||
const promise = manager.executeActions([
|
const promise = manager.executeActions([
|
||||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
{
|
||||||
createAction({
|
action: 'fire-dom-event',
|
||||||
advanced_camera_card_action: 'sleep',
|
advanced_camera_card_action: 'sleep',
|
||||||
duration: {
|
duration: {
|
||||||
m: 1,
|
m: 1,
|
||||||
},
|
},
|
||||||
})!,
|
},
|
||||||
createLogAction('Hello, world!'),
|
createLogAction('Hello, world!'),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Stop inflight actions.
|
// Stop inflight actions.
|
||||||
manager.uninitialize();
|
await manager.uninitialize();
|
||||||
|
|
||||||
// Advance timers (causes the sleep to end).
|
// Advance timers (causes the sleep to end).
|
||||||
vi.runOnlyPendingTimers();
|
vi.runOnlyPendingTimers();
|
||||||
@@ -288,34 +362,4 @@ describe('ActionsManager', () => {
|
|||||||
expect(consoleSpy).not.toBeCalled();
|
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 { 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 () => {
|
describe('should handle base action', () => {
|
||||||
const api = createCardAPI();
|
beforeEach(() => {
|
||||||
const action = new BaseAction(
|
vi.clearAllMocks();
|
||||||
{},
|
});
|
||||||
{
|
|
||||||
action: 'fire-dom-event',
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
await action.execute(api);
|
beforeAll(() => {
|
||||||
await action.stop();
|
vi.stubGlobal('confirm', vi.fn());
|
||||||
|
});
|
||||||
|
|
||||||
// These methods have no observable effect on the base class, so this test is
|
afterAll(() => {
|
||||||
// currently only providing coverage and proof of no exceptions!
|
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 { 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 { CameraSelectAction } from '../../../src/card-controller/actions/actions/camera-select';
|
||||||
import { CameraUIAction } from '../../../src/card-controller/actions/actions/camera-ui';
|
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 { DefaultAction } from '../../../src/card-controller/actions/actions/default';
|
||||||
import { DisplayModeSelectAction } from '../../../src/card-controller/actions/actions/display-mode-select';
|
import { DisplayModeSelectAction } from '../../../src/card-controller/actions/actions/display-mode-select';
|
||||||
import { DownloadAction } from '../../../src/card-controller/actions/actions/download';
|
import { DownloadAction } from '../../../src/card-controller/actions/actions/download';
|
||||||
import { ExpandAction } from '../../../src/card-controller/actions/actions/expand';
|
import { ExpandAction } from '../../../src/card-controller/actions/actions/expand';
|
||||||
import { FullscreenAction } from '../../../src/card-controller/actions/actions/fullscreen';
|
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 { InternalCallbackAction } from '../../../src/card-controller/actions/actions/internal-callback';
|
||||||
import { LogAction } from '../../../src/card-controller/actions/actions/log';
|
import { LogAction } from '../../../src/card-controller/actions/actions/log';
|
||||||
import { MediaPlayerAction } from '../../../src/card-controller/actions/actions/media-player';
|
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 { MicrophoneDisconnectAction } from '../../../src/card-controller/actions/actions/microphone-disconnect';
|
||||||
import { MicrophoneMuteAction } from '../../../src/card-controller/actions/actions/microphone-mute';
|
import { MicrophoneMuteAction } from '../../../src/card-controller/actions/actions/microphone-mute';
|
||||||
import { MicrophoneUnmuteAction } from '../../../src/card-controller/actions/actions/microphone-unmute';
|
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 { 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 { 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 { PlayAction } from '../../../src/card-controller/actions/actions/play';
|
||||||
import { PTZAction } from '../../../src/card-controller/actions/actions/ptz';
|
import { PTZAction } from '../../../src/card-controller/actions/actions/ptz';
|
||||||
import { PTZControlsAction } from '../../../src/card-controller/actions/actions/ptz-controls';
|
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 { SubstreamOffAction } from '../../../src/card-controller/actions/actions/substream-off';
|
||||||
import { SubstreamOnAction } from '../../../src/card-controller/actions/actions/substream-on';
|
import { SubstreamOnAction } from '../../../src/card-controller/actions/actions/substream-on';
|
||||||
import { SubstreamSelectAction } from '../../../src/card-controller/actions/actions/substream-select';
|
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 { 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 { ViewAction } from '../../../src/card-controller/actions/actions/view';
|
||||||
import { ActionFactory } from '../../../src/card-controller/actions/factory';
|
import { ActionFactory } from '../../../src/card-controller/actions/factory';
|
||||||
import {
|
import { ActionConfig, INTERNAL_CALLBACK_ACTION } from '../../../src/config/types';
|
||||||
AdvancedCameraCardCustomAction,
|
|
||||||
INTERNAL_CALLBACK_ACTION,
|
|
||||||
} from '../../../src/config/types';
|
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
describe('ActionFactory', () => {
|
describe('ActionFactory', () => {
|
||||||
@@ -55,23 +59,26 @@ describe('ActionFactory', () => {
|
|||||||
).toBeNull();
|
).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('generic', () => {
|
describe('stock actions', () => {
|
||||||
it('non advanced camera card action', () => {
|
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();
|
const factory = new ActionFactory();
|
||||||
expect(factory.createAction({}, { action: 'fire-dom-event' })).toBeInstanceOf(
|
expect(factory.createAction({}, action)).toBeInstanceOf(classObject);
|
||||||
GenericAction,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('non fire-dom-event', () => {
|
|
||||||
const factory = new ActionFactory();
|
|
||||||
expect(factory.createAction({}, { action: 'more-info' })).toBeInstanceOf(
|
|
||||||
GenericAction,
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('actions', () => {
|
describe('custom actions', () => {
|
||||||
it.each([
|
it.each([
|
||||||
[{ advanced_camera_card_action: 'camera_select' as const }, CameraSelectAction],
|
[{ advanced_camera_card_action: 'camera_select' as const }, CameraSelectAction],
|
||||||
[{ advanced_camera_card_action: 'camera_ui' as const }, CameraUIAction],
|
[{ advanced_camera_card_action: 'camera_ui' as const }, CameraUIAction],
|
||||||
@@ -178,10 +185,10 @@ describe('ActionFactory', () => {
|
|||||||
],
|
],
|
||||||
])(
|
])(
|
||||||
'advanced_camera_card_action: $advanced_camera_card_action',
|
'advanced_camera_card_action: $advanced_camera_card_action',
|
||||||
(action: Partial<AdvancedCameraCardCustomAction>, classObject: object) => {
|
(action: Partial<ActionConfig>, classObject: object) => {
|
||||||
const factory = new ActionFactory();
|
const factory = new ActionFactory();
|
||||||
expect(
|
expect(
|
||||||
factory.createAction({}, { action: 'fire-dom-event', ...action }),
|
factory.createAction({}, { ...action, action: 'fire-dom-event' }),
|
||||||
).toBeInstanceOf(classObject);
|
).toBeInstanceOf(classObject);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
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 { AutomationsManager } from '../../src/card-controller/automations-manager.js';
|
||||||
import { ConditionStateManager } from '../../src/conditions/state-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';
|
import { createCardAPI } from '../test-utils.js';
|
||||||
|
|
||||||
describe('AutomationsManager', () => {
|
describe('AutomationsManager', () => {
|
||||||
@@ -151,9 +150,9 @@ describe('AutomationsManager', () => {
|
|||||||
vi.mocked(api.getActionsManager().executeActions).mockImplementation(
|
vi.mocked(api.getActionsManager().executeActions).mockImplementation(
|
||||||
async (
|
async (
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
_action: ActionType | ActionType[],
|
_action: ActionConfig | ActionConfig[],
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
_config?: AuxillaryActionConfig,
|
_options?: unknown,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
fullscreen = !fullscreen;
|
fullscreen = !fullscreen;
|
||||||
stateManager.setState({ fullscreen: fullscreen });
|
stateManager.setState({ fullscreen: fullscreen });
|
||||||
|
|||||||
@@ -14,7 +14,6 @@ import {
|
|||||||
describe('CardElementManager', () => {
|
describe('CardElementManager', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.unstubAllGlobals();
|
vi.unstubAllGlobals();
|
||||||
global.window.location = mock<Location>();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should get element', () => {
|
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 { mock } from 'vitest-mock-extended';
|
||||||
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
|
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
|
||||||
import { SubstreamSelectViewModifier } from '../../src/card-controller/view/modifiers/substream-select';
|
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 setQueryString = (qs: string): void => {
|
||||||
const location: Location = mock<Location>();
|
const location: Location = mock<Location>();
|
||||||
location.search = qs;
|
location.search = qs;
|
||||||
global.window.location = location;
|
|
||||||
|
vi.spyOn(window, 'location', 'get').mockReturnValue(location);
|
||||||
};
|
};
|
||||||
|
|
||||||
// @vitest-environment jsdom
|
// @vitest-environment jsdom
|
||||||
describe('QueryStringManager', () => {
|
describe('QueryStringManager', () => {
|
||||||
beforeEach(() => {
|
afterEach(() => {
|
||||||
global.window.location = mock<Location>();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reject malformed query string', async () => {
|
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 { MenuController } from '../../src/components-lib/menu-controller.js';
|
||||||
import { SubmenuItem } from '../../src/components/submenu/types.js';
|
import { SubmenuItem } from '../../src/components/submenu/types.js';
|
||||||
import { MenuConfig, menuConfigSchema } from '../../src/config/types.js';
|
import { MenuConfig, menuConfigSchema } from '../../src/config/types.js';
|
||||||
import { handleActionConfig } from '../../src/ha/handle-action.js';
|
|
||||||
import {
|
import {
|
||||||
createInteractionActionEvent,
|
createInteractionActionEvent,
|
||||||
createLitElement,
|
createLitElement,
|
||||||
createSubmenuInteractionActionEvent,
|
createSubmenuInteractionActionEvent,
|
||||||
} from '../test-utils';
|
} from '../test-utils';
|
||||||
|
|
||||||
vi.mock('../../src/ha/handle-action.js');
|
|
||||||
vi.mock('../../src/utils/ha');
|
|
||||||
|
|
||||||
const createMenuConfig = (config: unknown): MenuConfig => {
|
const createMenuConfig = (config: unknown): MenuConfig => {
|
||||||
return menuConfigSchema.parse(config);
|
return menuConfigSchema.parse(config);
|
||||||
};
|
};
|
||||||
@@ -369,9 +365,13 @@ describe('MenuController', () => {
|
|||||||
|
|
||||||
describe('should handle actions', () => {
|
describe('should handle actions', () => {
|
||||||
it('should bail without config', () => {
|
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'));
|
controller.handleAction(createInteractionActionEvent('tap'));
|
||||||
expect(vi.mocked(handleActionConfig)).not.toBeCalled();
|
expect(handler).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should execute simple action in non-hidden menu', () => {
|
it('should execute simple action in non-hidden menu', () => {
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import {
|
import {
|
||||||
|
advancedCameraCardConditionSchema,
|
||||||
|
advancedCameraCardCustomActionsBaseSchema,
|
||||||
cameraConfigSchema,
|
cameraConfigSchema,
|
||||||
conditionalSchema,
|
conditionalSchema,
|
||||||
customSchema,
|
customSchema,
|
||||||
dimensionsConfigSchema,
|
dimensionsConfigSchema,
|
||||||
advancedCameraCardConditionSchema,
|
statusBarActionConfigSchema,
|
||||||
advancedCameraCardCustomActionsBaseSchema,
|
|
||||||
internalAdvancedCameraCardCustomActionSchema,
|
|
||||||
} from '../../src/config/types';
|
} from '../../src/config/types';
|
||||||
import { createConfig } from '../test-utils';
|
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 {
|
import {
|
||||||
AdvancedCameraCardConfig,
|
AdvancedCameraCardConfig,
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
InternalAdvancedCameraCardCustomAction,
|
|
||||||
PerformanceConfig,
|
PerformanceConfig,
|
||||||
RawAdvancedCameraCardConfig,
|
RawAdvancedCameraCardConfig,
|
||||||
advancedCameraCardConfigSchema,
|
advancedCameraCardConfigSchema,
|
||||||
cameraConfigSchema,
|
cameraConfigSchema,
|
||||||
internalAdvancedCameraCardCustomActionSchema,
|
|
||||||
performanceConfigSchema,
|
performanceConfigSchema,
|
||||||
} from '../src/config/types';
|
} from '../src/config/types';
|
||||||
import { CurrentUser, HomeAssistant } from '../src/ha/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 { MediaQueriesResults } from '../src/view/media-queries-results';
|
||||||
import { View, ViewParameters } from '../src/view/view';
|
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 => {
|
export const createCameraConfig = (config?: unknown): CameraConfig => {
|
||||||
return cameraConfigSchema.parse(config ?? {});
|
return cameraConfigSchema.parse(config ?? {});
|
||||||
};
|
};
|
||||||
|
|||||||
+19
-40
@@ -1,9 +1,7 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
import { mock } from 'vitest-mock-extended';
|
import { mock } from 'vitest-mock-extended';
|
||||||
import { actionSchema, INTERNAL_CALLBACK_ACTION } from '../../src/config/types.js';
|
import { ActionConfig, INTERNAL_CALLBACK_ACTION } from '../../src/config/types.js';
|
||||||
import { hasAction as customCardHasAction } from '../../src/ha/has-action.js';
|
|
||||||
import {
|
import {
|
||||||
convertActionToCardCustomAction,
|
|
||||||
createCameraAction,
|
createCameraAction,
|
||||||
createDisplayModeAction,
|
createDisplayModeAction,
|
||||||
createGeneralAction,
|
createGeneralAction,
|
||||||
@@ -21,30 +19,6 @@ import {
|
|||||||
stopEventFromActivatingCardWideActions,
|
stopEventFromActivatingCardWideActions,
|
||||||
} from '../../src/utils/action.js';
|
} 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', () => {
|
describe('createGeneralAction', () => {
|
||||||
it('should create general action', () => {
|
it('should create general action', () => {
|
||||||
expect(
|
expect(
|
||||||
@@ -293,10 +267,7 @@ describe('createPerformAction', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('getActionConfigGivenAction', () => {
|
describe('getActionConfigGivenAction', () => {
|
||||||
const action = actionSchema.parse({
|
const action = createViewAction('clips');
|
||||||
action: 'fire-dom-event',
|
|
||||||
advanced_camera_card_action: 'clips',
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not handle undefined arguments', () => {
|
it('should not handle undefined arguments', () => {
|
||||||
expect(getActionConfigGivenAction()).toBeNull();
|
expect(getActionConfigGivenAction()).toBeNull();
|
||||||
@@ -348,21 +319,29 @@ describe('getActionConfigGivenAction', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('hasAction', () => {
|
describe('hasAction', () => {
|
||||||
const action = actionSchema.parse({
|
const realAction = createViewAction('clips');
|
||||||
action: 'toggle',
|
const noneAction: ActionConfig = {
|
||||||
});
|
action: 'none',
|
||||||
|
};
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle non-array case', () => {
|
it('should return true for real action', () => {
|
||||||
expect(hasAction(action)).toBeFalsy();
|
expect(hasAction(realAction)).toBeTruthy();
|
||||||
expect(customCardHasAction).toBeCalledTimes(1);
|
|
||||||
});
|
});
|
||||||
it('should handle array case', () => {
|
|
||||||
expect(hasAction([action, action, action])).toBeFalsy();
|
it('should return false for none action', () => {
|
||||||
expect(customCardHasAction).toBeCalledTimes(3);
|
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];
|
const data = [1, 2, 3];
|
||||||
expect(arrayify(data)).toBe(data);
|
expect(arrayify(data)).toBe(data);
|
||||||
});
|
});
|
||||||
|
it('should handle undefined', () => {
|
||||||
|
expect(arrayify()).toEqual([]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('setify', () => {
|
describe('setify', () => {
|
||||||
|
|||||||
@@ -18,13 +18,13 @@ const media = new ViewMedia('clip', 'camera.office');
|
|||||||
describe('downloadURL', () => {
|
describe('downloadURL', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
global.window.location = mock<Location>();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should download same origin via link', () => {
|
it('should download same origin via link', () => {
|
||||||
const location: Location & { origin: string } = mock<Location>();
|
const location: Location & { origin: string } = mock<Location>();
|
||||||
location.origin = 'http://foo';
|
location.origin = 'http://foo';
|
||||||
global.window.location = location;
|
|
||||||
|
vi.spyOn(window, 'location', 'get').mockReturnValue(location);
|
||||||
|
|
||||||
const link = document.createElement('a');
|
const link = document.createElement('a');
|
||||||
link.click = vi.fn();
|
link.click = vi.fn();
|
||||||
@@ -55,7 +55,8 @@ describe('downloadURL', () => {
|
|||||||
// Set the origin to the same.
|
// Set the origin to the same.
|
||||||
const location: Location & { origin: string } = mock<Location>();
|
const location: Location & { origin: string } = mock<Location>();
|
||||||
location.origin = 'http://foo';
|
location.origin = 'http://foo';
|
||||||
global.window.location = location;
|
|
||||||
|
vi.spyOn(window, 'location', 'get').mockReturnValue(location);
|
||||||
|
|
||||||
const windowSpy = vi.spyOn(window, 'open').mockReturnValue(null);
|
const windowSpy = vi.spyOn(window, 'open').mockReturnValue(null);
|
||||||
|
|
||||||
@@ -66,8 +67,13 @@ describe('downloadURL', () => {
|
|||||||
|
|
||||||
describe('downloadMedia', () => {
|
describe('downloadMedia', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
vi.spyOn(window, 'location', 'get').mockReturnValue(
|
||||||
|
mock<Location>({ origin: 'https://foo' }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
global.window.location = mock<Location>({ origin: 'https://foo' });
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error when no media', () => {
|
it('should throw error when no media', () => {
|
||||||
|
|||||||
+1
-1
@@ -5,7 +5,7 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"lib": ["es2021", "dom", "dom.iterable"],
|
"lib": ["es2021", "dom", "dom.iterable"],
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"noErrorTruncation": true,
|
"noErrorTruncation": false,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ const FULL_COVERAGE_FILES_RELATIVE = [
|
|||||||
'conditions/**/*.ts',
|
'conditions/**/*.ts',
|
||||||
'config/**/*.ts',
|
'config/**/*.ts',
|
||||||
'const.ts',
|
'const.ts',
|
||||||
|
'ha/**/*.ts',
|
||||||
'types.ts',
|
'types.ts',
|
||||||
'utils/action.ts',
|
'utils/action.ts',
|
||||||
'utils/audio.ts',
|
'utils/audio.ts',
|
||||||
|
|||||||
Reference in New Issue
Block a user