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 { localize } from '../../localize/localize';
|
||||
import { PTZCapabilities, PTZMovementType } from '../../types';
|
||||
import { createSelectOptionAction } from '../../utils/action.js';
|
||||
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||
import { BrowseMediaCamera } from '../browse-media/camera';
|
||||
import { Camera, CameraInitializationOptions } from '../camera';
|
||||
@@ -18,7 +19,7 @@ interface ReolinkCameraInitializationOptions extends CameraInitializationOptions
|
||||
|
||||
class ReolinkInitializationError extends CameraInitializationError {}
|
||||
|
||||
interface PTZButtonEntities {
|
||||
interface PTZEntities {
|
||||
stop?: string;
|
||||
left?: string;
|
||||
right?: string;
|
||||
@@ -26,13 +27,14 @@ interface PTZButtonEntities {
|
||||
down?: string;
|
||||
zoom_in?: string;
|
||||
zoom_out?: string;
|
||||
presets?: string;
|
||||
}
|
||||
type PTZButton = keyof PTZButtonEntities;
|
||||
type PTZEntity = keyof PTZEntities;
|
||||
|
||||
export class ReolinkCamera extends BrowseMediaCamera {
|
||||
protected _channel: number | null = null;
|
||||
protected _reolinkUniqueID: string | null = null;
|
||||
protected _ptzButtons: PTZButtonEntities | null = null;
|
||||
protected _ptzEntities: PTZEntities | null = null;
|
||||
|
||||
public async initialize(options: ReolinkCameraInitializationOptions): Promise<Camera> {
|
||||
await super.initialize(options);
|
||||
@@ -65,30 +67,14 @@ export class ReolinkCamera extends BrowseMediaCamera {
|
||||
entityRegistry: EntityRegistryManager,
|
||||
): Promise<void> {
|
||||
const config = this.getConfig();
|
||||
|
||||
const ptzButtons = await this._getPTZButtons(hass, entityRegistry);
|
||||
const configPTZCapabilities = getPTZCapabilitiesFromCameraConfig(this.getConfig());
|
||||
|
||||
const reolinkPTZCapabilities: PTZCapabilities = {};
|
||||
for (const key of Object.keys(ptzButtons ?? {})) {
|
||||
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 ptzEntities = await this._getPTZEntities(hass, entityRegistry);
|
||||
const reolinkPTZCapabilities = ptzEntities
|
||||
? this._entitiesToCapabilities(hass, ptzEntities)
|
||||
: null;
|
||||
|
||||
const combinedPTZCapabilities: PTZCapabilities | null =
|
||||
configPTZCapabilities || Object.keys(reolinkPTZCapabilities).length
|
||||
configPTZCapabilities || reolinkPTZCapabilities
|
||||
? {
|
||||
...reolinkPTZCapabilities,
|
||||
...configPTZCapabilities,
|
||||
@@ -115,13 +101,47 @@ export class ReolinkCamera extends BrowseMediaCamera {
|
||||
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,
|
||||
entityRegistry: EntityRegistryManager,
|
||||
): Promise<PTZButtonEntities | null> {
|
||||
): Promise<PTZEntities | null> {
|
||||
/* istanbul ignore next: this path cannot be reached as an exception is
|
||||
thrown in initialize() if this value is not found -- @preserve */
|
||||
if (!this._reolinkUniqueID) {
|
||||
@@ -129,17 +149,24 @@ export class ReolinkCamera extends BrowseMediaCamera {
|
||||
}
|
||||
|
||||
const uniqueIDPrefix = `${this._reolinkUniqueID}_${this._channel}_`;
|
||||
const buttonEntities = await entityRegistry.getMatchingEntities(
|
||||
const allRelevantEntities = await entityRegistry.getMatchingEntities(
|
||||
hass,
|
||||
(ent: Entity) =>
|
||||
ent.config_entry_id === this._entity?.config_entry_id &&
|
||||
!!ent.unique_id &&
|
||||
String(ent.unique_id).startsWith(uniqueIDPrefix) &&
|
||||
!ent.disabled_by &&
|
||||
ent.entity_id.startsWith('button.'),
|
||||
!ent.disabled_by,
|
||||
);
|
||||
const buttonEntities = allRelevantEntities.filter((ent: Entity) =>
|
||||
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',
|
||||
'left',
|
||||
'right',
|
||||
@@ -149,19 +176,23 @@ export class ReolinkCamera extends BrowseMediaCamera {
|
||||
'zoom_out',
|
||||
];
|
||||
|
||||
const buttons: PTZButtonEntities = {};
|
||||
const ptzEntities: PTZEntities = {};
|
||||
for (const buttonEntity of buttonEntities) {
|
||||
for (const uniqueIDSuffix of uniqueSuffixes) {
|
||||
if (
|
||||
buttonEntity.unique_id &&
|
||||
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 {
|
||||
@@ -202,11 +233,28 @@ export class ReolinkCamera extends BrowseMediaCamera {
|
||||
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 =
|
||||
options?.phase === 'start'
|
||||
? this._ptzButtons?.[action]
|
||||
? this._ptzEntities[action]
|
||||
: options?.phase === 'stop'
|
||||
? this._ptzButtons?.stop
|
||||
? this._ptzEntities.stop
|
||||
: null;
|
||||
if (!entityID) {
|
||||
return false;
|
||||
|
||||
Reference in New Issue
Block a user