fix: Race condition in remote control entity handling (#2267)
- Related #2244
This commit is contained in:
@@ -1,10 +1,6 @@
|
|||||||
import { isEqual } from 'lodash-es';
|
import { isEqual } from 'lodash-es';
|
||||||
import { RemoteControlEntityPriority } from '../../config/schema/remote-control';
|
import { RemoteControlEntityPriority } from '../../config/schema/remote-control';
|
||||||
import {
|
import { createCameraAction, createInternalCallbackAction } from '../../utils/action';
|
||||||
createCameraAction,
|
|
||||||
createInternalCallbackAction,
|
|
||||||
createSelectOptionAction,
|
|
||||||
} from '../../utils/action';
|
|
||||||
import { CardActionsAPI, CardConfigLoaderAPI, TaggedAutomation } from '../types';
|
import { CardActionsAPI, CardConfigLoaderAPI, TaggedAutomation } from '../types';
|
||||||
|
|
||||||
export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
||||||
@@ -48,11 +44,15 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
actions: [
|
actions: [
|
||||||
// When the camera changes, update the entity to match.
|
// When the camera changes, update the entity to match (only if different
|
||||||
createSelectOptionAction(
|
// to avoid race conditions when multiple cards share the same entity).
|
||||||
'input_select',
|
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2244
|
||||||
cameraControlEntity,
|
createInternalCallbackAction((api: CardActionsAPI) =>
|
||||||
'{{ advanced_camera_card.trigger.camera.to }}',
|
selectOptionOnEntityIfDifferent(
|
||||||
|
cameraControlEntity,
|
||||||
|
api.getViewManager().getView()?.camera,
|
||||||
|
api,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
tag: automationTag,
|
tag: automationTag,
|
||||||
@@ -76,10 +76,12 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
|||||||
`{{ hass.states["${cameraControlEntity}"].state }}`,
|
`{{ hass.states["${cameraControlEntity}"].state }}`,
|
||||||
)
|
)
|
||||||
: // Set the selected option in the entity to the current camera ID.
|
: // Set the selected option in the entity to the current camera ID.
|
||||||
createSelectOptionAction(
|
createInternalCallbackAction((api: CardActionsAPI) =>
|
||||||
'input_select',
|
selectOptionOnEntityIfDifferent(
|
||||||
cameraControlEntity,
|
cameraControlEntity,
|
||||||
'{{ advanced_camera_card.camera }}',
|
api.getViewManager().getView()?.camera,
|
||||||
|
api,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
tag: automationTag,
|
tag: automationTag,
|
||||||
@@ -105,6 +107,33 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
|||||||
api.getAutomationsManager().addAutomations(automations);
|
api.getAutomationsManager().addAutomations(automations);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectOptionOnEntityIfDifferent = async (
|
||||||
|
entity: string,
|
||||||
|
option: string | undefined,
|
||||||
|
api: CardActionsAPI,
|
||||||
|
): Promise<void> => {
|
||||||
|
const hass = api.getHASSManager().getHASS();
|
||||||
|
const currentState = hass?.states[entity]?.state;
|
||||||
|
|
||||||
|
// Only update if the option is defined and different from current state.
|
||||||
|
// This prevents race conditions when multiple cards share the same entity.
|
||||||
|
// See: https://github.com/dermotduffy/advanced-camera-card/issues/2244
|
||||||
|
if (!option || option === currentState) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await hass?.callService(
|
||||||
|
'input_select',
|
||||||
|
'select_option',
|
||||||
|
{
|
||||||
|
option: option,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entity_id: entity,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const setCameraOptionsOnEntity = async (entity: string, api: CardActionsAPI) => {
|
const setCameraOptionsOnEntity = async (entity: string, api: CardActionsAPI) => {
|
||||||
const hass = api.getHASSManager().getHASS();
|
const hass = api.getHASSManager().getHASS();
|
||||||
const cameraIDs = api.getCameraManager().getStore().getCameraIDs();
|
const cameraIDs = api.getCameraManager().getStore().getCameraIDs();
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { assert, describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
import { setRemoteControlEntityFromConfig } from '../../../src/card-controller/config/load-control-entities';
|
import { setRemoteControlEntityFromConfig } from '../../../src/card-controller/config/load-control-entities';
|
||||||
import { INTERNAL_CALLBACK_ACTION } from '../../../src/config/schema/actions/custom/internal';
|
import {
|
||||||
|
INTERNAL_CALLBACK_ACTION,
|
||||||
|
InternalCallbackActionConfig,
|
||||||
|
} from '../../../src/config/schema/actions/custom/internal';
|
||||||
import { isAdvancedCameraCardCustomAction } from '../../../src/utils/action';
|
import { isAdvancedCameraCardCustomAction } from '../../../src/utils/action';
|
||||||
import {
|
import {
|
||||||
createCardAPI,
|
createCardAPI,
|
||||||
@@ -8,6 +11,7 @@ import {
|
|||||||
createHASS,
|
createHASS,
|
||||||
createStateEntity,
|
createStateEntity,
|
||||||
createStore,
|
createStore,
|
||||||
|
createView,
|
||||||
} from '../../test-utils';
|
} from '../../test-utils';
|
||||||
|
|
||||||
describe('setRemoteControlEntityFromConfig', () => {
|
describe('setRemoteControlEntityFromConfig', () => {
|
||||||
@@ -55,14 +59,9 @@ describe('setRemoteControlEntityFromConfig', () => {
|
|||||||
{
|
{
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
action: 'perform-action',
|
action: 'fire-dom-event',
|
||||||
data: {
|
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
|
||||||
option: '{{ advanced_camera_card.trigger.camera.to }}',
|
callback: expect.any(Function),
|
||||||
},
|
|
||||||
perform_action: 'input_select.select_option',
|
|
||||||
target: {
|
|
||||||
entity_id: 'input_select.camera',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
conditions: [
|
conditions: [
|
||||||
@@ -75,14 +74,9 @@ describe('setRemoteControlEntityFromConfig', () => {
|
|||||||
{
|
{
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
action: 'perform-action',
|
action: 'fire-dom-event',
|
||||||
data: {
|
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
|
||||||
option: '{{ advanced_camera_card.camera }}',
|
callback: expect.any(Function),
|
||||||
},
|
|
||||||
perform_action: 'input_select.select_option',
|
|
||||||
target: {
|
|
||||||
entity_id: 'input_select.camera',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
conditions: [
|
conditions: [
|
||||||
@@ -147,14 +141,9 @@ describe('setRemoteControlEntityFromConfig', () => {
|
|||||||
{
|
{
|
||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
action: 'perform-action',
|
action: 'fire-dom-event',
|
||||||
data: {
|
advanced_camera_card_action: '__INTERNAL_CALLBACK_ACTION__',
|
||||||
option: '{{ advanced_camera_card.trigger.camera.to }}',
|
callback: expect.any(Function),
|
||||||
},
|
|
||||||
perform_action: 'input_select.select_option',
|
|
||||||
target: {
|
|
||||||
entity_id: 'input_select.camera',
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
conditions: [
|
conditions: [
|
||||||
@@ -225,9 +214,12 @@ describe('setRemoteControlEntityFromConfig', () => {
|
|||||||
setRemoteControlEntityFromConfig(api);
|
setRemoteControlEntityFromConfig(api);
|
||||||
|
|
||||||
const addOptionsAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
const addOptionsAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
||||||
.calls[0][0][0].actions?.[0];
|
.calls[0][0][0].actions?.[0] as InternalCallbackActionConfig;
|
||||||
assert(addOptionsAction && isAdvancedCameraCardCustomAction(addOptionsAction));
|
expect(addOptionsAction).toBeTruthy();
|
||||||
assert(addOptionsAction.advanced_camera_card_action === INTERNAL_CALLBACK_ACTION);
|
expect(isAdvancedCameraCardCustomAction(addOptionsAction)).toBeTruthy();
|
||||||
|
expect(addOptionsAction.advanced_camera_card_action).toBe(
|
||||||
|
INTERNAL_CALLBACK_ACTION,
|
||||||
|
);
|
||||||
|
|
||||||
addOptionsAction.callback(api);
|
addOptionsAction.callback(api);
|
||||||
expect(hass.callService).toBeCalledWith(
|
expect(hass.callService).toBeCalledWith(
|
||||||
@@ -272,12 +264,306 @@ describe('setRemoteControlEntityFromConfig', () => {
|
|||||||
setRemoteControlEntityFromConfig(api);
|
setRemoteControlEntityFromConfig(api);
|
||||||
|
|
||||||
const addOptionsAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
const addOptionsAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
||||||
.calls[0][0][0].actions?.[0];
|
.calls[0][0][0].actions?.[0] as InternalCallbackActionConfig;
|
||||||
assert(addOptionsAction && isAdvancedCameraCardCustomAction(addOptionsAction));
|
expect(addOptionsAction).toBeTruthy();
|
||||||
assert(addOptionsAction.advanced_camera_card_action === INTERNAL_CALLBACK_ACTION);
|
expect(isAdvancedCameraCardCustomAction(addOptionsAction)).toBeTruthy();
|
||||||
|
expect(addOptionsAction.advanced_camera_card_action).toBe(
|
||||||
|
INTERNAL_CALLBACK_ACTION,
|
||||||
|
);
|
||||||
|
|
||||||
addOptionsAction.callback(api);
|
addOptionsAction.callback(api);
|
||||||
expect(hass.callService).not.toBeCalled();
|
expect(hass.callService).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not throw when hass is undefined setting options', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
||||||
|
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] as InternalCallbackActionConfig;
|
||||||
|
expect(addOptionsAction).toBeTruthy();
|
||||||
|
expect(isAdvancedCameraCardCustomAction(addOptionsAction)).toBeTruthy();
|
||||||
|
expect(addOptionsAction.advanced_camera_card_action).toBe(
|
||||||
|
INTERNAL_CALLBACK_ACTION,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should not throw
|
||||||
|
addOptionsAction.callback(api);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should select option on entity', () => {
|
||||||
|
it('should select option when camera differs from entity state', () => {
|
||||||
|
const hass = createHASS({
|
||||||
|
'input_select.camera': createStateEntity({
|
||||||
|
state: 'camera.one',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
remote_control: {
|
||||||
|
entities: {
|
||||||
|
camera: 'input_select.camera',
|
||||||
|
camera_priority: 'card',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({
|
||||||
|
camera: 'camera.two',
|
||||||
|
view: 'live',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
setRemoteControlEntityFromConfig(api);
|
||||||
|
|
||||||
|
// Get the camera sync callback (automation index 1)
|
||||||
|
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
||||||
|
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
|
||||||
|
expect(cameraSyncAction).toBeTruthy();
|
||||||
|
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
|
||||||
|
expect(cameraSyncAction.advanced_camera_card_action).toBe(
|
||||||
|
INTERNAL_CALLBACK_ACTION,
|
||||||
|
);
|
||||||
|
|
||||||
|
cameraSyncAction.callback(api);
|
||||||
|
expect(hass.callService).toBeCalledWith(
|
||||||
|
'input_select',
|
||||||
|
'select_option',
|
||||||
|
{
|
||||||
|
option: 'camera.two',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entity_id: 'input_select.camera',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not select option when camera matches entity state', () => {
|
||||||
|
const hass = createHASS({
|
||||||
|
'input_select.camera': createStateEntity({
|
||||||
|
state: 'camera.one',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
remote_control: {
|
||||||
|
entities: {
|
||||||
|
camera: 'input_select.camera',
|
||||||
|
camera_priority: 'card',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({
|
||||||
|
camera: 'camera.one',
|
||||||
|
view: 'live',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
setRemoteControlEntityFromConfig(api);
|
||||||
|
|
||||||
|
// Get the camera sync callback (automation index 1)
|
||||||
|
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
||||||
|
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
|
||||||
|
expect(cameraSyncAction).toBeTruthy();
|
||||||
|
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
|
||||||
|
expect(cameraSyncAction.advanced_camera_card_action).toBe(
|
||||||
|
INTERNAL_CALLBACK_ACTION,
|
||||||
|
);
|
||||||
|
|
||||||
|
cameraSyncAction.callback(api);
|
||||||
|
|
||||||
|
// Should NOT call select_option since entity already shows camera.one
|
||||||
|
expect(hass.callService).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not select option when camera is undefined', () => {
|
||||||
|
const hass = createHASS({
|
||||||
|
'input_select.camera': createStateEntity({
|
||||||
|
state: 'camera.one',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
remote_control: {
|
||||||
|
entities: {
|
||||||
|
camera: 'input_select.camera',
|
||||||
|
camera_priority: 'card',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(null);
|
||||||
|
|
||||||
|
setRemoteControlEntityFromConfig(api);
|
||||||
|
|
||||||
|
// Get the camera sync callback (automation index 1)
|
||||||
|
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
||||||
|
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
|
||||||
|
expect(cameraSyncAction).toBeTruthy();
|
||||||
|
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
|
||||||
|
expect(cameraSyncAction.advanced_camera_card_action).toBe(
|
||||||
|
INTERNAL_CALLBACK_ACTION,
|
||||||
|
);
|
||||||
|
|
||||||
|
cameraSyncAction.callback(api);
|
||||||
|
|
||||||
|
// Should NOT call select_option since camera is undefined
|
||||||
|
expect(hass.callService).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not throw when hass is undefined', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
remote_control: {
|
||||||
|
entities: {
|
||||||
|
camera: 'input_select.camera',
|
||||||
|
camera_priority: 'card',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({
|
||||||
|
camera: 'camera.two',
|
||||||
|
view: 'live',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
setRemoteControlEntityFromConfig(api);
|
||||||
|
|
||||||
|
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
||||||
|
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
|
||||||
|
expect(cameraSyncAction).toBeTruthy();
|
||||||
|
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
|
||||||
|
expect(cameraSyncAction.advanced_camera_card_action).toBe(
|
||||||
|
INTERNAL_CALLBACK_ACTION,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Should not throw and obviously not call service (as hass is null)
|
||||||
|
cameraSyncAction.callback(api);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select option when entity state is undefined', () => {
|
||||||
|
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',
|
||||||
|
camera_priority: 'card',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({
|
||||||
|
camera: 'camera.two',
|
||||||
|
view: 'live',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
setRemoteControlEntityFromConfig(api);
|
||||||
|
|
||||||
|
const cameraSyncAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
||||||
|
.calls[0][0][1].actions?.[0] as InternalCallbackActionConfig;
|
||||||
|
expect(cameraSyncAction).toBeTruthy();
|
||||||
|
expect(isAdvancedCameraCardCustomAction(cameraSyncAction)).toBeTruthy();
|
||||||
|
expect(cameraSyncAction.advanced_camera_card_action).toBe(
|
||||||
|
INTERNAL_CALLBACK_ACTION,
|
||||||
|
);
|
||||||
|
|
||||||
|
cameraSyncAction.callback(api);
|
||||||
|
expect(hass.callService).toBeCalledWith(
|
||||||
|
'input_select',
|
||||||
|
'select_option',
|
||||||
|
{
|
||||||
|
option: 'camera.two',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entity_id: 'input_select.camera',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should select option on initialization with card priority', () => {
|
||||||
|
const hass = createHASS({
|
||||||
|
'input_select.camera': createStateEntity({
|
||||||
|
state: 'camera.one',
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
remote_control: {
|
||||||
|
entities: {
|
||||||
|
camera: 'input_select.camera',
|
||||||
|
camera_priority: 'card',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({
|
||||||
|
camera: 'camera.two',
|
||||||
|
view: 'live',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
setRemoteControlEntityFromConfig(api);
|
||||||
|
|
||||||
|
// Get the initialization callback (automation index 2)
|
||||||
|
const initAction = vi.mocked(api.getAutomationsManager().addAutomations).mock
|
||||||
|
.calls[0][0][2].actions?.[0] as InternalCallbackActionConfig;
|
||||||
|
expect(initAction).toBeTruthy();
|
||||||
|
expect(isAdvancedCameraCardCustomAction(initAction)).toBeTruthy();
|
||||||
|
expect(initAction.advanced_camera_card_action).toBe(INTERNAL_CALLBACK_ACTION);
|
||||||
|
|
||||||
|
initAction.callback(api);
|
||||||
|
expect(hass.callService).toBeCalledWith(
|
||||||
|
'input_select',
|
||||||
|
'select_option',
|
||||||
|
{
|
||||||
|
option: 'camera.two',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
entity_id: 'input_select.camera',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user