feat: Add control/output of selected camera via an entity (#1895)

- Closes #1871
This commit is contained in:
Dermot Duffy
2025-02-17 13:33:10 -08:00
committed by GitHub
parent da79664a67
commit c66c3ec919
46 changed files with 677 additions and 91 deletions
@@ -13,7 +13,7 @@ describe('should handle camera_select action', () => {
{
action: 'fire-dom-event',
advanced_camera_card_action: 'camera_select',
camera: 'camera',
camera: 'camera.office',
},
);
@@ -23,13 +23,36 @@ describe('should handle camera_select action', () => {
expect.objectContaining({
params: {
view: 'live',
camera: 'camera',
camera: 'camera.office',
},
failSafe: true,
}),
);
});
it('should ignore requests to change to the current camera', async () => {
const api = createCardAPI();
vi.mocked(api.getViewManager().getView).mockReturnValue(
createView({
camera: 'camera.office',
}),
);
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
const action = new CameraSelectAction(
{},
{
action: 'fire-dom-event',
advanced_camera_card_action: 'camera_select',
camera: 'camera.office',
},
);
await action.execute(api);
expect(api.getViewManager().setViewByParametersWithNewQuery).not.toBeCalled();
});
it('without config', async () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(null);
@@ -45,7 +68,7 @@ describe('should handle camera_select action', () => {
{
action: 'fire-dom-event',
advanced_camera_card_action: 'camera_select',
camera: 'camera',
camera: 'camera.office',
},
);
@@ -55,7 +78,7 @@ describe('should handle camera_select action', () => {
expect.objectContaining({
params: {
view: 'timeline',
camera: 'camera',
camera: 'camera.office',
},
failSafe: true,
}),
@@ -84,7 +107,7 @@ describe('should handle camera_select action', () => {
{
action: 'fire-dom-event',
advanced_camera_card_action: 'camera_select',
camera: 'camera',
camera: 'camera.office',
},
);
@@ -94,7 +117,7 @@ describe('should handle camera_select action', () => {
expect.objectContaining({
params: {
view: 'clips',
camera: 'camera',
camera: 'camera.office',
},
failSafe: true,
}),
@@ -106,7 +129,7 @@ describe('should handle camera_select action', () => {
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(api.getTriggersManager().getMostRecentlyTriggeredCameraID).mockReturnValue(
'camera',
'camera.office',
);
const action = new CameraSelectAction(
@@ -124,7 +147,7 @@ describe('should handle camera_select action', () => {
expect.objectContaining({
params: {
view: 'live',
camera: 'camera',
camera: 'camera.office',
},
failSafe: true,
}),
@@ -136,7 +159,7 @@ describe('should handle camera_select action', () => {
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
vi.mocked(api.getTriggersManager().getMostRecentlyTriggeredCameraID).mockReturnValue(
'camera',
'camera.office',
);
const action = new CameraSelectAction(
@@ -0,0 +1,21 @@
import { expect, it, vi } from 'vitest';
import { InternalCallbackAction } from '../../../../src/card-controller/actions/actions/internal-callback';
import { INTERNAL_CALLBACK_ACTION } from '../../../../src/config/types';
import { createCardAPI } from '../../../test-utils';
it('should handle internal callback action', async () => {
const api = createCardAPI();
const callback = vi.fn();
const action = new InternalCallbackAction(
{},
{
action: 'fire-dom-event',
advanced_camera_card_action: INTERNAL_CALLBACK_ACTION,
callback: callback,
},
);
await action.execute(api);
expect(callback).toBeCalledWith(api);
});
+13 -2
View File
@@ -1,4 +1,4 @@
import { describe, expect, it } from 'vitest';
import { describe, expect, it, vi } from 'vitest';
import { CameraSelectAction } from '../../../src/card-controller/actions/actions/camera-select';
import { CameraUIAction } from '../../../src/card-controller/actions/actions/camera-ui';
import { DefaultAction } from '../../../src/card-controller/actions/actions/default';
@@ -7,6 +7,7 @@ import { DownloadAction } from '../../../src/card-controller/actions/actions/dow
import { ExpandAction } from '../../../src/card-controller/actions/actions/expand';
import { FullscreenAction } from '../../../src/card-controller/actions/actions/fullscreen';
import { GenericAction } from '../../../src/card-controller/actions/actions/generic';
import { InternalCallbackAction } from '../../../src/card-controller/actions/actions/internal-callback';
import { LogAction } from '../../../src/card-controller/actions/actions/log';
import { MediaPlayerAction } from '../../../src/card-controller/actions/actions/media-player';
import { MenuToggleAction } from '../../../src/card-controller/actions/actions/menu-toggle';
@@ -30,7 +31,10 @@ import { SubstreamSelectAction } from '../../../src/card-controller/actions/acti
import { UnmuteAction } from '../../../src/card-controller/actions/actions/unmute';
import { ViewAction } from '../../../src/card-controller/actions/actions/view';
import { ActionFactory } from '../../../src/card-controller/actions/factory';
import { AdvancedCameraCardCustomAction } from '../../../src/config/types';
import {
AdvancedCameraCardCustomAction,
INTERNAL_CALLBACK_ACTION,
} from '../../../src/config/types';
// @vitest-environment jsdom
describe('ActionFactory', () => {
@@ -165,6 +169,13 @@ describe('ActionFactory', () => {
},
StatusBarAction,
],
[
{
advanced_camera_card_action: INTERNAL_CALLBACK_ACTION,
callback: vi.fn(),
},
InternalCallbackAction,
],
])(
'advanced_camera_card_action: $advanced_camera_card_action',
(action: Partial<AdvancedCameraCardCustomAction>, classObject: object) => {
@@ -0,0 +1,137 @@
import { describe, expect, it, vi } from 'vitest';
import { setRemoteControlEntityFromConfig } from '../../../src/card-controller/config/load-control-entities';
import { createCardAPI, createConfig, createHASS, createStore } from '../../test-utils';
import { INTERNAL_CALLBACK_ACTION } from '../../../src/config/types';
describe('setRemoteControlEntityFromConfig', () => {
it('without control entity', () => {
const api = createCardAPI();
setRemoteControlEntityFromConfig(api);
expect(api.getAutomationsManager().deleteAutomations).toBeCalled();
expect(api.getAutomationsManager().addAutomations).not.toBeCalled();
});
it('with control entity', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
},
},
}),
);
setRemoteControlEntityFromConfig(api);
expect(api.getAutomationsManager().deleteAutomations).toBeCalled();
expect(api.getAutomationsManager().addAutomations).toBeCalledWith([
{
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
callback: expect.any(Function),
},
{
action: 'perform-action',
data: {
option: '{{ advanced_camera_card.camera }}',
},
perform_action: 'input_select.select_option',
target: {
entity_id: 'input_select.camera',
},
},
],
conditions: [
{
condition: 'config',
paths: ['remote_control.entities.camera'],
},
],
tag: setRemoteControlEntityFromConfig,
},
{
actions: [
{
action: 'perform-action',
data: {
option: '{{ advanced_camera_card.trigger.camera.to }}',
},
perform_action: 'input_select.select_option',
target: {
entity_id: 'input_select.camera',
},
},
],
conditions: [
{
condition: 'camera',
},
],
tag: setRemoteControlEntityFromConfig,
},
{
actions: [
{
action: 'fire-dom-event',
advanced_camera_card_action: 'camera_select',
camera: '{{ advanced_camera_card.trigger.state.to }}',
},
],
conditions: [
{
condition: 'state',
entity: 'input_select.camera',
},
],
tag: setRemoteControlEntityFromConfig,
},
]);
});
it('internal action should set options', () => {
const hass = createHASS();
const api = createCardAPI();
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
createConfig({
remote_control: {
entities: {
camera: 'input_select.camera',
},
},
}),
);
const store = createStore([
{
cameraID: 'camera.one',
},
{
cameraID: 'camera.two',
},
]);
vi.mocked(api.getCameraManager().getStore).mockReturnValue(store);
setRemoteControlEntityFromConfig(api);
const addOptionsAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
.calls[0][0][0].actions[0];
expect(addOptionsAction.advanced_camera_card_action).toBe(INTERNAL_CALLBACK_ACTION);
addOptionsAction.callback(api);
expect(hass.callService).toBeCalledWith(
'input_select',
'set_options',
{
options: ['camera.one', 'camera.two'],
},
{
entity_id: 'input_select.camera',
},
);
});
});
@@ -7,9 +7,11 @@ import { PTZAction } from '../../../src/config/ptz';
describe('setKeyboardShortcutsFromConfig', () => {
it('without shortcuts', () => {
const api = createCardAPI();
setKeyboardShortcutsFromConfig(api, 'tag');
setKeyboardShortcutsFromConfig(api);
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith('tag');
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith(
setKeyboardShortcutsFromConfig,
);
expect(api.getAutomationsManager().addAutomations).not.toBeCalled();
});
@@ -24,9 +26,11 @@ describe('setKeyboardShortcutsFromConfig', () => {
},
}),
);
setKeyboardShortcutsFromConfig(api, 'tag');
setKeyboardShortcutsFromConfig(api);
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith('tag');
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith(
setKeyboardShortcutsFromConfig,
);
expect(api.getAutomationsManager().addAutomations).not.toBeCalled();
});
@@ -59,9 +63,11 @@ describe('setKeyboardShortcutsFromConfig', () => {
}),
);
setKeyboardShortcutsFromConfig(api, 'tag');
setKeyboardShortcutsFromConfig(api);
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith('tag');
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith(
setKeyboardShortcutsFromConfig,
);
expect(api.getAutomationsManager().addAutomations).toBeCalledWith([
{
actions: [
@@ -83,7 +89,7 @@ describe('setKeyboardShortcutsFromConfig', () => {
state: 'down',
},
],
tag: 'tag',
tag: setKeyboardShortcutsFromConfig,
},
{
actions: [
@@ -101,7 +107,7 @@ describe('setKeyboardShortcutsFromConfig', () => {
state: 'up',
},
],
tag: 'tag',
tag: setKeyboardShortcutsFromConfig,
},
]);
});
@@ -110,9 +116,11 @@ describe('setKeyboardShortcutsFromConfig', () => {
const api = createCardAPI();
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig());
setKeyboardShortcutsFromConfig(api, 'tag');
setKeyboardShortcutsFromConfig(api);
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith('tag');
expect(api.getAutomationsManager().deleteAutomations).toBeCalledWith(
setKeyboardShortcutsFromConfig,
);
expect(api.getAutomationsManager().addAutomations).toBeCalledWith(
expect.arrayContaining([
{
@@ -133,7 +141,7 @@ describe('setKeyboardShortcutsFromConfig', () => {
state: 'down',
},
],
tag: 'tag',
tag: setKeyboardShortcutsFromConfig,
},
]),
);