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
+1
View File
@@ -14,6 +14,7 @@
- [`overrides`](overrides.md)
- [`performance`](performance.md)
- [`profiles`](profiles.md)
- [`remote_control`](remote-control.md)
- [`status_bar`](status-bar.md)
- [`timeline`](timeline.md)
- [`view`](view.md)
+1
View File
@@ -17,6 +17,7 @@
- [`overrides`](../overrides.md)
- [`performance`](../performance.md)
- [`profiles`](../profiles.md)
- [`remote_control`](../remote-control.md)
- [`status_bar`](../status-bar.md)
- [`timeline`](../timeline.md)
- [`view`](../view.md)
@@ -17,6 +17,7 @@
- [`overrides`](../../overrides.md)
- [`performance`](../../performance.md)
- [`profiles`](../../profiles.md)
- [`remote_control`](../../remote-control.md)
- [`status_bar`](../../status-bar.md)
- [`timeline`](../../timeline.md)
- [`view`](../../view.md)
@@ -17,6 +17,7 @@
- [`overrides`](../../overrides.md)
- [`performance`](../../performance.md)
- [`profiles`](../../profiles.md)
- [`remote_control`](../../remote-control.md)
- [`status_bar`](../../status-bar.md)
- [`timeline`](../../timeline.md)
- [`view`](../../view.md)
+14 -13
View File
@@ -52,19 +52,20 @@ cameras:
### Capabilities
| Capability | Purpose |
| --------------------- | -------------------------------------------------------- |
| `clips` | Clips can be fetched from the camera. |
| `favorite-events` | Events can be favorited. |
| `favorite-recordings` | Recordings can be favorited. |
| `live` | Live video can be received from the camera. |
| `menu` | The camera should show up in the card camera menu. |
| `ptz` | The camera can be PTZ controlled. |
| `recordings` | Recordings can be fetched from the camera. |
| `seek` | Clips can be seeked / scrubbed by the timeline. |
| `snapshots` | Snapshots can be fetched from the camera. |
| `substream` | The camera can be used as a substream on another camera. |
| `trigger` | The camera can be triggered. |
| Capability | Purpose |
| ----------------------- | ------------------------------------------------------------------------------------------------- |
| `clips` | Clips can be fetched from the camera. |
| `remote-control-entity` | The camera can be selected by a [Camera Remote Control Entity](../remote-control.md?id=entities). |
| `favorite-events` | Events can be favorited. |
| `favorite-recordings` | Recordings can be favorited. |
| `live` | Live video can be received from the camera. |
| `menu` | The camera should show up in the card camera menu. |
| `ptz` | The camera can be PTZ controlled. |
| `recordings` | Recordings can be fetched from the camera. |
| `seek` | Clips can be seeked / scrubbed by the timeline. |
| `snapshots` | Snapshots can be fetched from the camera. |
| `substream` | The camera can be used as a substream on another camera. |
| `trigger` | The camera can be triggered. |
## `cast`
+1
View File
@@ -16,6 +16,7 @@
- [`overrides`](../overrides.md)
- [`performance`](../performance.md)
- [`profiles`](../profiles.md)
- [`remote_control`](../remote-control.md)
- [`status_bar`](../status-bar.md)
- [`timeline`](../timeline.md)
- [`view`](../view.md)
+1
View File
@@ -16,6 +16,7 @@
- [`overrides`](../overrides.md)
- [`performance`](../performance.md)
- [`profiles`](../profiles.md)
- [`remote_control`](../remote-control.md)
- [`status_bar`](../status-bar.md)
- [`timeline`](../timeline.md)
- [`view`](../view.md)
@@ -16,6 +16,7 @@
- [`overrides`](../../overrides.md)
- [`performance`](../../performance.md)
- [`profiles`](../../profiles.md)
- [`remote_control`](../../remote-control.md)
- [`status_bar`](../../status-bar.md)
- [`timeline`](../../timeline.md)
- [`view`](../../view.md)
@@ -16,6 +16,7 @@
- [`overrides`](../../overrides.md)
- [`performance`](../../performance.md)
- [`profiles`](../../profiles.md)
- [`remote_control`](../../remote-control.md)
- [`status_bar`](../../status-bar.md)
- [`timeline`](../../timeline.md)
- [`view`](../../view.md)
+43
View File
@@ -0,0 +1,43 @@
# `remote_control`
The `remote_control` configuration options control how a card instance can be remotely controlled.
```yaml
remote_control:
# [...]
```
## `entities`
Sets entities that are used to control different aspects of the card.
```yaml
remote_control:
entities:
# [...]
```
| Option | Default | Description |
| -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `camera` | | An `input_select` entity that the card will use for bidirectional control. When the selected camera on the card changes the entity will be updated to match. Likewise, when the entity state changes, the selected camera on the card will be updated to match. When the card is first started, the `input_select` entity will be updated to only have valid camera IDs from this card. Values must start with `input_select.`. |
?> To create an `input_select` entity to use in this manner, in the visual card
editor, under `Remote Control -> Remote Control Entities`, choose `Create a new
Dropdown helper`. Give the new entity an entity name (e.g. `my_selected_camera`)
and an optional icon. You must specify at least one option -- you can use any
placeholder value (e.g. `camera`) then choose `Add` (the card will automatically
reset the allowable options on start). Finally, click `Create`.
## Related Topics
- [Passing actions to the card in the URL](../usage/url-actions.md).
## Fully expanded reference
[](common/expanded-warning.md ':include')
```yaml
remote_control:
entities:
camera: input_select.my_selected_camera
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 MiB

+6
View File
@@ -14,6 +14,12 @@ A dashboard with the card can be cast onto a suitable device (such as the Nest H
![](images/card-on-nest-hub.jpg 'Card casting :size=400')
## Control via Entities
Bidirectional control of camera selection via an entity.
![](images/control-entities.gif 'Control via Entities :size=400')
## Dark mode
![](images/dark-mode.gif 'Dark mode :size=400')
+1
View File
@@ -139,6 +139,7 @@ export class FrigateCamera extends Camera {
{
'favorite-events': !birdseye,
'favorite-recordings': false,
'remote-control-entity': true,
seek: !birdseye,
clips: !birdseye,
snapshots: !birdseye,
@@ -63,6 +63,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
{
'favorite-events': false,
'favorite-recordings': false,
'remote-control-entity': true,
clips: false,
live: true,
menu: true,
@@ -83,6 +83,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
{
'favorite-events': false,
'favorite-recordings': false,
'remote-control-entity': true,
clips: true,
live: true,
menu: true,
@@ -137,6 +137,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
{
'favorite-events': false,
'favorite-recordings': false,
'remote-control-entity': true,
clips: true,
live: true,
menu: true,
@@ -12,7 +12,11 @@ export class CameraSelectAction extends AdvancedCameraCardAction<CameraSelectAct
const view = api.getViewManager().getView();
const config = api.getConfigManager().getConfig();
if (selectCameraID && view) {
// Don't do anything if the camera is already selected (especially important
// for control entities, as otherwise every camera change will generate a
// double request for events, once when the camera changes and another when
// the observed state of the control entity changes to match).
if (selectCameraID && view && selectCameraID !== view.camera) {
const viewOnCameraSelect = config?.view.camera_select ?? 'current';
const targetViewName =
viewOnCameraSelect === 'current' ? view.view : viewOnCameraSelect;
@@ -0,0 +1,9 @@
import { InternalCallbackActionConfig } from '../../../config/types';
import { CardActionsAPI } from '../../types';
import { AdvancedCameraCardAction } from './base';
export class InternalCallbackAction extends AdvancedCameraCardAction<InternalCallbackActionConfig> {
public async execute(api: CardActionsAPI): Promise<void> {
await this._action.callback(api);
}
}
+4 -1
View File
@@ -1,6 +1,6 @@
import { ActionConfig } from '@dermotduffy/custom-card-helpers';
import { ActionContext } from 'action';
import { ActionType } from '../../config/types';
import { ActionType, INTERNAL_CALLBACK_ACTION } from '../../config/types';
import { convertActionToCardCustomAction } from '../../utils/action';
import { CameraSelectAction } from './actions/camera-select';
import { CameraUIAction } from './actions/camera-ui';
@@ -10,6 +10,7 @@ import { DownloadAction } from './actions/download';
import { ExpandAction } from './actions/expand';
import { FullscreenAction } from './actions/fullscreen';
import { GenericAction } from './actions/generic';
import { InternalCallbackAction } from './actions/internal-callback';
import { LogAction } from './actions/log';
import { MediaPlayerAction } from './actions/media-player';
import { MenuToggleAction } from './actions/menu-toggle';
@@ -132,6 +133,8 @@ export class ActionFactory {
return new LogAction(context, cardCustomAction, options?.config);
case 'status_bar':
return new StatusBarAction(context, cardCustomAction, options?.config);
case INTERNAL_CALLBACK_ACTION:
return new InternalCallbackAction(context, cardCustomAction, options?.config);
}
/* istanbul ignore next: this path cannot be reached -- @preserve */
+3 -1
View File
@@ -14,6 +14,7 @@ import { InitializationAspect } from '../initialization-manager.js';
import { CardConfigAPI } from '../types.js';
import { getOverriddenConfig } from './get-overridden-config.js';
import { setAutomationsFromConfig } from './load-automations.js';
import { setRemoteControlEntityFromConfig } from './load-control-entities.js';
import { setKeyboardShortcutsFromConfig } from './load-keyboard-shortcuts.js';
export class ConfigManager {
@@ -109,7 +110,8 @@ export class ConfigManager {
this._api.getMessageManager().reset();
this._api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
setKeyboardShortcutsFromConfig(this._api, this);
setKeyboardShortcutsFromConfig(this._api);
setRemoteControlEntityFromConfig(this._api);
setAutomationsFromConfig(this._api);
this._processOverrideConfig();
@@ -0,0 +1,98 @@
import {
createCameraAction,
createInternalCallbackAction,
createPerformAction,
} from '../../utils/action';
import { CardActionsAPI, CardConfigLoaderAPI, TaggedAutomation } from '../types';
export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
const automationTag = setRemoteControlEntityFromConfig;
api.getAutomationsManager().deleteAutomations(automationTag);
const cameraControlEntity = api.getConfigManager().getConfig()?.remote_control
?.entities?.camera;
if (!cameraControlEntity) {
return;
}
const createSelectOptionAction = (option: string) =>
createPerformAction('input_select.select_option', {
target: {
entity_id: cameraControlEntity,
},
data: {
option: option,
},
});
// Control entities functionality is implemented entirely by populating
// automations.
const automations: TaggedAutomation[] = [
{
conditions: [
{
condition: 'config' as const,
paths: ['remote_control.entities.camera'],
},
],
actions: [
// Set the possible options on the entity to the camera IDs via a
// callback to `setCameraOptionsOnEntity` (below).
createInternalCallbackAction((api: CardActionsAPI) =>
setCameraOptionsOnEntity(cameraControlEntity, api),
),
// Set the selected option to the current camera ID.
createSelectOptionAction('{{ advanced_camera_card.camera }}'),
],
tag: automationTag,
},
{
conditions: [
{
condition: 'camera' as const,
},
],
actions: [
// When the camera changes, update the entity to match.
createSelectOptionAction('{{ advanced_camera_card.trigger.camera.to }}'),
],
tag: automationTag,
},
{
conditions: [
{
condition: 'state' as const,
entity: cameraControlEntity,
},
],
actions: [
// When the entity state changes, updated the selected option.
createCameraAction(
'camera_select',
'{{ advanced_camera_card.trigger.state.to }}',
),
],
tag: automationTag,
},
];
api.getAutomationsManager().addAutomations(automations);
};
const setCameraOptionsOnEntity = async (entity: string, api: CardActionsAPI) => {
const hass = api.getHASSManager().getHASS();
const cameraIDs = api.getCameraManager().getStore().getCameraIDs();
await hass?.callService(
'input_select',
'set_options',
{
options: [...cameraIDs],
},
{
entity_id: entity,
},
);
};
@@ -6,18 +6,16 @@ import { PTZAction } from '../../config/ptz';
import { createPTZMultiAction } from '../../utils/action';
import { CardConfigLoaderAPI, TaggedAutomation } from '../types';
export const setKeyboardShortcutsFromConfig = (
api: CardConfigLoaderAPI,
tag: unknown,
) => {
api.getAutomationsManager().deleteAutomations(tag);
export const setKeyboardShortcutsFromConfig = (api: CardConfigLoaderAPI) => {
const automationTag = setKeyboardShortcutsFromConfig;
api.getAutomationsManager().deleteAutomations(automationTag);
const shortcuts = api.getConfigManager().getConfig()?.view.keyboard_shortcuts;
if (!shortcuts) {
return;
}
const automations = convertKeyboardShortcutsToAutomations(tag, shortcuts);
const automations = convertKeyboardShortcutsToAutomations(automationTag, shortcuts);
if (automations.length) {
api.getAutomationsManager().addAutomations(automations);
}
+14 -6
View File
@@ -1,5 +1,9 @@
import { AdvancedCameraCardCustomAction, ViewActionConfig } from '../config/types';
import { createCameraAction, createGeneralAction } from '../utils/action.js';
import {
createCameraAction,
createGeneralAction,
createViewAction,
} from '../utils/action.js';
import { ViewParameters } from '../view/view';
import { CardQueryStringAPI } from './types';
import { SubstreamSelectViewModifier } from './view/modifiers/substream-select';
@@ -113,21 +117,25 @@ export class QueryStringManager {
}
break;
case 'camera_ui':
case 'clip':
case 'clips':
case 'default':
case 'diagnostics':
case 'download':
case 'expand':
case 'menu_toggle':
customAction = createGeneralAction(action, {
cardID: cardID,
});
break;
case 'clip':
case 'clips':
case 'diagnostics':
case 'image':
case 'live':
case 'menu_toggle':
case 'recording':
case 'recordings':
case 'snapshot':
case 'snapshots':
case 'timeline':
customAction = createGeneralAction(action, {
customAction = createViewAction(action, {
cardID: cardID,
});
break;
+4 -1
View File
@@ -90,6 +90,7 @@ export interface CardConfigAPI {
getConditionStateManager(): ConditionStateManager;
getConfigManager(): ConfigManager;
getDefaultManager(): DefaultManager;
getHASSManager(): HASSManager;
getInitializationManager(): InitializationManager;
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
getMediaPlayerManager(): MediaPlayerManager;
@@ -101,8 +102,9 @@ export interface CardConfigAPI {
}
export interface CardConfigLoaderAPI {
getConfigManager(): ConfigManager;
getAutomationsManager(): AutomationsManager;
getConfigManager(): ConfigManager;
getHASSManager(): HASSManager;
}
export interface CardDefaultManagerAPI {
@@ -178,6 +180,7 @@ export interface CardInitializerAPI {
getCardElementManager(): CardElementManager;
getConditionStateManager(): ConditionStateManager;
getConfigManager(): ConfigManager;
getConditionStateManager(): ConditionStateManager;
getDefaultManager(): DefaultManager;
getEntityRegistryManager(): EntityRegistryManager;
getHASSManager(): HASSManager;
+11 -14
View File
@@ -20,6 +20,7 @@ import {
createMediaPlayerAction,
createPTZControlsAction,
createPTZMultiAction,
createViewAction,
} from '../utils/action';
import { isTruthy } from '../utils/basic';
import { isBeingCasted } from '../utils/casting';
@@ -119,7 +120,7 @@ export class MenuButtonController {
config.menu?.style === 'hidden'
? (createGeneralAction('menu_toggle') as AdvancedCameraCardCustomAction)
: (createGeneralAction('default') as AdvancedCameraCardCustomAction),
hold_action: createGeneralAction('diagnostics') as AdvancedCameraCardCustomAction,
hold_action: createViewAction('diagnostics') as AdvancedCameraCardCustomAction,
};
}
@@ -233,7 +234,7 @@ export class MenuButtonController {
type: 'custom:advanced-camera-card-menu-icon',
title: localize('config.view.views.live'),
style: view.is('live') ? this._getEmphasizedStyle() : {},
tap_action: createGeneralAction('live') as AdvancedCameraCardCustomAction,
tap_action: createViewAction('live') as AdvancedCameraCardCustomAction,
}
: null;
}
@@ -250,8 +251,8 @@ export class MenuButtonController {
type: 'custom:advanced-camera-card-menu-icon',
title: localize('config.view.views.clips'),
style: view?.is('clips') ? this._getEmphasizedStyle() : {},
tap_action: createGeneralAction('clips') as AdvancedCameraCardCustomAction,
hold_action: createGeneralAction('clip') as AdvancedCameraCardCustomAction,
tap_action: createViewAction('clips') as AdvancedCameraCardCustomAction,
hold_action: createViewAction('clip') as AdvancedCameraCardCustomAction,
}
: null;
}
@@ -268,8 +269,8 @@ export class MenuButtonController {
type: 'custom:advanced-camera-card-menu-icon',
title: localize('config.view.views.snapshots'),
style: view?.is('snapshots') ? this._getEmphasizedStyle() : {},
tap_action: createGeneralAction('snapshots') as AdvancedCameraCardCustomAction,
hold_action: createGeneralAction('snapshot') as AdvancedCameraCardCustomAction,
tap_action: createViewAction('snapshots') as AdvancedCameraCardCustomAction,
hold_action: createViewAction('snapshot') as AdvancedCameraCardCustomAction,
}
: null;
}
@@ -286,12 +287,8 @@ export class MenuButtonController {
type: 'custom:advanced-camera-card-menu-icon',
title: localize('config.view.views.recordings'),
style: view.is('recordings') ? this._getEmphasizedStyle() : {},
tap_action: createGeneralAction(
'recordings',
) as AdvancedCameraCardCustomAction,
hold_action: createGeneralAction(
'recording',
) as AdvancedCameraCardCustomAction,
tap_action: createViewAction('recordings') as AdvancedCameraCardCustomAction,
hold_action: createViewAction('recording') as AdvancedCameraCardCustomAction,
}
: null;
}
@@ -308,7 +305,7 @@ export class MenuButtonController {
type: 'custom:advanced-camera-card-menu-icon',
title: localize('config.view.views.image'),
style: view?.is('image') ? this._getEmphasizedStyle() : {},
tap_action: createGeneralAction('image') as AdvancedCameraCardCustomAction,
tap_action: createViewAction('image') as AdvancedCameraCardCustomAction,
}
: null;
}
@@ -325,7 +322,7 @@ export class MenuButtonController {
type: 'custom:advanced-camera-card-menu-icon',
title: localize('config.view.views.timeline'),
style: view.is('timeline') ? this._getEmphasizedStyle() : {},
tap_action: createGeneralAction('timeline') as AdvancedCameraCardCustomAction,
tap_action: createViewAction('timeline') as AdvancedCameraCardCustomAction,
}
: null;
}
+40 -2
View File
@@ -397,7 +397,7 @@ const logActionConfigSchema = advancedCameraCardCustomActionsBaseSchema.extend({
});
export type LogActionConfig = z.infer<typeof logActionConfigSchema>;
export const advancedCameraCardCustomActionSchema = z.union([
const advancedCameraCardCustomActionSchema = z.union([
cameraSelectActionConfigSchema,
generalActionConfigSchema,
substreamSelectActionConfigSchema,
@@ -416,6 +416,26 @@ 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__';
const internalCallbackActionConfigSchema =
advancedCameraCardCustomActionsBaseSchema.extend({
advanced_camera_card_action: z.literal(INTERNAL_CALLBACK_ACTION),
// The callback is expected to be called with a CardController API object.
callback: z.function().args(z.any()).returns(z.promise(z.void())),
});
export type InternalCallbackActionConfig = z.infer<
typeof internalCallbackActionConfigSchema
>;
export const internalAdvancedCameraCardCustomActionSchema =
advancedCameraCardCustomActionSchema.or(internalCallbackActionConfigSchema);
export type InternalAdvancedCameraCardCustomAction = z.infer<
typeof internalAdvancedCameraCardCustomActionSchema
>;
// Cannot use discriminatedUnion since advancedCameraCardCustomActionSchema uses
// a transform on the discriminated union key.
export const actionSchema = z.union([
@@ -429,7 +449,9 @@ export const actionSchema = z.union([
customActionSchema,
advancedCameraCardCustomActionSchema,
]);
export type ActionType = z.infer<typeof actionSchema>;
const internalActionSchema = actionSchema.or(internalCallbackActionConfigSchema);
export type ActionType = z.infer<typeof internalActionSchema>;
const actionsBaseSchema = z
.object({
@@ -2075,6 +2097,20 @@ const PROFILES = ['casting', 'low-performance', 'scrubbing'] as const;
export type ProfileType = (typeof PROFILES)[number];
export const profilesSchema = z.enum(PROFILES).array().optional();
// *************************************************************************
// *** Remote Control Configuration ***
// *************************************************************************
const remoteControlConfigSchema = z
.object({
entities: z
.object({
camera: z.string().startsWith('input_select.').optional(),
})
.optional(),
})
.optional();
// *************************************************************************
// *** Card Configuration ***
// *************************************************************************
@@ -2114,6 +2150,8 @@ export const advancedCameraCardConfigSchema = z.object({
// that be easily used in a URL.
card_id: z.string().regex(cardIDRegex).optional(),
remote_control: remoteControlConfigSchema,
// Stock lovelace card config.
type: z.string(),
});
+4
View File
@@ -382,6 +382,10 @@ export const CONF_PERFORMANCE_STYLE_BORDER_RADIUS = `${CONF_PERFORMANCE}.style.b
export const CONF_PROFILES = 'profiles' as const;
export const CONF_REMOTE_CONTROL = 'remote_control' as const;
export const CONF_REMOTE_CONTROL_ENTITIES_CAMERA =
`${CONF_REMOTE_CONTROL}.entities.camera` as const;
// Taken from https://github.com/home-assistant/frontend/blob/a759767d794f02527d127802831e68d3caf0cb7a/src/data/media-player.ts#L82
export const MEDIA_PLAYER_SUPPORT_TURN_OFF = 256;
export const MEDIA_PLAYER_SUPPORT_STOP = 4096;
+24
View File
@@ -190,6 +190,7 @@ import {
CONF_PERFORMANCE_STYLE_BORDER_RADIUS,
CONF_PERFORMANCE_STYLE_BOX_SHADOW,
CONF_PROFILES,
CONF_REMOTE_CONTROL_ENTITIES_CAMERA,
CONF_STATUS_BAR_HEIGHT,
CONF_STATUS_BAR_ITEMS,
CONF_STATUS_BAR_POPUP_SECONDS,
@@ -281,6 +282,7 @@ const MENU_MENU_BUTTONS = 'menu.buttons';
const MENU_OPTIONS = 'options';
const MENU_PERFORMANCE_FEATURES = 'performance.features';
const MENU_PERFORMANCE_STYLE = 'performance.style';
const MENU_REMOTE_CONTROL_ENTITIES = 'remote_control.entities';
const MENU_STATUS_BAR_ITEMS = 'status_bar.items';
const MENU_TIMELINE_FORMAT = 'timeline.format';
const MENU_TIMELINE_CONTROLS_THUMBNAILS = 'timeline.controls.thumbnails';
@@ -369,6 +371,11 @@ const options: EditorOptions = {
name: localize('editor.profiles'),
secondary: localize('editor.profiles_secondary'),
},
remote_control: {
icon: 'remote',
name: localize('editor.remote_control'),
secondary: localize('editor.remote_control_secondary'),
},
overrides: {
icon: 'file-replace',
name: localize('editor.overrides'),
@@ -3051,6 +3058,23 @@ export class AdvancedCameraCardEditor extends LitElement implements LovelaceCard
)}
</div>`
: ''}
${this._renderOptionSetHeader('remote_control')}
${this._expandedMenus[MENU_OPTIONS] === 'remote_control'
? html` <div class="values">
${this._putInSubmenu(
MENU_REMOTE_CONTROL_ENTITIES,
true,
'config.remote_control.entities.editor_label',
'mdi:devices',
html`
${this._renderEntitySelector(
CONF_REMOTE_CONTROL_ENTITIES_CAMERA,
'input_select',
)}
`,
)}
</div>`
: ''}
${this._config['overrides'] !== undefined
? html` ${this._renderOptionSetHeader('overrides')}
${this._expandedMenus[MENU_OPTIONS] === 'overrides'
+8
View File
@@ -459,6 +459,12 @@
"low-performance": "",
"scrubbing": ""
},
"remote_control": {
"entities": {
"camera": "",
"editor_label": ""
}
},
"status_bar": {
"height": "",
"items": {
@@ -596,6 +602,8 @@
"performance_secondary": "Opcions de rendiment de la targeta",
"profiles": "",
"profiles_secondary": "",
"remote_control": "",
"remote_control_secondary": "",
"status_bar": "",
"status_bar_secondary": "",
"timeline": "Cronologia",
+8
View File
@@ -459,6 +459,12 @@
"low-performance": "Low performance",
"scrubbing": "Video scrubbing"
},
"remote_control": {
"entities": {
"editor_label": "Remote Control Entities",
"camera": "Input Select entity to control camera"
}
},
"status_bar": {
"height": "Status bar height in pixels",
"items": {
@@ -596,6 +602,8 @@
"performance_secondary": "Card performance options",
"profiles": "Configuration profiles",
"profiles_secondary": "Choose pre-configured sets of defaults",
"remote_control": "Remote Control",
"remote_control_secondary": "Options for remote controlling the card",
"status_bar": "Status bar",
"status_bar_secondary": "Status bar look & feel options",
"timeline": "Timeline",
+8
View File
@@ -459,6 +459,12 @@
"low-performance": "Basse performance",
"scrubbing": "Balayage vidéo"
},
"remote_control": {
"entities": {
"camera": "",
"editor_label": ""
}
},
"status_bar": {
"height": "Hauteur de la barre d'état en pixels",
"items": {
@@ -596,6 +602,8 @@
"performance_secondary": "Options de performances de la carte",
"profiles": "Profils de configuration",
"profiles_secondary": "Choisir des ensembles de paramètres par défaut pré-configurés",
"remote_control": "",
"remote_control_secondary": "",
"status_bar": "Barre d'état",
"status_bar_secondary": "Options d'apparence et de comportement de la barre d'état",
"timeline": "Chronologie",
+8
View File
@@ -459,6 +459,12 @@
"low-performance": "",
"scrubbing": ""
},
"remote_control": {
"entities": {
"camera": "",
"editor_label": ""
}
},
"status_bar": {
"height": "",
"items": {
@@ -596,6 +602,8 @@
"performance_secondary": "",
"profiles": "",
"profiles_secondary": "",
"remote_control": "",
"remote_control_secondary": "",
"status_bar": "",
"status_bar_secondary": "",
"timeline": "Timeline",
+8
View File
@@ -459,6 +459,12 @@
"low-performance": "",
"scrubbing": ""
},
"remote_control": {
"entities": {
"camera": "",
"editor_label": ""
}
},
"status_bar": {
"height": "",
"items": {
@@ -596,6 +602,8 @@
"performance_secondary": "Opções de desempenho do cartão",
"profiles": "",
"profiles_secondary": "",
"remote_control": "",
"remote_control_secondary": "",
"status_bar": "",
"status_bar_secondary": "",
"timeline": "Linha do tempo",
+8
View File
@@ -459,6 +459,12 @@
"low-performance": "",
"scrubbing": ""
},
"remote_control": {
"entities": {
"camera": "",
"editor_label": ""
}
},
"status_bar": {
"height": "",
"items": {
@@ -596,6 +602,8 @@
"performance_secondary": "",
"profiles": "",
"profiles_secondary": "",
"remote_control": "",
"remote_control_secondary": "",
"status_bar": "",
"status_bar_secondary": "",
"timeline": "Linha do tempo",
+3
View File
@@ -111,6 +111,8 @@ export interface CapabilitiesRaw {
'favorite-events'?: boolean;
'favorite-recordings'?: boolean;
'remote-control-entity'?: boolean;
seek?: boolean;
ptz?: PTZCapabilities;
@@ -123,6 +125,7 @@ export interface CapabilitiesRaw {
export type CapabilityKey = keyof CapabilitiesRaw;
export const capabilityKeys: readonly [CapabilityKey, ...CapabilityKey[]] = [
'clips',
'remote-control-entity',
'favorite-events',
'favorite-recordings',
'live',
+63 -10
View File
@@ -1,22 +1,33 @@
import {
ActionConfig,
ServiceCallRequest,
hasAction as customCardHasAction,
} from '@dermotduffy/custom-card-helpers';
import { CardActionsAPI } from '../card-controller/types.js';
import { ZoomSettingsBase } from '../components-lib/zoom/types.js';
import { PTZAction } from '../config/ptz.js';
import {
ActionPhase,
ActionType,
Actions,
AdvancedCameraCardCustomAction,
AdvancedCameraCardGeneralAction,
AdvancedCameraCardUserSpecifiedView,
CameraSelectActionConfig,
DisplayModeActionConfig,
GeneralActionConfig,
INTERNAL_CALLBACK_ACTION,
InternalAdvancedCameraCardCustomAction,
InternalCallbackActionConfig,
LogActionConfig,
LogActionLevel,
MediaPlayerActionConfig,
PTZActionConfig,
PTZControlsActionConfig,
PTZDigitialActionConfig,
PTZMultiActionConfig,
advancedCameraCardCustomActionSchema,
SubstreamSelectActionConfig,
ViewActionConfig,
internalAdvancedCameraCardCustomActionSchema,
} from '../config/types.js';
import { arrayify } from './basic.js';
@@ -27,22 +38,35 @@ import { arrayify } from './basic.js';
*/
export function convertActionToCardCustomAction(
action: unknown,
): AdvancedCameraCardCustomAction | null {
): 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 = advancedCameraCardCustomActionSchema.safeParse(action);
const parseResult = internalAdvancedCameraCardCustomActionSchema.safeParse(action);
return parseResult.success ? parseResult.data : null;
}
export function createGeneralAction(
action: AdvancedCameraCardGeneralAction | AdvancedCameraCardUserSpecifiedView,
action: AdvancedCameraCardGeneralAction,
options?: {
cardID?: string;
},
): AdvancedCameraCardCustomAction {
): GeneralActionConfig {
return {
action: 'fire-dom-event',
advanced_camera_card_action: action,
...(options?.cardID && { card_id: options.cardID }),
};
}
export function createViewAction(
action: AdvancedCameraCardUserSpecifiedView,
options?: {
cardID?: string;
},
): ViewActionConfig {
return {
action: 'fire-dom-event',
advanced_camera_card_action: action,
@@ -56,7 +80,7 @@ export function createCameraAction(
options?: {
cardID?: string;
},
): AdvancedCameraCardCustomAction {
): CameraSelectActionConfig | SubstreamSelectActionConfig {
return {
action: 'fire-dom-event',
advanced_camera_card_action: action,
@@ -71,7 +95,7 @@ export function createMediaPlayerAction(
options?: {
cardID?: string;
},
): AdvancedCameraCardCustomAction {
): MediaPlayerActionConfig {
return {
action: 'fire-dom-event',
advanced_camera_card_action: 'media_player',
@@ -86,7 +110,7 @@ export function createDisplayModeAction(
options?: {
cardID?: string;
},
): AdvancedCameraCardCustomAction {
): DisplayModeActionConfig {
return {
action: 'fire-dom-event',
advanced_camera_card_action: 'display_mode_select',
@@ -100,7 +124,7 @@ export function createPTZControlsAction(
options?: {
cardID?: string;
},
): AdvancedCameraCardCustomAction {
): PTZControlsActionConfig {
return {
action: 'fire-dom-event',
advanced_camera_card_action: 'ptz_controls',
@@ -179,6 +203,35 @@ export function createLogAction(
};
}
export function createInternalCallbackAction(
callback: (api: CardActionsAPI) => Promise<void>,
options?: {
cardID?: string;
},
): InternalCallbackActionConfig {
return {
action: 'fire-dom-event',
advanced_camera_card_action: INTERNAL_CALLBACK_ACTION,
callback: callback,
...(options?.cardID && { card_id: options.cardID }),
};
}
export function createPerformAction(
perform_action: string,
options?: {
data?: ServiceCallRequest['serviceData'];
target?: ServiceCallRequest['target'];
},
): ActionType {
return {
action: 'perform-action' as const,
perform_action: perform_action,
...(options?.target && { target: options.target }),
...(options?.data && { data: options.data }),
};
}
/**
* Get an action configuration given a config and an interaction (e.g. 'tap').
* @param interaction The interaction: `tap`, `hold` or `double_tap`
@@ -242,6 +242,7 @@ describe('ReolinkCameraManagerEngine', () => {
'favorite-events': false,
'favorite-recordings': false,
clips: true,
'remote-control-entity': true,
live: true,
menu: true,
recordings: false,
@@ -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,
},
]),
);
@@ -20,7 +20,7 @@ import {
ViewDisplayMode,
} from '../../src/config/types';
import { AdvancedCameraCardMediaPlayer } from '../../src/types';
import { createGeneralAction } from '../../src/utils/action';
import { createGeneralAction, createViewAction } from '../../src/utils/action';
import { ViewMedia } from '../../src/view/media';
import { MediaQueriesResults } from '../../src/view/media-queries-results';
import { View } from '../../src/view/view';
@@ -94,7 +94,7 @@ describe('MenuButtonController', () => {
type: 'custom:advanced-camera-card-menu-icon',
title: 'Iris / Default View / Unhide menu',
tap_action: createGeneralAction('menu_toggle'),
hold_action: createGeneralAction('diagnostics'),
hold_action: createViewAction('diagnostics'),
});
});
@@ -111,7 +111,7 @@ describe('MenuButtonController', () => {
type: 'custom:advanced-camera-card-menu-icon',
title: 'Iris / Default View / Unhide menu',
tap_action: createGeneralAction('default'),
hold_action: createGeneralAction('diagnostics'),
hold_action: createViewAction('diagnostics'),
});
});
});
+2 -2
View File
@@ -6,7 +6,7 @@ import {
dimensionsConfigSchema,
advancedCameraCardConditionSchema,
advancedCameraCardCustomActionsBaseSchema,
advancedCameraCardCustomActionSchema,
internalAdvancedCameraCardCustomActionSchema,
} from '../../src/config/types';
import { createConfig } from '../test-utils';
@@ -641,7 +641,7 @@ describe('should lazy evaluate schemas', () => {
},
],
};
expect(advancedCameraCardCustomActionSchema.parse(input)).toEqual(input);
expect(internalAdvancedCameraCardCustomActionSchema.parse(input)).toEqual(input);
});
});
+6 -7
View File
@@ -19,7 +19,6 @@ import { ActionsManager } from '../src/card-controller/actions/actions-manager';
import { AutomationsManager } from '../src/card-controller/automations-manager';
import { CameraURLManager } from '../src/card-controller/camera-url-manager';
import { CardElementManager } from '../src/card-controller/card-element-manager';
import { ConditionStateManager } from '../src/conditions/state-manager';
import { ConfigManager } from '../src/card-controller/config/config-manager';
import { CardController } from '../src/card-controller/controller';
import { DefaultManager } from '../src/card-controller/default-manager';
@@ -40,18 +39,17 @@ import { StatusBarItemManager } from '../src/card-controller/status-bar-item-man
import { StyleManager } from '../src/card-controller/style-manager';
import { TriggersManager } from '../src/card-controller/triggers-manager';
import { ViewManager } from '../src/card-controller/view/view-manager';
import { ConditionStateManager } from '../src/conditions/state-manager';
import {
ActionsConfig,
AdvancedCameraCardCondition,
AdvancedCameraCardConfig,
AdvancedCameraCardCustomAction,
CameraConfig,
InternalAdvancedCameraCardCustomAction,
PerformanceConfig,
RawAdvancedCameraCardConfig,
advancedCameraCardConditionSchema,
advancedCameraCardConfigSchema,
advancedCameraCardCustomActionSchema,
cameraConfigSchema,
internalAdvancedCameraCardCustomActionSchema,
performanceConfigSchema,
} from '../src/config/types';
import { CapabilitiesRaw, ExtendedHomeAssistant, MediaLoadedInfo } from '../src/types';
@@ -65,8 +63,8 @@ import { View, ViewParameters } from '../src/view/view';
export const createAction = (
action: Record<string, unknown>,
): AdvancedCameraCardCustomAction | null => {
const result = advancedCameraCardCustomActionSchema.safeParse({
): InternalAdvancedCameraCardCustomAction | null => {
const result = internalAdvancedCameraCardCustomActionSchema.safeParse({
action: 'custom:advanced-camera-card-action',
...action,
});
@@ -260,6 +258,7 @@ export const createCapabilities = (capabilities?: CapabilitiesRaw): Capabilities
return new Capabilities({
'favorite-events': false,
'favorite-recordings': false,
'remote-control-entity': true,
clips: false,
live: false,
recordings: false,
+34 -2
View File
@@ -1,18 +1,20 @@
import { hasAction as customCardHasAction } from '@dermotduffy/custom-card-helpers';
import { afterEach, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { actionSchema } from '../../src/config/types';
import { actionSchema, INTERNAL_CALLBACK_ACTION } from '../../src/config/types';
import {
convertActionToCardCustomAction,
createCameraAction,
createDisplayModeAction,
createGeneralAction,
createInternalCallbackAction,
createLogAction,
createMediaPlayerAction,
createPTZAction,
createPTZControlsAction,
createPTZDigitalAction,
createPTZMultiAction,
createViewAction,
getActionConfigGivenAction,
hasAction,
stopEventFromActivatingCardWideActions,
@@ -45,7 +47,21 @@ describe('convertActionToAdvancedCameraCardCustomAction', () => {
describe('createGeneralAction', () => {
it('should create general action', () => {
expect(
createGeneralAction('clips', {
createGeneralAction('camera_ui', {
cardID: 'card_id',
}),
).toEqual({
action: 'fire-dom-event',
advanced_camera_card_action: 'camera_ui',
card_id: 'card_id',
});
});
});
describe('createViewAction', () => {
it('should create view action', () => {
expect(
createViewAction('clips', {
cardID: 'card_id',
}),
).toEqual({
@@ -241,6 +257,22 @@ describe('createLogAction', () => {
});
});
describe('createInternalCallbackAction', () => {
it('should create internal callback action', () => {
const callback = vi.fn();
expect(
createInternalCallbackAction(callback, {
cardID: 'card_id',
}),
).toEqual({
action: 'fire-dom-event',
advanced_camera_card_action: INTERNAL_CALLBACK_ACTION,
callback: callback,
card_id: 'card_id',
});
});
});
describe('getActionConfigGivenAction', () => {
const action = actionSchema.parse({
action: 'fire-dom-event',