@@ -8,7 +8,10 @@ import {
|
|||||||
AuxillaryActionConfig,
|
AuxillaryActionConfig,
|
||||||
} from '../../config/schema/actions/types.js';
|
} from '../../config/schema/actions/types.js';
|
||||||
import { forwardHaptic } from '../../ha/haptic.js';
|
import { forwardHaptic } from '../../ha/haptic.js';
|
||||||
import { getActionConfigGivenAction } from '../../utils/action.js';
|
import {
|
||||||
|
getActionConfigGivenAction,
|
||||||
|
isAdvancedCameraCardCustomAction,
|
||||||
|
} from '../../utils/action.js';
|
||||||
import { allPromises } from '../../utils/basic.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';
|
||||||
@@ -101,7 +104,16 @@ export class ActionsManager {
|
|||||||
// 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;
|
||||||
}
|
}
|
||||||
await this.executeActions(ev.detail as ActionConfig);
|
const action: ActionConfig = ev.detail;
|
||||||
|
|
||||||
|
// If the received action is not a custom action specifically for this card
|
||||||
|
// to handle, ignore it. Otherwise, we can get action "loops". See:
|
||||||
|
// https://github.com/dermotduffy/advanced-camera-card/issues/1969
|
||||||
|
if (!isAdvancedCameraCardCustomAction(action)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.executeActions(action);
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export class PTZDigitalAction extends AdvancedCameraCardAction<PTZDigitialAction
|
|||||||
|
|
||||||
/* istanbul ignore else: the else path cannot be reached -- @preserve */
|
/* istanbul ignore else: the else path cannot be reached -- @preserve */
|
||||||
if (this._action.ptz_phase === 'start') {
|
if (this._action.ptz_phase === 'start') {
|
||||||
stopInProgressForThisTarget(targetID, this._context.ptzDigital);
|
await stopInProgressForThisTarget(targetID, this._context.ptzDigital);
|
||||||
setInProgressForThisTarget(targetID, this._context, 'ptzDigital', this);
|
setInProgressForThisTarget(targetID, this._context, 'ptzDigital', this);
|
||||||
|
|
||||||
await this._stepChange(api, targetID);
|
await this._stepChange(api, targetID);
|
||||||
@@ -75,7 +75,7 @@ export class PTZDigitalAction extends AdvancedCameraCardAction<PTZDigitialAction
|
|||||||
this._stepChange(api, targetID),
|
this._stepChange(api, targetID),
|
||||||
);
|
);
|
||||||
} else if (this._action.ptz_phase === 'stop') {
|
} else if (this._action.ptz_phase === 'stop') {
|
||||||
stopInProgressForThisTarget(targetID, this._context.ptzDigital);
|
await stopInProgressForThisTarget(targetID, this._context.ptzDigital);
|
||||||
delete this._context.ptzDigital?.[targetID];
|
delete this._context.ptzDigital?.[targetID];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,8 +22,10 @@ declare module 'action' {
|
|||||||
|
|
||||||
export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
||||||
protected _timer = new Timer();
|
protected _timer = new Timer();
|
||||||
|
protected _stopped = false;
|
||||||
|
|
||||||
public async stop(): Promise<void> {
|
public async stop(): Promise<void> {
|
||||||
|
this._stopped = true;
|
||||||
this._timer.stop();
|
this._timer.stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +81,7 @@ export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
|||||||
|
|
||||||
if (this._action.ptz_phase === 'start') {
|
if (this._action.ptz_phase === 'start') {
|
||||||
// Scenario: Asked to start a continuous move, camera only supports relative moves natively.
|
// Scenario: Asked to start a continuous move, camera only supports relative moves natively.
|
||||||
stopInProgressForThisTarget(ptzCameraID, this._context.ptz);
|
await stopInProgressForThisTarget(ptzCameraID, this._context.ptz);
|
||||||
setInProgressForThisTarget(ptzCameraID, this._context, 'ptz', this);
|
setInProgressForThisTarget(ptzCameraID, this._context, 'ptz', this);
|
||||||
|
|
||||||
const singleStep = async (): Promise<void> => {
|
const singleStep = async (): Promise<void> => {
|
||||||
@@ -89,15 +91,26 @@ export class PTZAction extends AdvancedCameraCardAction<PTZActionConfig> {
|
|||||||
.executePTZAction(ptzCameraID, this._action.ptz_action, {
|
.executePTZAction(ptzCameraID, this._action.ptz_action, {
|
||||||
preset: this._action.ptz_preset,
|
preset: this._action.ptz_preset,
|
||||||
}));
|
}));
|
||||||
// Only start the timer for the next step after this step returns.
|
|
||||||
this._timer.start(ptzConfiguration.r2c_delay_between_calls_seconds, singleStep);
|
if (!this._stopped) {
|
||||||
|
// Only start the timer for the next step after this step returns, and
|
||||||
|
// only if this action has not been stopped.
|
||||||
|
// See: https://github.com/dermotduffy/advanced-camera-card/issues/1967
|
||||||
|
this._timer.start(
|
||||||
|
ptzConfiguration.r2c_delay_between_calls_seconds,
|
||||||
|
singleStep,
|
||||||
|
);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
this._stopped = false;
|
||||||
await singleStep();
|
await singleStep();
|
||||||
} else if (this._action.ptz_phase === 'stop') {
|
} else if (this._action.ptz_phase === 'stop') {
|
||||||
// Scenario: Asked to stop continuous move, camera only supports relative moves natively.
|
// Scenario: Asked to stop continuous move, camera only supports relative moves natively.
|
||||||
stopInProgressForThisTarget(ptzCameraID, this._context.ptz);
|
await stopInProgressForThisTarget(ptzCameraID, this._context.ptz);
|
||||||
} else {
|
} else {
|
||||||
|
this._stopped = false;
|
||||||
|
|
||||||
// Relative move (but camera only supports continuous).
|
// Relative move (but camera only supports continuous).
|
||||||
await api
|
await api
|
||||||
.getCameraManager()
|
.getCameraManager()
|
||||||
|
|||||||
@@ -2,11 +2,11 @@ import merge from 'lodash-es/merge';
|
|||||||
import { Action, TargetedActionContext } from '../types';
|
import { Action, TargetedActionContext } from '../types';
|
||||||
import { ActionContext } from 'action';
|
import { ActionContext } from 'action';
|
||||||
|
|
||||||
export const stopInProgressForThisTarget = (
|
export const stopInProgressForThisTarget = async (
|
||||||
targetID: string,
|
targetID: string,
|
||||||
context?: TargetedActionContext,
|
context?: TargetedActionContext,
|
||||||
): void => {
|
): Promise<void> => {
|
||||||
context?.[targetID]?.inProgressAction?.stop();
|
await context?.[targetID]?.inProgressAction?.stop();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const setInProgressForThisTarget = (
|
export const setInProgressForThisTarget = (
|
||||||
@@ -14,7 +14,7 @@ export const setInProgressForThisTarget = (
|
|||||||
context: ActionContext,
|
context: ActionContext,
|
||||||
contextKey: keyof ActionContext,
|
contextKey: keyof ActionContext,
|
||||||
action: Action,
|
action: Action,
|
||||||
) => {
|
): void => {
|
||||||
merge(context, {
|
merge(context, {
|
||||||
[contextKey]: {
|
[contextKey]: {
|
||||||
[targetID]: {
|
[targetID]: {
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ describe('ActionsManager', () => {
|
|||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle event', async () => {
|
it('should handle advanced camera card 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,
|
||||||
@@ -220,6 +220,27 @@ describe('ActionsManager', () => {
|
|||||||
expect(consoleSpy).toBeCalled();
|
expect(consoleSpy).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not handle generic event', async () => {
|
||||||
|
const event = new CustomEvent('ll-custom', {
|
||||||
|
detail: {
|
||||||
|
type: 'fire-dom-event',
|
||||||
|
foo: 'bar',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
const handler = vi.fn();
|
||||||
|
card.addEventListener('ll-custom', handler);
|
||||||
|
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(card);
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.handleCustomActionEvent(event);
|
||||||
|
|
||||||
|
expect(handler).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
it('should not handle event without detail', async () => {
|
it('should not handle event without detail', async () => {
|
||||||
const manager = new ActionsManager(createCardAPI());
|
const manager = new ActionsManager(createCardAPI());
|
||||||
|
|
||||||
|
|||||||
@@ -478,7 +478,8 @@ describe('should handle ptz action', () => {
|
|||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
vi.mocked(api.getCameraManager).mockReturnValue(createCameraManager(store));
|
const cameraManager = createCameraManager(store);
|
||||||
|
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
|
||||||
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
createView({ camera: 'camera.office' }),
|
createView({ camera: 'camera.office' }),
|
||||||
);
|
);
|
||||||
@@ -497,11 +498,24 @@ describe('should handle ptz action', () => {
|
|||||||
await vi.runOnlyPendingTimersAsync();
|
await vi.runOnlyPendingTimersAsync();
|
||||||
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(2);
|
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(2);
|
||||||
|
|
||||||
|
// Emulate the stop being called while the action is running, but before
|
||||||
|
// the *next* timer is scheduled.
|
||||||
|
let resolve: () => void;
|
||||||
|
const promise: Promise<void> = new Promise((_resolve) => {
|
||||||
|
resolve = _resolve;
|
||||||
|
});
|
||||||
|
vi.mocked(cameraManager.executePTZAction).mockReturnValueOnce(promise);
|
||||||
|
|
||||||
|
await vi.runOnlyPendingTimersAsync();
|
||||||
|
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(3);
|
||||||
|
|
||||||
action.stop();
|
action.stop();
|
||||||
|
|
||||||
|
resolve!();
|
||||||
await vi.runOnlyPendingTimersAsync();
|
await vi.runOnlyPendingTimersAsync();
|
||||||
|
|
||||||
// There should be no additional calls.
|
// There should be no additional calls.
|
||||||
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(2);
|
expect(api.getCameraManager().executePTZAction).toBeCalledTimes(3);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user