diff --git a/docs/configuration/cameras/README.md b/docs/configuration/cameras/README.md index 3a4a5cf9..175b89dd 100644 --- a/docs/configuration/cameras/README.md +++ b/docs/configuration/cameras/README.md @@ -64,6 +64,7 @@ cameras: | `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` @@ -415,6 +416,22 @@ cameras: dynamic: true ssl_verification: auto ssl_ciphers: auto + - camera_entity: camera.capabilities_reference + capabilities: + disable_except: + - clips + - favorite-events + - favorite-recordings + - live + - menu + - ptz + - recordings + - seek + - snapshots + - substream + - trigger + disable: + # Capabilities to selectively disable. cameras_global: triggers: motion: false diff --git a/docs/support.md b/docs/support.md index 19f7b918..e32eeaf3 100644 --- a/docs/support.md +++ b/docs/support.md @@ -29,7 +29,7 @@ not providing this information, please do not be offended. I want to help, just need to prioritize my limited development time well. Feel free to re-open your issue, providing the requested information. -## Misconceptions About open source +## Misconceptions About Open Source - The fixes aren't fast enough! - The features aren't good enough! @@ -52,6 +52,6 @@ hobbiest-level skills and ... at best ... hobbiest level support. If you find yourself complaining, your expectations are incorrect! This is undoubtedly frustrating (and I have often shared in this frustration), but it is -nonetheless just "the way open source is". +nonetheless just the way open source is. Take it or leave it. Thank you for understanding! diff --git a/src/camera-manager/browse-media/camera.ts b/src/camera-manager/browse-media/camera.ts index eace0a73..4a207ab2 100644 --- a/src/camera-manager/browse-media/camera.ts +++ b/src/camera-manager/browse-media/camera.ts @@ -25,7 +25,7 @@ export class BrowseMediaCamera extends Camera { throw new CameraInitializationError(localize('error.no_camera_entity'), config); } this._entity = entity; - return this; + return await super.initialize(options); } public getEntity(): Entity | null { diff --git a/src/camera-manager/camera.ts b/src/camera-manager/camera.ts index b93c84d7..062536dc 100644 --- a/src/camera-manager/camera.ts +++ b/src/camera-manager/camera.ts @@ -34,10 +34,12 @@ export class Camera { } async initialize(options: CameraInitializationOptions): Promise { - options.stateWatcher.subscribe( - this._stateChangeHandler, - this._config.triggers.entities, - ); + if (this._capabilities?.has('trigger')) { + options.stateWatcher.subscribe( + this._stateChangeHandler, + this._config.triggers.entities, + ); + } this._onDestroy(() => options.stateWatcher.unsubscribe(this._stateChangeHandler)); return this; } diff --git a/src/camera-manager/frigate/camera.ts b/src/camera-manager/frigate/camera.ts index 28707947..9d94fefc 100644 --- a/src/camera-manager/frigate/camera.ts +++ b/src/camera-manager/frigate/camera.ts @@ -49,7 +49,11 @@ export class FrigateCamera extends Camera { public async initialize(options: FrigateCameraInitializationOptions): Promise { await this._initializeConfig(options.hass, options.entityRegistryManager); await this._initializeCapabilities(options.hass); - await this._subscribeToEvents(options.hass, options.frigateEventWatcher); + + if (this._capabilities?.has('trigger')) { + await this._subscribeToEvents(options.hass, options.frigateEventWatcher); + } + return await super.initialize(options); } @@ -142,6 +146,7 @@ export class FrigateCamera extends Camera { live: true, menu: true, substream: true, + trigger: true, ...(combinedPTZCapabilities && { ptz: combinedPTZCapabilities }), }, { diff --git a/src/camera-manager/generic/engine-generic.ts b/src/camera-manager/generic/engine-generic.ts index e756d67c..feb5b91b 100644 --- a/src/camera-manager/generic/engine-generic.ts +++ b/src/camera-manager/generic/engine-generic.ts @@ -70,6 +70,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine { seek: false, snapshots: false, substream: true, + trigger: true, ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined, }, { diff --git a/src/camera-manager/motioneye/engine-motioneye.ts b/src/camera-manager/motioneye/engine-motioneye.ts index 95f2eca0..47abd42b 100644 --- a/src/camera-manager/motioneye/engine-motioneye.ts +++ b/src/camera-manager/motioneye/engine-motioneye.ts @@ -90,6 +90,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine seek: false, snapshots: true, substream: true, + trigger: true, ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined, }, { diff --git a/src/camera-manager/reolink/engine-reolink.ts b/src/camera-manager/reolink/engine-reolink.ts index 06815717..6cd9bea1 100644 --- a/src/camera-manager/reolink/engine-reolink.ts +++ b/src/camera-manager/reolink/engine-reolink.ts @@ -144,6 +144,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine { seek: false, snapshots: false, substream: true, + trigger: true, ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined, }, { diff --git a/src/types.ts b/src/types.ts index ed03741d..6e74e57a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -106,6 +106,8 @@ export interface CapabilitiesRaw { ptz?: PTZCapabilities; menu?: boolean; + + trigger?: boolean; } export type CapabilityKey = keyof CapabilitiesRaw; @@ -120,6 +122,7 @@ export const capabilityKeys: readonly [CapabilityKey, ...CapabilityKey[]] = [ 'seek', 'snapshots', 'substream', + 'trigger', ] as const; export interface Icon { diff --git a/tests/camera-manager/camera.test.ts b/tests/camera-manager/camera.test.ts index f64a09de..488d395d 100644 --- a/tests/camera-manager/camera.test.ts +++ b/tests/camera-manager/camera.test.ts @@ -81,6 +81,9 @@ describe('Camera', () => { }, }), new GenericCameraManagerEngine(mock()), + { + capabilities: createCapabilities({ trigger: true }), + }, ); const stateWatcher = mock(); @@ -117,7 +120,10 @@ describe('Camera', () => { }, }), new GenericCameraManagerEngine(mock()), - { eventCallback: eventCallback }, + { + capabilities: createCapabilities({ trigger: true }), + eventCallback: eventCallback, + }, ); const stateWatcher = mock(); @@ -125,6 +131,8 @@ describe('Camera', () => { stateWatcher: stateWatcher, }); + expect(stateWatcher.subscribe).toBeCalled(); + const diff = { entityID: 'sensor.force_update', oldState: createStateEntity({ state: stateFrom }), @@ -138,6 +146,30 @@ describe('Camera', () => { }); }, ); + + it('should not trigger without trigger capability', async () => { + const eventCallback = vi.fn(); + const camera = new Camera( + createCameraConfig({ + id: 'camera_1', + triggers: { + entities: ['binary_sensor.foo'], + }, + }), + new GenericCameraManagerEngine(mock()), + { + capabilities: createCapabilities({ trigger: false }), + eventCallback: eventCallback, + }, + ); + + const stateWatcher = mock(); + await camera.initialize({ + stateWatcher: stateWatcher, + }); + + expect(stateWatcher.subscribe).not.toBeCalled(); + }); }); describe('should get proxy config', () => { diff --git a/tests/camera-manager/frigate/camera.test.ts b/tests/camera-manager/frigate/camera.test.ts index 81aadf01..90c8c410 100644 --- a/tests/camera-manager/frigate/camera.test.ts +++ b/tests/camera-manager/frigate/camera.test.ts @@ -162,6 +162,7 @@ describe('FrigateCamera', () => { expect(camera.getCapabilities()?.has('live')).toBeTruthy(); expect(camera.getCapabilities()?.has('snapshots')).toBeTruthy(); expect(camera.getCapabilities()?.has('recordings')).toBeTruthy(); + expect(camera.getCapabilities()?.has('trigger')).toBeTruthy(); expect(vi.mocked(getPTZInfo)).toBeCalled(); }); @@ -188,6 +189,7 @@ describe('FrigateCamera', () => { expect(camera.getCapabilities()?.has('live')).toBeTruthy(); expect(camera.getCapabilities()?.has('snapshots')).toBeFalsy(); expect(camera.getCapabilities()?.has('recordings')).toBeFalsy(); + expect(camera.getCapabilities()?.has('trigger')).toBeTruthy(); expect(vi.mocked(getPTZInfo)).not.toBeCalled(); }); @@ -344,6 +346,31 @@ describe('FrigateCamera', () => { expect(eventWatcher.subscribe).not.toBeCalled(); }); + it('should not subscribe without trigger capability', async () => { + const camera = new FrigateCamera( + createCameraConfig({ + frigate: { + client_id: 'CLIENT_ID', + camera_name: 'CAMERA', + }, + capabilities: { + disable: ['trigger'], + }, + }), + mock(), + ); + const hass = createHASS(); + + const eventWatcher = mock(); + await camera.initialize({ + hass: hass, + entityRegistryManager: mock(), + stateWatcher: mock(), + frigateEventWatcher: eventWatcher, + }); + expect(eventWatcher.subscribe).not.toBeCalled(); + }); + it('should not subscribe with no camera name', async () => { const camera = new FrigateCamera( createCameraConfig({ diff --git a/tests/camera-manager/generic/engine-generic.test.ts b/tests/camera-manager/generic/engine-generic.test.ts index 8b642153..ea24fc45 100644 --- a/tests/camera-manager/generic/engine-generic.test.ts +++ b/tests/camera-manager/generic/engine-generic.test.ts @@ -37,6 +37,7 @@ describe('GenericCameraManagerEngine', () => { expect(camera.getCapabilities()?.has('clips')).toBeFalsy(); expect(camera.getCapabilities()?.has('recordings')).toBeFalsy(); expect(camera.getCapabilities()?.has('snapshots')).toBeFalsy(); + expect(camera.getCapabilities()?.has('trigger')).toBeTruthy(); }); it('should generate default event query', () => { diff --git a/tests/camera-manager/reolink/engine-reolink.test.ts b/tests/camera-manager/reolink/engine-reolink.test.ts index d03a1669..0046d2fa 100644 --- a/tests/camera-manager/reolink/engine-reolink.test.ts +++ b/tests/camera-manager/reolink/engine-reolink.test.ts @@ -248,6 +248,7 @@ describe('ReolinkCameraManagerEngine', () => { seek: false, snapshots: false, substream: true, + trigger: true, }); });