fix: Reolink PTZ support should detect presets (#1997)
- Related: #1964
This commit is contained in:
@@ -3,6 +3,7 @@ import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/pt
|
|||||||
import { HomeAssistant } from '../../ha/types';
|
import { HomeAssistant } from '../../ha/types';
|
||||||
import { localize } from '../../localize/localize';
|
import { localize } from '../../localize/localize';
|
||||||
import { PTZCapabilities, PTZMovementType } from '../../types';
|
import { PTZCapabilities, PTZMovementType } from '../../types';
|
||||||
|
import { createSelectOptionAction } from '../../utils/action.js';
|
||||||
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||||
import { BrowseMediaCamera } from '../browse-media/camera';
|
import { BrowseMediaCamera } from '../browse-media/camera';
|
||||||
import { Camera, CameraInitializationOptions } from '../camera';
|
import { Camera, CameraInitializationOptions } from '../camera';
|
||||||
@@ -18,7 +19,7 @@ interface ReolinkCameraInitializationOptions extends CameraInitializationOptions
|
|||||||
|
|
||||||
class ReolinkInitializationError extends CameraInitializationError {}
|
class ReolinkInitializationError extends CameraInitializationError {}
|
||||||
|
|
||||||
interface PTZButtonEntities {
|
interface PTZEntities {
|
||||||
stop?: string;
|
stop?: string;
|
||||||
left?: string;
|
left?: string;
|
||||||
right?: string;
|
right?: string;
|
||||||
@@ -26,13 +27,14 @@ interface PTZButtonEntities {
|
|||||||
down?: string;
|
down?: string;
|
||||||
zoom_in?: string;
|
zoom_in?: string;
|
||||||
zoom_out?: string;
|
zoom_out?: string;
|
||||||
|
presets?: string;
|
||||||
}
|
}
|
||||||
type PTZButton = keyof PTZButtonEntities;
|
type PTZEntity = keyof PTZEntities;
|
||||||
|
|
||||||
export class ReolinkCamera extends BrowseMediaCamera {
|
export class ReolinkCamera extends BrowseMediaCamera {
|
||||||
protected _channel: number | null = null;
|
protected _channel: number | null = null;
|
||||||
protected _reolinkUniqueID: string | null = null;
|
protected _reolinkUniqueID: string | null = null;
|
||||||
protected _ptzButtons: PTZButtonEntities | null = null;
|
protected _ptzEntities: PTZEntities | null = null;
|
||||||
|
|
||||||
public async initialize(options: ReolinkCameraInitializationOptions): Promise<Camera> {
|
public async initialize(options: ReolinkCameraInitializationOptions): Promise<Camera> {
|
||||||
await super.initialize(options);
|
await super.initialize(options);
|
||||||
@@ -65,30 +67,14 @@ export class ReolinkCamera extends BrowseMediaCamera {
|
|||||||
entityRegistry: EntityRegistryManager,
|
entityRegistry: EntityRegistryManager,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const config = this.getConfig();
|
const config = this.getConfig();
|
||||||
|
|
||||||
const ptzButtons = await this._getPTZButtons(hass, entityRegistry);
|
|
||||||
const configPTZCapabilities = getPTZCapabilitiesFromCameraConfig(this.getConfig());
|
const configPTZCapabilities = getPTZCapabilitiesFromCameraConfig(this.getConfig());
|
||||||
|
const ptzEntities = await this._getPTZEntities(hass, entityRegistry);
|
||||||
const reolinkPTZCapabilities: PTZCapabilities = {};
|
const reolinkPTZCapabilities = ptzEntities
|
||||||
for (const key of Object.keys(ptzButtons ?? {})) {
|
? this._entitiesToCapabilities(hass, ptzEntities)
|
||||||
switch (key) {
|
: null;
|
||||||
case 'left':
|
|
||||||
case 'right':
|
|
||||||
case 'up':
|
|
||||||
case 'down':
|
|
||||||
reolinkPTZCapabilities[key] = [PTZMovementType.Continuous];
|
|
||||||
break;
|
|
||||||
case 'zoom_in':
|
|
||||||
reolinkPTZCapabilities.zoomIn = [PTZMovementType.Continuous];
|
|
||||||
break;
|
|
||||||
case 'zoom_out':
|
|
||||||
reolinkPTZCapabilities.zoomOut = [PTZMovementType.Continuous];
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const combinedPTZCapabilities: PTZCapabilities | null =
|
const combinedPTZCapabilities: PTZCapabilities | null =
|
||||||
configPTZCapabilities || Object.keys(reolinkPTZCapabilities).length
|
configPTZCapabilities || reolinkPTZCapabilities
|
||||||
? {
|
? {
|
||||||
...reolinkPTZCapabilities,
|
...reolinkPTZCapabilities,
|
||||||
...configPTZCapabilities,
|
...configPTZCapabilities,
|
||||||
@@ -115,13 +101,47 @@ export class ReolinkCamera extends BrowseMediaCamera {
|
|||||||
disableExcept: config.capabilities?.disable_except,
|
disableExcept: config.capabilities?.disable_except,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
this._ptzButtons = ptzButtons;
|
this._ptzEntities = ptzEntities;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _getPTZButtons(
|
protected _entitiesToCapabilities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
ptzEntities: PTZEntities,
|
||||||
|
): PTZCapabilities | null {
|
||||||
|
const reolinkPTZCapabilities: PTZCapabilities = {};
|
||||||
|
for (const key of Object.keys(ptzEntities)) {
|
||||||
|
switch (key) {
|
||||||
|
case 'left':
|
||||||
|
case 'right':
|
||||||
|
case 'up':
|
||||||
|
case 'down':
|
||||||
|
reolinkPTZCapabilities[key] = [PTZMovementType.Continuous];
|
||||||
|
break;
|
||||||
|
case 'zoom_in':
|
||||||
|
reolinkPTZCapabilities.zoomIn = [PTZMovementType.Continuous];
|
||||||
|
break;
|
||||||
|
case 'zoom_out':
|
||||||
|
reolinkPTZCapabilities.zoomOut = [PTZMovementType.Continuous];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const ptzPresetsEntityState = ptzEntities?.presets
|
||||||
|
? hass.states[ptzEntities.presets]
|
||||||
|
: null;
|
||||||
|
if (Array.isArray(ptzPresetsEntityState?.attributes.options)) {
|
||||||
|
reolinkPTZCapabilities.presets = ptzPresetsEntityState.attributes.options;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* istanbul ignore next: this path cannot be reached as ptzEntities will
|
||||||
|
always have contents when this function is called -- @preserve */
|
||||||
|
return Object.keys(reolinkPTZCapabilities).length ? reolinkPTZCapabilities : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _getPTZEntities(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityRegistry: EntityRegistryManager,
|
entityRegistry: EntityRegistryManager,
|
||||||
): Promise<PTZButtonEntities | null> {
|
): Promise<PTZEntities | null> {
|
||||||
/* istanbul ignore next: this path cannot be reached as an exception is
|
/* istanbul ignore next: this path cannot be reached as an exception is
|
||||||
thrown in initialize() if this value is not found -- @preserve */
|
thrown in initialize() if this value is not found -- @preserve */
|
||||||
if (!this._reolinkUniqueID) {
|
if (!this._reolinkUniqueID) {
|
||||||
@@ -129,17 +149,24 @@ export class ReolinkCamera extends BrowseMediaCamera {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const uniqueIDPrefix = `${this._reolinkUniqueID}_${this._channel}_`;
|
const uniqueIDPrefix = `${this._reolinkUniqueID}_${this._channel}_`;
|
||||||
const buttonEntities = await entityRegistry.getMatchingEntities(
|
const allRelevantEntities = await entityRegistry.getMatchingEntities(
|
||||||
hass,
|
hass,
|
||||||
(ent: Entity) =>
|
(ent: Entity) =>
|
||||||
ent.config_entry_id === this._entity?.config_entry_id &&
|
ent.config_entry_id === this._entity?.config_entry_id &&
|
||||||
!!ent.unique_id &&
|
!!ent.unique_id &&
|
||||||
String(ent.unique_id).startsWith(uniqueIDPrefix) &&
|
String(ent.unique_id).startsWith(uniqueIDPrefix) &&
|
||||||
!ent.disabled_by &&
|
!ent.disabled_by,
|
||||||
|
);
|
||||||
|
const buttonEntities = allRelevantEntities.filter((ent: Entity) =>
|
||||||
ent.entity_id.startsWith('button.'),
|
ent.entity_id.startsWith('button.'),
|
||||||
);
|
);
|
||||||
|
const ptzPresetEntities = allRelevantEntities.filter(
|
||||||
|
(ent: Entity) =>
|
||||||
|
ent.unique_id === `${uniqueIDPrefix}ptz_preset` &&
|
||||||
|
ent.entity_id.startsWith('select.'),
|
||||||
|
);
|
||||||
|
|
||||||
const uniqueSuffixes: PTZButton[] = [
|
const uniqueSuffixes: PTZEntity[] = [
|
||||||
'stop',
|
'stop',
|
||||||
'left',
|
'left',
|
||||||
'right',
|
'right',
|
||||||
@@ -149,19 +176,23 @@ export class ReolinkCamera extends BrowseMediaCamera {
|
|||||||
'zoom_out',
|
'zoom_out',
|
||||||
];
|
];
|
||||||
|
|
||||||
const buttons: PTZButtonEntities = {};
|
const ptzEntities: PTZEntities = {};
|
||||||
for (const buttonEntity of buttonEntities) {
|
for (const buttonEntity of buttonEntities) {
|
||||||
for (const uniqueIDSuffix of uniqueSuffixes) {
|
for (const uniqueIDSuffix of uniqueSuffixes) {
|
||||||
if (
|
if (
|
||||||
buttonEntity.unique_id &&
|
buttonEntity.unique_id &&
|
||||||
String(buttonEntity.unique_id).endsWith(uniqueIDSuffix)
|
String(buttonEntity.unique_id).endsWith(uniqueIDSuffix)
|
||||||
) {
|
) {
|
||||||
buttons[uniqueIDSuffix] = buttonEntity.entity_id;
|
ptzEntities[uniqueIDSuffix] = buttonEntity.entity_id;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.keys(buttons).length ? buttons : null;
|
if (ptzPresetEntities.length === 1) {
|
||||||
|
ptzEntities.presets = ptzPresetEntities[0].entity_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.keys(ptzEntities).length ? ptzEntities : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getChannel(): number | null {
|
public getChannel(): number | null {
|
||||||
@@ -202,11 +233,28 @@ export class ReolinkCamera extends BrowseMediaCamera {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!this._ptzEntities) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (action === 'preset') {
|
||||||
|
const entityID = this._ptzEntities.presets;
|
||||||
|
const preset = options?.preset;
|
||||||
|
if (!preset || !entityID) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await executor.executeActions({
|
||||||
|
actions: [createSelectOptionAction('select', entityID, preset)],
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const entityID =
|
const entityID =
|
||||||
options?.phase === 'start'
|
options?.phase === 'start'
|
||||||
? this._ptzButtons?.[action]
|
? this._ptzEntities[action]
|
||||||
: options?.phase === 'stop'
|
: options?.phase === 'stop'
|
||||||
? this._ptzButtons?.stop
|
? this._ptzEntities.stop
|
||||||
: null;
|
: null;
|
||||||
if (!entityID) {
|
if (!entityID) {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { RemoteControlEntityPriority } from '../../config/schema/remote-control'
|
|||||||
import {
|
import {
|
||||||
createCameraAction,
|
createCameraAction,
|
||||||
createInternalCallbackAction,
|
createInternalCallbackAction,
|
||||||
createPerformAction,
|
createSelectOptionAction,
|
||||||
} from '../../utils/action';
|
} from '../../utils/action';
|
||||||
import { CardActionsAPI, CardConfigLoaderAPI, TaggedAutomation } from '../types';
|
import { CardActionsAPI, CardConfigLoaderAPI, TaggedAutomation } from '../types';
|
||||||
|
|
||||||
@@ -21,16 +21,6 @@ export const setRemoteControlEntityFromConfig = (api: CardConfigLoaderAPI) => {
|
|||||||
const cameraPriority: RemoteControlEntityPriority =
|
const cameraPriority: RemoteControlEntityPriority =
|
||||||
remoteControlConfig.entities.camera_priority;
|
remoteControlConfig.entities.camera_priority;
|
||||||
|
|
||||||
const createSelectOptionAction = (option: string) =>
|
|
||||||
createPerformAction('input_select.select_option', {
|
|
||||||
target: {
|
|
||||||
entity_id: cameraControlEntity,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
option: option,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Control entities functionality is implemented entirely by populating
|
// Control entities functionality is implemented entirely by populating
|
||||||
// automations.
|
// automations.
|
||||||
|
|
||||||
@@ -59,7 +49,11 @@ 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.
|
||||||
createSelectOptionAction('{{ advanced_camera_card.trigger.camera.to }}'),
|
createSelectOptionAction(
|
||||||
|
'input_select',
|
||||||
|
cameraControlEntity,
|
||||||
|
'{{ advanced_camera_card.trigger.camera.to }}',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
tag: automationTag,
|
tag: automationTag,
|
||||||
},
|
},
|
||||||
@@ -82,7 +76,11 @@ 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('{{ advanced_camera_card.camera }}'),
|
createSelectOptionAction(
|
||||||
|
'input_select',
|
||||||
|
cameraControlEntity,
|
||||||
|
'{{ advanced_camera_card.camera }}',
|
||||||
|
),
|
||||||
],
|
],
|
||||||
tag: automationTag,
|
tag: automationTag,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,9 +10,11 @@ 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 { MenuSubmenuSelect } from '../../config/schema/elements/custom/menu/submenu-select.js';
|
import { MenuSubmenuSelect } from '../../config/schema/elements/custom/menu/submenu-select.js';
|
||||||
import { MenuSubmenuItem } from '../../config/schema/elements/custom/menu/submenu.js';
|
import { MenuSubmenuItem } from '../../config/schema/elements/custom/menu/submenu.js';
|
||||||
|
import { computeDomain } from '../../ha/compute-domain.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 { Icon } from '../../types.js';
|
||||||
|
import { createSelectOptionAction } from '../../utils/action.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/types.js';
|
import { EntityRegistryManager } from '../../utils/ha/registry/entity/types.js';
|
||||||
@@ -83,6 +85,7 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const entityID = this.submenuSelect.entity;
|
const entityID = this.submenuSelect.entity;
|
||||||
|
const entityDomain = computeDomain(entityID);
|
||||||
const stateObj = this.hass.states[entityID];
|
const stateObj = this.hass.states[entityID];
|
||||||
const options = stateObj?.attributes?.options;
|
const options = stateObj?.attributes?.options;
|
||||||
if (!stateObj || !options) {
|
if (!stateObj || !options) {
|
||||||
@@ -98,19 +101,8 @@ export class AdvancedCameraCardSubmenuSelectButton extends LitElement {
|
|||||||
selected: stateObj.state === option,
|
selected: stateObj.state === option,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
title: title || option,
|
title: title || option,
|
||||||
...((entityID.startsWith('select.') || entityID.startsWith('input_select.')) && {
|
...((entityDomain === 'select' || entityDomain === 'input_select') && {
|
||||||
tap_action: {
|
tap_action: createSelectOptionAction(entityDomain, entityID, option),
|
||||||
action: 'perform-action',
|
|
||||||
perform_action: entityID.startsWith('select.')
|
|
||||||
? 'select.select_option'
|
|
||||||
: 'input_select.select_option',
|
|
||||||
target: {
|
|
||||||
entity_id: entityID,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
option: option,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
// Apply overrides the user may have specified for a given option.
|
// Apply overrides the user may have specified for a given option.
|
||||||
...(this.submenuSelect.options && this.submenuSelect.options[option]),
|
...(this.submenuSelect.options && this.submenuSelect.options[option]),
|
||||||
|
|||||||
@@ -218,6 +218,25 @@ export function createPerformAction(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function createSelectOptionAction(
|
||||||
|
domain: 'select' | 'input_select',
|
||||||
|
entityID: string,
|
||||||
|
option: string,
|
||||||
|
options?: {
|
||||||
|
cardID?: string;
|
||||||
|
},
|
||||||
|
): PerformActionActionConfig {
|
||||||
|
return createPerformAction(`${domain}.select_option`, {
|
||||||
|
...options,
|
||||||
|
target: {
|
||||||
|
entity_id: entityID,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
option: option,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get an action configuration given a config and an interaction (e.g. 'tap').
|
* Get an action configuration given a config and an interaction (e.g. 'tap').
|
||||||
* @param interaction The interaction: `tap`, `hold` or `double_tap`
|
* @param interaction The interaction: `tap`, `hold` or `double_tap`
|
||||||
|
|||||||
@@ -7,7 +7,12 @@ import { ActionsExecutor } from '../../../src/card-controller/actions/types';
|
|||||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||||
import { ProxyConfig } from '../../../src/config/schema/cameras';
|
import { ProxyConfig } from '../../../src/config/schema/cameras';
|
||||||
import { EntityRegistryManagerLive } from '../../../src/utils/ha/registry/entity';
|
import { EntityRegistryManagerLive } from '../../../src/utils/ha/registry/entity';
|
||||||
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
|
import {
|
||||||
|
createCameraConfig,
|
||||||
|
createHASS,
|
||||||
|
createRegistryEntity,
|
||||||
|
createStateEntity,
|
||||||
|
} from '../../test-utils';
|
||||||
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
|
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
|
||||||
|
|
||||||
describe('ReolinkCamera', () => {
|
describe('ReolinkCamera', () => {
|
||||||
@@ -51,6 +56,11 @@ describe('ReolinkCamera', () => {
|
|||||||
unique_id: '85270002TS7D4RUP_0_ptz_stop',
|
unique_id: '85270002TS7D4RUP_0_ptz_stop',
|
||||||
platform: 'reolink',
|
platform: 'reolink',
|
||||||
});
|
});
|
||||||
|
const selectEntityPTZ = createRegistryEntity({
|
||||||
|
entity_id: 'select.office_reolink_ptz_preset',
|
||||||
|
unique_id: '85270002TS7D4RUP_0_ptz_preset',
|
||||||
|
platform: 'reolink',
|
||||||
|
});
|
||||||
|
|
||||||
const ptzPopulatedEntityRegistryManager = new EntityRegistryManagerMock([
|
const ptzPopulatedEntityRegistryManager = new EntityRegistryManagerMock([
|
||||||
cameraEntity,
|
cameraEntity,
|
||||||
@@ -61,6 +71,7 @@ describe('ReolinkCamera', () => {
|
|||||||
buttonEntityPTZZoomIn,
|
buttonEntityPTZZoomIn,
|
||||||
buttonEntityPTZZoomOut,
|
buttonEntityPTZZoomOut,
|
||||||
buttonEntityPTZStop,
|
buttonEntityPTZStop,
|
||||||
|
selectEntityPTZ,
|
||||||
|
|
||||||
// Unrelated button.
|
// Unrelated button.
|
||||||
createRegistryEntity({
|
createRegistryEntity({
|
||||||
@@ -178,6 +189,36 @@ describe('ReolinkCamera', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should find PTZ select entity', async () => {
|
||||||
|
const config = createCameraConfig({
|
||||||
|
camera_entity: 'camera.office_reolink',
|
||||||
|
});
|
||||||
|
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||||
|
|
||||||
|
await camera.initialize({
|
||||||
|
hass: createHASS({
|
||||||
|
'select.office_reolink_ptz_preset': createStateEntity({
|
||||||
|
state: 'foo',
|
||||||
|
attributes: {
|
||||||
|
options: ['preset-one', 'preset-two'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||||
|
stateWatcher: mock<StateWatcher>(),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||||
|
left: ['continuous'],
|
||||||
|
right: ['continuous'],
|
||||||
|
up: ['continuous'],
|
||||||
|
down: ['continuous'],
|
||||||
|
zoomIn: ['continuous'],
|
||||||
|
zoomOut: ['continuous'],
|
||||||
|
presets: ['preset-one', 'preset-two'],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should allow configured PTZ actions to override', async () => {
|
it('should allow configured PTZ actions to override', async () => {
|
||||||
const config = createCameraConfig({
|
const config = createCameraConfig({
|
||||||
camera_entity: 'camera.office_reolink',
|
camera_entity: 'camera.office_reolink',
|
||||||
@@ -428,5 +469,78 @@ describe('ReolinkCamera', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should ignore relative actions', async () => {
|
||||||
|
const config = createCameraConfig({
|
||||||
|
camera_entity: 'camera.office_reolink',
|
||||||
|
});
|
||||||
|
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||||
|
|
||||||
|
await camera.initialize({
|
||||||
|
hass: createHASS(),
|
||||||
|
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||||
|
stateWatcher: mock<StateWatcher>(),
|
||||||
|
});
|
||||||
|
const executor = mock<ActionsExecutor>();
|
||||||
|
|
||||||
|
await camera.executePTZAction(executor, 'left');
|
||||||
|
expect(executor.executeActions).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should execute preset', () => {
|
||||||
|
it('for existing preset', async () => {
|
||||||
|
const config = createCameraConfig({
|
||||||
|
camera_entity: 'camera.office_reolink',
|
||||||
|
});
|
||||||
|
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||||
|
|
||||||
|
await camera.initialize({
|
||||||
|
hass: createHASS({
|
||||||
|
'select.office_reolink_ptz_preset': createStateEntity({
|
||||||
|
state: 'foo',
|
||||||
|
attributes: {
|
||||||
|
options: ['preset-one', 'preset-two'],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||||
|
stateWatcher: mock<StateWatcher>(),
|
||||||
|
});
|
||||||
|
const executor = mock<ActionsExecutor>();
|
||||||
|
|
||||||
|
await camera.executePTZAction(executor, 'preset', { preset: 'preset-two' });
|
||||||
|
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||||
|
actions: [
|
||||||
|
{
|
||||||
|
action: 'perform-action',
|
||||||
|
perform_action: 'select.select_option',
|
||||||
|
target: {
|
||||||
|
entity_id: 'select.office_reolink_ptz_preset',
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
option: 'preset-two',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('for non-existant preset', async () => {
|
||||||
|
const config = createCameraConfig({
|
||||||
|
camera_entity: 'camera.office_reolink',
|
||||||
|
});
|
||||||
|
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||||
|
|
||||||
|
await camera.initialize({
|
||||||
|
hass: createHASS(),
|
||||||
|
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||||
|
stateWatcher: mock<StateWatcher>(),
|
||||||
|
});
|
||||||
|
const executor = mock<ActionsExecutor>();
|
||||||
|
|
||||||
|
await camera.executePTZAction(executor, 'preset');
|
||||||
|
expect(executor.executeActions).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
createPTZControlsAction,
|
createPTZControlsAction,
|
||||||
createPTZDigitalAction,
|
createPTZDigitalAction,
|
||||||
createPTZMultiAction,
|
createPTZMultiAction,
|
||||||
|
createSelectOptionAction,
|
||||||
createViewAction,
|
createViewAction,
|
||||||
getActionConfigGivenAction,
|
getActionConfigGivenAction,
|
||||||
hasAction,
|
hasAction,
|
||||||
@@ -267,6 +268,24 @@ describe('createPerformAction', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('createSelectOptionAction', () => {
|
||||||
|
it('should create select option action', () => {
|
||||||
|
expect(
|
||||||
|
createSelectOptionAction('select', 'select.foo', 'option', {
|
||||||
|
cardID: 'card_id',
|
||||||
|
}),
|
||||||
|
).toEqual({
|
||||||
|
action: 'perform-action',
|
||||||
|
perform_action: 'select.select_option',
|
||||||
|
card_id: 'card_id',
|
||||||
|
target: { entity_id: 'select.foo' },
|
||||||
|
data: {
|
||||||
|
option: 'option',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('getActionConfigGivenAction', () => {
|
describe('getActionConfigGivenAction', () => {
|
||||||
const action = createViewAction('clips');
|
const action = createViewAction('clips');
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user