Skip to content

Commit

Permalink
fix: Substream only cammeras should not be triggerable
Browse files Browse the repository at this point in the history
  • Loading branch information
dermotduffy committed Jan 21, 2025
1 parent b1aa782 commit 287d49e
Show file tree
Hide file tree
Showing 13 changed files with 100 additions and 9 deletions.
17 changes: 17 additions & 0 deletions docs/configuration/cameras/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/support.md
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Expand All @@ -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!
2 changes: 1 addition & 1 deletion src/camera-manager/browse-media/camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
10 changes: 6 additions & 4 deletions src/camera-manager/camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ export class Camera {
}

async initialize(options: CameraInitializationOptions): Promise<Camera> {
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;
}
Expand Down
7 changes: 6 additions & 1 deletion src/camera-manager/frigate/camera.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,11 @@ export class FrigateCamera extends Camera {
public async initialize(options: FrigateCameraInitializationOptions): Promise<Camera> {
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);
}

Expand Down Expand Up @@ -142,6 +146,7 @@ export class FrigateCamera extends Camera {
live: true,
menu: true,
substream: true,
trigger: true,
...(combinedPTZCapabilities && { ptz: combinedPTZCapabilities }),
},
{
Expand Down
1 change: 1 addition & 0 deletions src/camera-manager/generic/engine-generic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
seek: false,
snapshots: false,
substream: true,
trigger: true,
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
},
{
Expand Down
1 change: 1 addition & 0 deletions src/camera-manager/motioneye/engine-motioneye.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
seek: false,
snapshots: true,
substream: true,
trigger: true,
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
},
{
Expand Down
1 change: 1 addition & 0 deletions src/camera-manager/reolink/engine-reolink.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
seek: false,
snapshots: false,
substream: true,
trigger: true,
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
},
{
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ export interface CapabilitiesRaw {
ptz?: PTZCapabilities;

menu?: boolean;

trigger?: boolean;
}

export type CapabilityKey = keyof CapabilitiesRaw;
Expand All @@ -120,6 +122,7 @@ export const capabilityKeys: readonly [CapabilityKey, ...CapabilityKey[]] = [
'seek',
'snapshots',
'substream',
'trigger',
] as const;

export interface Icon {
Expand Down
34 changes: 33 additions & 1 deletion tests/camera-manager/camera.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ describe('Camera', () => {
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
{
capabilities: createCapabilities({ trigger: true }),
},
);

const stateWatcher = mock<StateWatcherSubscriptionInterface>();
Expand Down Expand Up @@ -117,14 +120,19 @@ describe('Camera', () => {
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
{ eventCallback: eventCallback },
{
capabilities: createCapabilities({ trigger: true }),
eventCallback: eventCallback,
},
);

const stateWatcher = mock<StateWatcherSubscriptionInterface>();
await camera.initialize({
stateWatcher: stateWatcher,
});

expect(stateWatcher.subscribe).toBeCalled();

const diff = {
entityID: 'sensor.force_update',
oldState: createStateEntity({ state: stateFrom }),
Expand All @@ -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<StateWatcherSubscriptionInterface>()),
{
capabilities: createCapabilities({ trigger: false }),
eventCallback: eventCallback,
},
);

const stateWatcher = mock<StateWatcherSubscriptionInterface>();
await camera.initialize({
stateWatcher: stateWatcher,
});

expect(stateWatcher.subscribe).not.toBeCalled();
});
});

describe('should get proxy config', () => {
Expand Down
27 changes: 27 additions & 0 deletions tests/camera-manager/frigate/camera.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand All @@ -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();
});

Expand Down Expand Up @@ -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<CameraManagerEngine>(),
);
const hass = createHASS();

const eventWatcher = mock<FrigateEventWatcher>();
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
frigateEventWatcher: eventWatcher,
});
expect(eventWatcher.subscribe).not.toBeCalled();
});

it('should not subscribe with no camera name', async () => {
const camera = new FrigateCamera(
createCameraConfig({
Expand Down
1 change: 1 addition & 0 deletions tests/camera-manager/generic/engine-generic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
1 change: 1 addition & 0 deletions tests/camera-manager/reolink/engine-reolink.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ describe('ReolinkCameraManagerEngine', () => {
seek: false,
snapshots: false,
substream: true,
trigger: true,
});
});

Expand Down

0 comments on commit 287d49e

Please sign in to comment.