From 36783855a22d263ad5a8cd7347d1d95a8f01d640 Mon Sep 17 00:00:00 2001 From: Dermot Duffy Date: Sat, 29 Mar 2025 12:49:36 -0700 Subject: [PATCH] fix: Fix issue with detection of zoom in/out buttons (#1986) - Related: #1964 --- docs/configuration/cameras/engine.md | 8 +++- src/camera-manager/reolink/camera.ts | 70 ++++++++++++++++------------ 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/docs/configuration/cameras/engine.md b/docs/configuration/cameras/engine.md index b1af506e..558384a0 100644 --- a/docs/configuration/cameras/engine.md +++ b/docs/configuration/cameras/engine.md @@ -9,7 +9,7 @@ A "Camera Engine" defines what "type" of camera is being configured (e.g. `friga | `frigate` | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | | `generic` | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | | `motioneye` | :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :white_check_mark: | -| `reolink` | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | +| `reolink` | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :eight_spoked_asterisk: | :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :white_check_mark: | :heavy_multiplication_x: | ### Live providers supported per Engine @@ -101,6 +101,12 @@ cameras: | `media_resolution` | `low` | Whether to retrieve `high` or `low` resolution media items. | | `url` | | The URL of the Reolink camera/NVR UI. If set, this value will be (exclusively) used for a `Camera UI` menu button. | +### PTZ Support + +Zero-configuration PTZ support is available for Reolink if your camera supports it. + +!> For Home Assistant control of Reolink camera PTZ functions, the relevent `button` entities must be enabled. To verify, navigate to `Settings -> Devices & services -> Reolink -> [Choose Device]`, and ensure the `PTZ` entities are enabled. Disabled entities are shown under the `+X disabled entities` label. :eight_spoked_asterisk: + ## Fully expanded reference [](../common/expanded-warning.md ':include') diff --git a/src/camera-manager/reolink/camera.ts b/src/camera-manager/reolink/camera.ts index ee691468..9eb07e4e 100644 --- a/src/camera-manager/reolink/camera.ts +++ b/src/camera-manager/reolink/camera.ts @@ -18,20 +18,21 @@ interface ReolinkCameraInitializationOptions extends CameraInitializationOptions class ReolinkInitializationError extends CameraInitializationError {} -interface PTZActionToButtonEntity { +interface PTZButtonEntities { stop?: string; left?: string; right?: string; up?: string; down?: string; - zoomIn?: string; - zoomOut?: string; + zoom_in?: string; + zoom_out?: string; } +type PTZButton = keyof PTZButtonEntities; export class ReolinkCamera extends BrowseMediaCamera { protected _channel: number | null = null; protected _reolinkUniqueID: string | null = null; - protected _ptzButtons: PTZActionToButtonEntity | null = null; + protected _ptzButtons: PTZButtonEntities | null = null; public async initialize(options: ReolinkCameraInitializationOptions): Promise { await super.initialize(options); @@ -65,18 +66,29 @@ export class ReolinkCamera extends BrowseMediaCamera { ): Promise { const config = this.getConfig(); - const ptzButtonMap = await this._getPTZButtonEntities(hass, entityRegistry); + const ptzButtons = await this._getPTZButtons(hass, entityRegistry); const configPTZCapabilities = getPTZCapabilitiesFromCameraConfig(this.getConfig()); - const reolinkPTZCapabilities = ptzButtonMap - ? Object.keys(ptzButtonMap).reduce( - (acc, key) => - key === 'stop' ? acc : { [key]: [PTZMovementType.Continuous], ...acc }, - {}, - ) - : null; + + 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 combinedPTZCapabilities: PTZCapabilities | null = - configPTZCapabilities || reolinkPTZCapabilities + configPTZCapabilities || Object.keys(reolinkPTZCapabilities).length ? { ...reolinkPTZCapabilities, ...configPTZCapabilities, @@ -103,13 +115,13 @@ export class ReolinkCamera extends BrowseMediaCamera { disableExcept: config.capabilities?.disable_except, }, ); - this._ptzButtons = ptzButtonMap; + this._ptzButtons = ptzButtons; } - protected async _getPTZButtonEntities( + protected async _getPTZButtons( hass: HomeAssistant, entityRegistry: EntityRegistryManager, - ): Promise { + ): Promise { /* 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) { @@ -127,29 +139,29 @@ export class ReolinkCamera extends BrowseMediaCamera { ent.entity_id.startsWith('button.'), ); - const capabilityMap = { - _ptz_stop: 'stop', - _ptz_left: 'left', - _ptz_right: 'right', - _ptz_up: 'up', - _ptz_down: 'down', - _ptz_zoom_in: 'zoomIn', - _ptz_zoom_out: 'zoomOut', - }; + const uniqueSuffixes: PTZButton[] = [ + 'stop', + 'left', + 'right', + 'up', + 'down', + 'zoom_in', + 'zoom_out', + ]; - const buttonMap: PTZActionToButtonEntity = {}; + const buttons: PTZButtonEntities = {}; for (const buttonEntity of buttonEntities) { - for (const [uniqueIDSuffix, capability] of Object.entries(capabilityMap)) { + for (const uniqueIDSuffix of uniqueSuffixes) { if ( buttonEntity.unique_id && String(buttonEntity.unique_id).endsWith(uniqueIDSuffix) ) { - buttonMap[capability] = buttonEntity.entity_id; + buttons[uniqueIDSuffix] = buttonEntity.entity_id; } } } - return Object.keys(buttonMap).length ? buttonMap : null; + return Object.keys(buttons).length ? buttons : null; } public getChannel(): number | null {