fix: Substream only cameras should not be triggerable (#1848)
- Closes #1846
This commit is contained in:
@@ -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
|
||||
|
||||
+2
-2
@@ -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!
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -34,10 +34,12 @@ export class Camera {
|
||||
}
|
||||
|
||||
async initialize(options: CameraInitializationOptions): Promise<Camera> {
|
||||
if (this._capabilities?.has('trigger')) {
|
||||
options.stateWatcher.subscribe(
|
||||
this._stateChangeHandler,
|
||||
this._config.triggers.entities,
|
||||
);
|
||||
}
|
||||
this._onDestroy(() => options.stateWatcher.unsubscribe(this._stateChangeHandler));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
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 }),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -70,6 +70,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
seek: false,
|
||||
snapshots: false,
|
||||
substream: true,
|
||||
trigger: true,
|
||||
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -90,6 +90,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
seek: false,
|
||||
snapshots: true,
|
||||
substream: true,
|
||||
trigger: true,
|
||||
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -144,6 +144,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
seek: false,
|
||||
snapshots: false,
|
||||
substream: true,
|
||||
trigger: true,
|
||||
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -81,6 +81,9 @@ describe('Camera', () => {
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
{
|
||||
capabilities: createCapabilities({ trigger: true }),
|
||||
},
|
||||
);
|
||||
|
||||
const stateWatcher = mock<StateWatcherSubscriptionInterface>();
|
||||
@@ -117,7 +120,10 @@ describe('Camera', () => {
|
||||
},
|
||||
}),
|
||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||
{ eventCallback: eventCallback },
|
||||
{
|
||||
capabilities: createCapabilities({ trigger: true }),
|
||||
eventCallback: eventCallback,
|
||||
},
|
||||
);
|
||||
|
||||
const stateWatcher = mock<StateWatcherSubscriptionInterface>();
|
||||
@@ -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<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', () => {
|
||||
|
||||
@@ -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<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({
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -248,6 +248,7 @@ describe('ReolinkCameraManagerEngine', () => {
|
||||
seek: false,
|
||||
snapshots: false,
|
||||
substream: true,
|
||||
trigger: true,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user