fix: Fix issue with detection of zoom in/out buttons (#1986)

- Related: #1964
This commit is contained in:
Dermot Duffy
2025-03-29 12:49:36 -07:00
committed by GitHub
parent b5eee5878a
commit 36783855a2
2 changed files with 48 additions and 30 deletions
+7 -1
View File
@@ -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: | | `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: | | `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: | | `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 ### Live providers supported per Engine
@@ -101,6 +101,12 @@ cameras:
| `media_resolution` | `low` | Whether to retrieve `high` or `low` resolution media items. | | `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. | | `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 ## Fully expanded reference
[](../common/expanded-warning.md ':include') [](../common/expanded-warning.md ':include')
+41 -29
View File
@@ -18,20 +18,21 @@ interface ReolinkCameraInitializationOptions extends CameraInitializationOptions
class ReolinkInitializationError extends CameraInitializationError {} class ReolinkInitializationError extends CameraInitializationError {}
interface PTZActionToButtonEntity { interface PTZButtonEntities {
stop?: string; stop?: string;
left?: string; left?: string;
right?: string; right?: string;
up?: string; up?: string;
down?: string; down?: string;
zoomIn?: string; zoom_in?: string;
zoomOut?: string; zoom_out?: string;
} }
type PTZButton = keyof PTZButtonEntities;
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: PTZActionToButtonEntity | null = null; protected _ptzButtons: PTZButtonEntities | 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,18 +66,29 @@ export class ReolinkCamera extends BrowseMediaCamera {
): Promise<void> { ): Promise<void> {
const config = this.getConfig(); const config = this.getConfig();
const ptzButtonMap = await this._getPTZButtonEntities(hass, entityRegistry); const ptzButtons = await this._getPTZButtons(hass, entityRegistry);
const configPTZCapabilities = getPTZCapabilitiesFromCameraConfig(this.getConfig()); const configPTZCapabilities = getPTZCapabilitiesFromCameraConfig(this.getConfig());
const reolinkPTZCapabilities = ptzButtonMap
? Object.keys(ptzButtonMap).reduce( const reolinkPTZCapabilities: PTZCapabilities = {};
(acc, key) => for (const key of Object.keys(ptzButtons ?? {})) {
key === 'stop' ? acc : { [key]: [PTZMovementType.Continuous], ...acc }, switch (key) {
{}, case 'left':
) case 'right':
: null; 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 || reolinkPTZCapabilities configPTZCapabilities || Object.keys(reolinkPTZCapabilities).length
? { ? {
...reolinkPTZCapabilities, ...reolinkPTZCapabilities,
...configPTZCapabilities, ...configPTZCapabilities,
@@ -103,13 +115,13 @@ export class ReolinkCamera extends BrowseMediaCamera {
disableExcept: config.capabilities?.disable_except, disableExcept: config.capabilities?.disable_except,
}, },
); );
this._ptzButtons = ptzButtonMap; this._ptzButtons = ptzButtons;
} }
protected async _getPTZButtonEntities( protected async _getPTZButtons(
hass: HomeAssistant, hass: HomeAssistant,
entityRegistry: EntityRegistryManager, entityRegistry: EntityRegistryManager,
): Promise<PTZActionToButtonEntity | null> { ): Promise<PTZButtonEntities | 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) {
@@ -127,29 +139,29 @@ export class ReolinkCamera extends BrowseMediaCamera {
ent.entity_id.startsWith('button.'), ent.entity_id.startsWith('button.'),
); );
const capabilityMap = { const uniqueSuffixes: PTZButton[] = [
_ptz_stop: 'stop', 'stop',
_ptz_left: 'left', 'left',
_ptz_right: 'right', 'right',
_ptz_up: 'up', 'up',
_ptz_down: 'down', 'down',
_ptz_zoom_in: 'zoomIn', 'zoom_in',
_ptz_zoom_out: 'zoomOut', 'zoom_out',
}; ];
const buttonMap: PTZActionToButtonEntity = {}; const buttons: PTZButtonEntities = {};
for (const buttonEntity of buttonEntities) { for (const buttonEntity of buttonEntities) {
for (const [uniqueIDSuffix, capability] of Object.entries(capabilityMap)) { 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)
) { ) {
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 { public getChannel(): number | null {