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. |
|
| `seek` | Clips can be seeked / scrubbed by the timeline. |
|
||||||
| `snapshots` | Snapshots can be fetched from the camera. |
|
| `snapshots` | Snapshots can be fetched from the camera. |
|
||||||
| `substream` | The camera can be used as a substream on another camera. |
|
| `substream` | The camera can be used as a substream on another camera. |
|
||||||
|
| `trigger` | The camera can be triggered. |
|
||||||
|
|
||||||
## `cast`
|
## `cast`
|
||||||
|
|
||||||
@@ -415,6 +416,22 @@ cameras:
|
|||||||
dynamic: true
|
dynamic: true
|
||||||
ssl_verification: auto
|
ssl_verification: auto
|
||||||
ssl_ciphers: 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:
|
cameras_global:
|
||||||
triggers:
|
triggers:
|
||||||
motion: false
|
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
|
need to prioritize my limited development time well. Feel free to re-open your
|
||||||
issue, providing the requested information.
|
issue, providing the requested information.
|
||||||
|
|
||||||
## Misconceptions About open source
|
## Misconceptions About Open Source
|
||||||
|
|
||||||
- The fixes aren't fast enough!
|
- The fixes aren't fast enough!
|
||||||
- The features aren't good 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
|
If you find yourself complaining, your expectations are incorrect! This is
|
||||||
undoubtedly frustrating (and I have often shared in this frustration), but it 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!
|
Thank you for understanding!
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export class BrowseMediaCamera extends Camera {
|
|||||||
throw new CameraInitializationError(localize('error.no_camera_entity'), config);
|
throw new CameraInitializationError(localize('error.no_camera_entity'), config);
|
||||||
}
|
}
|
||||||
this._entity = entity;
|
this._entity = entity;
|
||||||
return this;
|
return await super.initialize(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEntity(): Entity | null {
|
public getEntity(): Entity | null {
|
||||||
|
|||||||
@@ -34,10 +34,12 @@ export class Camera {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async initialize(options: CameraInitializationOptions): Promise<Camera> {
|
async initialize(options: CameraInitializationOptions): Promise<Camera> {
|
||||||
options.stateWatcher.subscribe(
|
if (this._capabilities?.has('trigger')) {
|
||||||
this._stateChangeHandler,
|
options.stateWatcher.subscribe(
|
||||||
this._config.triggers.entities,
|
this._stateChangeHandler,
|
||||||
);
|
this._config.triggers.entities,
|
||||||
|
);
|
||||||
|
}
|
||||||
this._onDestroy(() => options.stateWatcher.unsubscribe(this._stateChangeHandler));
|
this._onDestroy(() => options.stateWatcher.unsubscribe(this._stateChangeHandler));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,7 +49,11 @@ export class FrigateCamera extends Camera {
|
|||||||
public async initialize(options: FrigateCameraInitializationOptions): Promise<Camera> {
|
public async initialize(options: FrigateCameraInitializationOptions): Promise<Camera> {
|
||||||
await this._initializeConfig(options.hass, options.entityRegistryManager);
|
await this._initializeConfig(options.hass, options.entityRegistryManager);
|
||||||
await this._initializeCapabilities(options.hass);
|
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);
|
return await super.initialize(options);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +146,7 @@ export class FrigateCamera extends Camera {
|
|||||||
live: true,
|
live: true,
|
||||||
menu: true,
|
menu: true,
|
||||||
substream: true,
|
substream: true,
|
||||||
|
trigger: true,
|
||||||
...(combinedPTZCapabilities && { ptz: combinedPTZCapabilities }),
|
...(combinedPTZCapabilities && { ptz: combinedPTZCapabilities }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
|||||||
seek: false,
|
seek: false,
|
||||||
snapshots: false,
|
snapshots: false,
|
||||||
substream: true,
|
substream: true,
|
||||||
|
trigger: true,
|
||||||
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
|
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
|||||||
seek: false,
|
seek: false,
|
||||||
snapshots: true,
|
snapshots: true,
|
||||||
substream: true,
|
substream: true,
|
||||||
|
trigger: true,
|
||||||
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
|
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -144,6 +144,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
|||||||
seek: false,
|
seek: false,
|
||||||
snapshots: false,
|
snapshots: false,
|
||||||
substream: true,
|
substream: true,
|
||||||
|
trigger: true,
|
||||||
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
|
ptz: getPTZCapabilitiesFromCameraConfig(cameraConfig) ?? undefined,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -106,6 +106,8 @@ export interface CapabilitiesRaw {
|
|||||||
ptz?: PTZCapabilities;
|
ptz?: PTZCapabilities;
|
||||||
|
|
||||||
menu?: boolean;
|
menu?: boolean;
|
||||||
|
|
||||||
|
trigger?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CapabilityKey = keyof CapabilitiesRaw;
|
export type CapabilityKey = keyof CapabilitiesRaw;
|
||||||
@@ -120,6 +122,7 @@ export const capabilityKeys: readonly [CapabilityKey, ...CapabilityKey[]] = [
|
|||||||
'seek',
|
'seek',
|
||||||
'snapshots',
|
'snapshots',
|
||||||
'substream',
|
'substream',
|
||||||
|
'trigger',
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export interface Icon {
|
export interface Icon {
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ describe('Camera', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||||
|
{
|
||||||
|
capabilities: createCapabilities({ trigger: true }),
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const stateWatcher = mock<StateWatcherSubscriptionInterface>();
|
const stateWatcher = mock<StateWatcherSubscriptionInterface>();
|
||||||
@@ -117,7 +120,10 @@ describe('Camera', () => {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
|
||||||
{ eventCallback: eventCallback },
|
{
|
||||||
|
capabilities: createCapabilities({ trigger: true }),
|
||||||
|
eventCallback: eventCallback,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
const stateWatcher = mock<StateWatcherSubscriptionInterface>();
|
const stateWatcher = mock<StateWatcherSubscriptionInterface>();
|
||||||
@@ -125,6 +131,8 @@ describe('Camera', () => {
|
|||||||
stateWatcher: stateWatcher,
|
stateWatcher: stateWatcher,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
expect(stateWatcher.subscribe).toBeCalled();
|
||||||
|
|
||||||
const diff = {
|
const diff = {
|
||||||
entityID: 'sensor.force_update',
|
entityID: 'sensor.force_update',
|
||||||
oldState: createStateEntity({ state: stateFrom }),
|
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', () => {
|
describe('should get proxy config', () => {
|
||||||
|
|||||||
@@ -162,6 +162,7 @@ describe('FrigateCamera', () => {
|
|||||||
expect(camera.getCapabilities()?.has('live')).toBeTruthy();
|
expect(camera.getCapabilities()?.has('live')).toBeTruthy();
|
||||||
expect(camera.getCapabilities()?.has('snapshots')).toBeTruthy();
|
expect(camera.getCapabilities()?.has('snapshots')).toBeTruthy();
|
||||||
expect(camera.getCapabilities()?.has('recordings')).toBeTruthy();
|
expect(camera.getCapabilities()?.has('recordings')).toBeTruthy();
|
||||||
|
expect(camera.getCapabilities()?.has('trigger')).toBeTruthy();
|
||||||
expect(vi.mocked(getPTZInfo)).toBeCalled();
|
expect(vi.mocked(getPTZInfo)).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -188,6 +189,7 @@ describe('FrigateCamera', () => {
|
|||||||
expect(camera.getCapabilities()?.has('live')).toBeTruthy();
|
expect(camera.getCapabilities()?.has('live')).toBeTruthy();
|
||||||
expect(camera.getCapabilities()?.has('snapshots')).toBeFalsy();
|
expect(camera.getCapabilities()?.has('snapshots')).toBeFalsy();
|
||||||
expect(camera.getCapabilities()?.has('recordings')).toBeFalsy();
|
expect(camera.getCapabilities()?.has('recordings')).toBeFalsy();
|
||||||
|
expect(camera.getCapabilities()?.has('trigger')).toBeTruthy();
|
||||||
expect(vi.mocked(getPTZInfo)).not.toBeCalled();
|
expect(vi.mocked(getPTZInfo)).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -344,6 +346,31 @@ describe('FrigateCamera', () => {
|
|||||||
expect(eventWatcher.subscribe).not.toBeCalled();
|
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 () => {
|
it('should not subscribe with no camera name', async () => {
|
||||||
const camera = new FrigateCamera(
|
const camera = new FrigateCamera(
|
||||||
createCameraConfig({
|
createCameraConfig({
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ describe('GenericCameraManagerEngine', () => {
|
|||||||
expect(camera.getCapabilities()?.has('clips')).toBeFalsy();
|
expect(camera.getCapabilities()?.has('clips')).toBeFalsy();
|
||||||
expect(camera.getCapabilities()?.has('recordings')).toBeFalsy();
|
expect(camera.getCapabilities()?.has('recordings')).toBeFalsy();
|
||||||
expect(camera.getCapabilities()?.has('snapshots')).toBeFalsy();
|
expect(camera.getCapabilities()?.has('snapshots')).toBeFalsy();
|
||||||
|
expect(camera.getCapabilities()?.has('trigger')).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate default event query', () => {
|
it('should generate default event query', () => {
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ describe('ReolinkCameraManagerEngine', () => {
|
|||||||
seek: false,
|
seek: false,
|
||||||
snapshots: false,
|
snapshots: false,
|
||||||
substream: true,
|
substream: true,
|
||||||
|
trigger: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user