feat: Add out of the box support for Reolink PTZ (#1982)
- Closes: #1964
This commit is contained in:
@@ -3,8 +3,10 @@ import { mock } from 'vitest-mock-extended';
|
||||
import { BrowseMediaCamera } from '../../../src/camera-manager/browse-media/camera';
|
||||
import { CameraManagerEngine } from '../../../src/camera-manager/engine';
|
||||
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { EntityRegistryManager } from '../../../src/utils/ha/registry/entity';
|
||||
import { Entity } from '../../../src/utils/ha/registry/entity/types';
|
||||
import {
|
||||
Entity,
|
||||
EntityRegistryManager,
|
||||
} from '../../../src/utils/ha/registry/entity/types';
|
||||
import { createCameraConfig, createHASS } from '../../test-utils';
|
||||
|
||||
describe('BrowseMediaCamera', () => {
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { Capabilities } from '../../src/camera-manager/capabilities';
|
||||
import { CapabilityKey, PTZCapabilities, capabilityKeys } from '../../src/types';
|
||||
import {
|
||||
CapabilityKey,
|
||||
PTZCapabilities,
|
||||
PTZMovementType,
|
||||
capabilityKeys,
|
||||
} from '../../src/types';
|
||||
|
||||
describe('Capabilities', () => {
|
||||
it('default capabilities', () => {
|
||||
@@ -73,7 +78,7 @@ describe('Capabilities', () => {
|
||||
|
||||
it('when set', () => {
|
||||
const ptz: PTZCapabilities = {
|
||||
left: ['continuous' as const],
|
||||
left: [PTZMovementType.Continuous],
|
||||
presets: ['1', '2'],
|
||||
};
|
||||
const capabilities = new Capabilities({
|
||||
|
||||
@@ -8,10 +8,7 @@ import { ReolinkCameraManagerEngine } from '../../src/camera-manager/reolink/eng
|
||||
import { Engine } from '../../src/camera-manager/types.js';
|
||||
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
||||
import { CardWideConfig } from '../../src/config/schema/types.js';
|
||||
import {
|
||||
createEntityRegistryCache,
|
||||
EntityRegistryManager,
|
||||
} from '../../src/utils/ha/registry/entity/index.js';
|
||||
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity/types.js';
|
||||
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media';
|
||||
import {
|
||||
createCameraConfig,
|
||||
@@ -19,6 +16,7 @@ import {
|
||||
createRegistryEntity,
|
||||
createStateEntity,
|
||||
} from '../test-utils';
|
||||
import { EntityRegistryManagerMock } from '../utils/ha/registry/entity/mock.js';
|
||||
|
||||
vi.mock('../../src/utils/ha/entity-registry');
|
||||
vi.mock('../../src/utils/ha/entity-registry/cache');
|
||||
@@ -28,8 +26,7 @@ const createFactory = (options?: {
|
||||
cardWideConfig?: CardWideConfig;
|
||||
}): CameraManagerEngineFactory => {
|
||||
return new CameraManagerEngineFactory(
|
||||
options?.entityRegistryManager ??
|
||||
new EntityRegistryManager(createEntityRegistryCache()),
|
||||
options?.entityRegistryManager ?? new EntityRegistryManagerMock(),
|
||||
);
|
||||
};
|
||||
|
||||
@@ -44,15 +41,9 @@ describe('getEngineForCamera()', () => {
|
||||
|
||||
it('from auto detection', async () => {
|
||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||
const entityRegistryManager = new EntityRegistryManager(
|
||||
createEntityRegistryCache(),
|
||||
);
|
||||
|
||||
entityRegistryManager.getEntity = vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'frigate' }),
|
||||
);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'frigate' }),
|
||||
]);
|
||||
|
||||
expect(
|
||||
await createFactory({
|
||||
@@ -81,15 +72,9 @@ describe('getEngineForCamera()', () => {
|
||||
|
||||
it('from auto detection', async () => {
|
||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||
const entityRegistryManager = new EntityRegistryManager(
|
||||
createEntityRegistryCache(),
|
||||
);
|
||||
|
||||
entityRegistryManager.getEntity = vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'motioneye' }),
|
||||
);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'motioneye' }),
|
||||
]);
|
||||
|
||||
expect(
|
||||
await createFactory({
|
||||
@@ -109,15 +94,9 @@ describe('getEngineForCamera()', () => {
|
||||
|
||||
it('from auto detection', async () => {
|
||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||
const entityRegistryManager = new EntityRegistryManager(
|
||||
createEntityRegistryCache(),
|
||||
);
|
||||
|
||||
entityRegistryManager.getEntity = vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'reolink' }),
|
||||
);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'reolink' }),
|
||||
]);
|
||||
|
||||
expect(
|
||||
await createFactory({
|
||||
@@ -137,15 +116,9 @@ describe('getEngineForCamera()', () => {
|
||||
|
||||
it('from auto detection', async () => {
|
||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||
const entityRegistryManager = new EntityRegistryManager(
|
||||
createEntityRegistryCache(),
|
||||
);
|
||||
|
||||
entityRegistryManager.getEntity = vi
|
||||
.fn()
|
||||
.mockResolvedValue(
|
||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'generic' }),
|
||||
);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity({ entity_id: 'camera.foo', platform: 'generic' }),
|
||||
]);
|
||||
|
||||
expect(
|
||||
await createFactory({
|
||||
@@ -159,11 +132,7 @@ describe('getEngineForCamera()', () => {
|
||||
engine: 'auto',
|
||||
webrtc_card: { entity: 'camera.foo' },
|
||||
});
|
||||
const entityRegistryManager = new EntityRegistryManager(
|
||||
createEntityRegistryCache(),
|
||||
);
|
||||
|
||||
entityRegistryManager.getEntity = vi.fn().mockResolvedValue(null);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock();
|
||||
|
||||
expect(
|
||||
await createFactory({
|
||||
@@ -182,10 +151,7 @@ describe('getEngineForCamera()', () => {
|
||||
engine: 'auto',
|
||||
webrtc_card: { entity: 'camera.foo' },
|
||||
});
|
||||
const entityRegistryManager = new EntityRegistryManager(
|
||||
createEntityRegistryCache(),
|
||||
);
|
||||
entityRegistryManager.getEntity = vi.fn().mockResolvedValue(null);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock();
|
||||
|
||||
expect(
|
||||
async () =>
|
||||
@@ -225,9 +191,8 @@ describe('getEngineForCamera()', () => {
|
||||
|
||||
it('should throw error on invalid entity', async () => {
|
||||
const config = createCameraConfig({ engine: 'auto', camera_entity: 'camera.foo' });
|
||||
const entityRegistryManager = new EntityRegistryManager(createEntityRegistryCache());
|
||||
|
||||
entityRegistryManager.getEntity = vi.fn().mockRejectedValue(new Error());
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
entityRegistryManager.getEntity.mockRejectedValue(new Error());
|
||||
|
||||
await expect(
|
||||
createFactory({
|
||||
|
||||
@@ -5,11 +5,16 @@ import { FrigateCamera } from '../../../src/camera-manager/frigate/camera';
|
||||
import { FrigateEventWatcher } from '../../../src/camera-manager/frigate/event-watcher';
|
||||
import { getPTZInfo } from '../../../src/camera-manager/frigate/requests';
|
||||
import { FrigateEventChange } from '../../../src/camera-manager/frigate/types';
|
||||
import { ActionsExecutor } from '../../../src/card-controller/actions/types';
|
||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { PTZAction } from '../../../src/config/schema/actions/custom/ptz';
|
||||
import { CameraTriggerEventType } from '../../../src/config/schema/cameras';
|
||||
import { EntityRegistryManager } from '../../../src/utils/ha/registry/entity';
|
||||
import { Entity } from '../../../src/utils/ha/registry/entity/types';
|
||||
import {
|
||||
Entity,
|
||||
EntityRegistryManager,
|
||||
} from '../../../src/utils/ha/registry/entity/types';
|
||||
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
|
||||
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
|
||||
|
||||
vi.mock('../../../src/camera-manager/frigate/requests');
|
||||
|
||||
@@ -53,8 +58,7 @@ describe('FrigateCamera', () => {
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
);
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
entityRegistryManager.getEntity.mockResolvedValue(null);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock();
|
||||
|
||||
expect(
|
||||
async () =>
|
||||
@@ -74,12 +78,13 @@ describe('FrigateCamera', () => {
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
);
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
const entity = createRegistryEntity({
|
||||
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:camera:fnt_dr',
|
||||
platform: 'frigate',
|
||||
});
|
||||
entityRegistryManager.getEntity.mockResolvedValue(entity);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity({
|
||||
entity_id: 'camera.front_door',
|
||||
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:camera:fnt_dr',
|
||||
platform: 'frigate',
|
||||
}),
|
||||
]);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
@@ -97,12 +102,13 @@ describe('FrigateCamera', () => {
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
);
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
const entity = createRegistryEntity({
|
||||
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:WRONG:fnt_dr',
|
||||
platform: 'frigate',
|
||||
});
|
||||
entityRegistryManager.getEntity.mockResolvedValue(entity);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity({
|
||||
entity_id: 'camera.front_door',
|
||||
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:WRONG:fnt_dr',
|
||||
platform: 'frigate',
|
||||
}),
|
||||
]);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
@@ -120,12 +126,13 @@ describe('FrigateCamera', () => {
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
);
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
const entity = createRegistryEntity({
|
||||
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:camera:fnt_dr',
|
||||
platform: 'something_else',
|
||||
});
|
||||
entityRegistryManager.getEntity.mockResolvedValue(entity);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity({
|
||||
entity_id: 'camera.front_door',
|
||||
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:camera:fnt_dr',
|
||||
platform: 'something_else',
|
||||
}),
|
||||
]);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
@@ -656,6 +663,7 @@ describe('FrigateCamera', () => {
|
||||
describe('should handle triggers', () => {
|
||||
const cameraEntity: Partial<Entity> = {
|
||||
config_entry_id: 'config_entry_id',
|
||||
entity_id: 'camera.front_door',
|
||||
};
|
||||
|
||||
const occupancySensorEntityAll: Partial<Entity> = {
|
||||
@@ -674,11 +682,8 @@ describe('FrigateCamera', () => {
|
||||
|
||||
describe('should detect motion sensor', () => {
|
||||
it('without a camera name', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
entityRegistryManager.getEntity.mockResolvedValue(
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity(cameraEntity),
|
||||
);
|
||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
||||
createRegistryEntity(motionSensorEntity),
|
||||
]);
|
||||
const camera = new FrigateCamera(
|
||||
@@ -702,11 +707,8 @@ describe('FrigateCamera', () => {
|
||||
});
|
||||
|
||||
it('with camera entity and name', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
entityRegistryManager.getEntity.mockResolvedValue(
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity(cameraEntity),
|
||||
);
|
||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
||||
createRegistryEntity(motionSensorEntity),
|
||||
]);
|
||||
const camera = new FrigateCamera(
|
||||
@@ -733,43 +735,7 @@ describe('FrigateCamera', () => {
|
||||
expect(camera.getConfig().triggers.entities).toEqual(['binary_sensor.foo']);
|
||||
});
|
||||
|
||||
it('with matching entity', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
entityRegistryManager.getEntity.mockResolvedValue(
|
||||
createRegistryEntity(cameraEntity),
|
||||
);
|
||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
||||
createRegistryEntity(motionSensorEntity),
|
||||
]);
|
||||
const camera = new FrigateCamera(
|
||||
createCameraConfig({
|
||||
frigate: {
|
||||
camera_name: 'front_door',
|
||||
},
|
||||
triggers: {
|
||||
motion: true,
|
||||
},
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
);
|
||||
|
||||
const hass = createHASS();
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
});
|
||||
|
||||
expect(camera.getConfig().triggers.entities).toEqual(['binary_sensor.foo']);
|
||||
});
|
||||
|
||||
it('without matching entity', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
entityRegistryManager.getEntity.mockResolvedValue(
|
||||
createRegistryEntity(cameraEntity),
|
||||
);
|
||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([]);
|
||||
const camera = new FrigateCamera(
|
||||
createCameraConfig({
|
||||
frigate: {
|
||||
@@ -784,7 +750,7 @@ describe('FrigateCamera', () => {
|
||||
const hass = createHASS();
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
entityRegistryManager: new EntityRegistryManagerMock(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
});
|
||||
@@ -795,11 +761,8 @@ describe('FrigateCamera', () => {
|
||||
|
||||
describe('should detect occupancy sensor', () => {
|
||||
it('without a camera name', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
entityRegistryManager.getEntity.mockResolvedValue(
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity(cameraEntity),
|
||||
);
|
||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
||||
createRegistryEntity(occupancySensorEntityAll),
|
||||
]);
|
||||
const camera = new FrigateCamera(
|
||||
@@ -822,11 +785,8 @@ describe('FrigateCamera', () => {
|
||||
});
|
||||
|
||||
it('without a camera name but with occupancy trigger', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
entityRegistryManager.getEntity.mockResolvedValue(
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity(cameraEntity),
|
||||
);
|
||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
||||
createRegistryEntity(occupancySensorEntityAll),
|
||||
]);
|
||||
const camera = new FrigateCamera(
|
||||
@@ -848,42 +808,7 @@ describe('FrigateCamera', () => {
|
||||
expect(camera.getConfig().triggers.entities).toEqual([]);
|
||||
});
|
||||
|
||||
it('with matching entity', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
entityRegistryManager.getEntity.mockResolvedValue(
|
||||
createRegistryEntity(cameraEntity),
|
||||
);
|
||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
||||
createRegistryEntity(occupancySensorEntityAll),
|
||||
]);
|
||||
const camera = new FrigateCamera(
|
||||
createCameraConfig({
|
||||
frigate: {
|
||||
camera_name: 'front_door',
|
||||
},
|
||||
triggers: {
|
||||
occupancy: true,
|
||||
},
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
);
|
||||
const hass = createHASS();
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
});
|
||||
|
||||
expect(camera.getConfig().triggers.entities).toEqual(['binary_sensor.foo']);
|
||||
});
|
||||
|
||||
it('without matching entity', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
entityRegistryManager.getEntity.mockResolvedValue(
|
||||
createRegistryEntity(cameraEntity),
|
||||
);
|
||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([]);
|
||||
const camera = new FrigateCamera(
|
||||
createCameraConfig({
|
||||
frigate: {
|
||||
@@ -898,7 +823,7 @@ describe('FrigateCamera', () => {
|
||||
const hass = createHASS();
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
entityRegistryManager: new EntityRegistryManagerMock(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
});
|
||||
@@ -907,11 +832,8 @@ describe('FrigateCamera', () => {
|
||||
});
|
||||
|
||||
it('with zones', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
entityRegistryManager.getEntity.mockResolvedValue(
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity(cameraEntity),
|
||||
);
|
||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
||||
createRegistryEntity({
|
||||
...occupancySensorEntityAll,
|
||||
unique_id: '8c4e19d258359e82bc0cf9d47b021c46:occupancy_sensor:zone_all',
|
||||
@@ -919,6 +841,7 @@ describe('FrigateCamera', () => {
|
||||
]);
|
||||
const camera = new FrigateCamera(
|
||||
createCameraConfig({
|
||||
camera_entity: 'camera.front_door',
|
||||
frigate: {
|
||||
camera_name: 'front_door',
|
||||
zones: ['zone'],
|
||||
@@ -941,19 +864,18 @@ describe('FrigateCamera', () => {
|
||||
});
|
||||
|
||||
it('with labels', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
entityRegistryManager.getEntity.mockResolvedValue(
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity(cameraEntity),
|
||||
);
|
||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
||||
createRegistryEntity({
|
||||
...occupancySensorEntityAll,
|
||||
unique_id:
|
||||
'8c4e19d258359e82bc0cf9d47b021c46:occupancy_sensor:front_door_car',
|
||||
}),
|
||||
]);
|
||||
|
||||
const camera = new FrigateCamera(
|
||||
createCameraConfig({
|
||||
camera_entity: 'camera.front_door',
|
||||
frigate: {
|
||||
camera_name: 'front_door',
|
||||
labels: ['car'],
|
||||
@@ -976,74 +898,194 @@ describe('FrigateCamera', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should filter entities with correct function', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
entityRegistryManager.getEntity.mockResolvedValue(
|
||||
createRegistryEntity(cameraEntity),
|
||||
);
|
||||
entityRegistryManager.getMatchingEntities.mockResolvedValue([
|
||||
createRegistryEntity({
|
||||
...occupancySensorEntityAll,
|
||||
}),
|
||||
]);
|
||||
const camera = new FrigateCamera(
|
||||
createCameraConfig({
|
||||
camera_entity: 'camera.foo',
|
||||
triggers: {
|
||||
occupancy: true,
|
||||
},
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
);
|
||||
const hass = createHASS();
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
describe('should execute PTZ action', () => {
|
||||
it('should ignore preset action without a preset', async () => {
|
||||
const camera = new FrigateCamera(
|
||||
createCameraConfig(),
|
||||
mock<CameraManagerEngine>(),
|
||||
);
|
||||
|
||||
const hass = createHASS();
|
||||
await camera.initialize({
|
||||
hass: hass,
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
});
|
||||
|
||||
const executor = mock<ActionsExecutor>();
|
||||
await camera.executePTZAction(executor, 'preset');
|
||||
|
||||
expect(executor.executeActions).not.toBeCalled();
|
||||
});
|
||||
|
||||
const filterFunc = entityRegistryManager.getMatchingEntities.mock.calls[0][1];
|
||||
|
||||
expect(
|
||||
filterFunc(
|
||||
createRegistryEntity({
|
||||
config_entry_id: cameraEntity.config_entry_id,
|
||||
disabled_by: '',
|
||||
entity_id: 'binary_sensor.foo',
|
||||
it('should ignore actions with configured action', async () => {
|
||||
const camera = new FrigateCamera(
|
||||
createCameraConfig({
|
||||
camera_entity: 'camera.office_frigate',
|
||||
ptz: {
|
||||
actions_left_start: {
|
||||
action: 'perform-action',
|
||||
perform_action: 'button.press',
|
||||
target: {
|
||||
entity_id: 'button.foo',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toBeTruthy();
|
||||
mock<CameraManagerEngine>(),
|
||||
);
|
||||
|
||||
expect(
|
||||
filterFunc(
|
||||
createRegistryEntity({
|
||||
config_entry_id: cameraEntity.config_entry_id,
|
||||
disabled_by: 'user',
|
||||
entity_id: 'binary_sensor.foo',
|
||||
}),
|
||||
),
|
||||
).toBeFalsy();
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([
|
||||
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
});
|
||||
|
||||
expect(
|
||||
filterFunc(
|
||||
createRegistryEntity({
|
||||
config_entry_id: cameraEntity.config_entry_id,
|
||||
disabled_by: '',
|
||||
entity_id: 'camera.is_not_a_binary_sensor',
|
||||
}),
|
||||
),
|
||||
).toBeFalsy();
|
||||
const executor = mock<ActionsExecutor>();
|
||||
await camera.executePTZAction(executor, 'left', { phase: 'start' });
|
||||
|
||||
expect(
|
||||
filterFunc(
|
||||
createRegistryEntity({
|
||||
config_entry_id: 'not_a_matching_config_entry_id',
|
||||
disabled_by: '',
|
||||
entity_id: 'binary_sensor.foo',
|
||||
expect(executor.executeActions).toBeCalledTimes(1);
|
||||
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||
actions: {
|
||||
action: 'perform-action',
|
||||
perform_action: 'button.press',
|
||||
target: {
|
||||
entity_id: 'button.foo',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['left' as const],
|
||||
['right' as const],
|
||||
['up' as const],
|
||||
['down' as const],
|
||||
])('should execute action %s', async (action: PTZAction) => {
|
||||
const camera = new FrigateCamera(
|
||||
createCameraConfig({
|
||||
camera_entity: 'camera.office_frigate',
|
||||
}),
|
||||
),
|
||||
).toBeFalsy();
|
||||
mock<CameraManagerEngine>(),
|
||||
);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([
|
||||
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
});
|
||||
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
await camera.executePTZAction(executor, action, { phase: 'start' });
|
||||
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||
actions: {
|
||||
action: 'perform-action',
|
||||
data: {
|
||||
action: 'move',
|
||||
argument: action,
|
||||
},
|
||||
perform_action: 'frigate.ptz',
|
||||
target: {
|
||||
entity_id: 'camera.office_frigate',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await camera.executePTZAction(executor, action, { phase: 'stop' });
|
||||
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||
actions: {
|
||||
action: 'perform-action',
|
||||
data: {
|
||||
action: 'stop',
|
||||
},
|
||||
perform_action: 'frigate.ptz',
|
||||
target: {
|
||||
entity_id: 'camera.office_frigate',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it.each([
|
||||
['zoom_in' as const, 'in' as const],
|
||||
['zoom_out' as const, 'out' as const],
|
||||
])('should execute action %s', async (action: PTZAction, zoom: 'in' | 'out') => {
|
||||
const camera = new FrigateCamera(
|
||||
createCameraConfig({
|
||||
camera_entity: 'camera.office_frigate',
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([
|
||||
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
});
|
||||
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
await camera.executePTZAction(executor, action, { phase: 'start' });
|
||||
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||
actions: {
|
||||
action: 'perform-action',
|
||||
data: {
|
||||
action: 'zoom',
|
||||
argument: zoom,
|
||||
},
|
||||
perform_action: 'frigate.ptz',
|
||||
target: {
|
||||
entity_id: 'camera.office_frigate',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute preset', async () => {
|
||||
const camera = new FrigateCamera(
|
||||
createCameraConfig({
|
||||
camera_entity: 'camera.office_frigate',
|
||||
}),
|
||||
mock<CameraManagerEngine>(),
|
||||
);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([
|
||||
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
frigateEventWatcher: mock<FrigateEventWatcher>(),
|
||||
});
|
||||
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
await camera.executePTZAction(executor, 'preset', { preset: 'foo' });
|
||||
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||
actions: {
|
||||
action: 'perform-action',
|
||||
data: {
|
||||
action: 'preset',
|
||||
argument: 'foo',
|
||||
},
|
||||
perform_action: 'frigate.ptz',
|
||||
target: {
|
||||
entity_id: 'camera.office_frigate',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,20 +7,16 @@ import {
|
||||
} from '../../../src/camera-manager/frigate/media';
|
||||
import { FrigateEvent, eventSchema } from '../../../src/camera-manager/frigate/types.js';
|
||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { PTZAction } from '../../../src/config/schema/actions/custom/ptz';
|
||||
import { CameraConfig } from '../../../src/config/schema/cameras';
|
||||
import { AdvancedCameraCardView } from '../../../src/config/schema/common/const';
|
||||
import { RawAdvancedCameraCardConfig } from '../../../src/config/types';
|
||||
import {
|
||||
EntityRegistryManager,
|
||||
createEntityRegistryCache,
|
||||
} from '../../../src/utils/ha/registry/entity';
|
||||
import { ViewMedia } from '../../../src/view/media';
|
||||
import { TestViewMedia, createCameraConfig, createHASS } from '../../test-utils';
|
||||
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
|
||||
|
||||
const createEngine = (): FrigateCameraManagerEngine => {
|
||||
return new FrigateCameraManagerEngine(
|
||||
new EntityRegistryManager(createEntityRegistryCache()),
|
||||
new EntityRegistryManagerMock(),
|
||||
new StateWatcher(),
|
||||
new RecordingSegmentsCache(),
|
||||
new RequestCache(),
|
||||
@@ -401,101 +397,3 @@ describe('getCameraEndpoints', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('executePTZAction', () => {
|
||||
describe('preset', () => {
|
||||
it('should reject without preset argument', () => {
|
||||
const hass = createHASS();
|
||||
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
||||
|
||||
createEngine().executePTZAction(hass, cameraConfig, 'preset');
|
||||
|
||||
expect(hass.callService).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should succeed', () => {
|
||||
const hass = createHASS();
|
||||
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
||||
|
||||
createEngine().executePTZAction(hass, cameraConfig, 'preset', {
|
||||
preset: 'preset-foo',
|
||||
});
|
||||
|
||||
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
||||
entity_id: 'camera.office',
|
||||
action: 'preset',
|
||||
argument: 'preset-foo',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('zoom', () => {
|
||||
describe.each([['zoom_in' as const], ['zoom_out' as const]])(
|
||||
'%s',
|
||||
(actionName: PTZAction) => {
|
||||
it('start', () => {
|
||||
const hass = createHASS();
|
||||
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
||||
|
||||
createEngine().executePTZAction(hass, cameraConfig, actionName);
|
||||
|
||||
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
||||
entity_id: 'camera.office',
|
||||
action: 'zoom',
|
||||
argument: actionName === 'zoom_in' ? 'in' : 'out',
|
||||
});
|
||||
});
|
||||
|
||||
it('stop', () => {
|
||||
const hass = createHASS();
|
||||
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
||||
|
||||
createEngine().executePTZAction(hass, cameraConfig, actionName, {
|
||||
phase: 'stop',
|
||||
});
|
||||
|
||||
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
||||
entity_id: 'camera.office',
|
||||
action: 'stop',
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('move', () => {
|
||||
describe.each([
|
||||
['left' as const],
|
||||
['right' as const],
|
||||
['up' as const],
|
||||
['down' as const],
|
||||
])('%s', (actionName: PTZAction) => {
|
||||
it('start', () => {
|
||||
const hass = createHASS();
|
||||
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
||||
|
||||
createEngine().executePTZAction(hass, cameraConfig, actionName);
|
||||
|
||||
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
||||
entity_id: 'camera.office',
|
||||
action: 'move',
|
||||
argument: actionName,
|
||||
});
|
||||
});
|
||||
|
||||
it('stop', () => {
|
||||
const hass = createHASS();
|
||||
const cameraConfig = createCameraConfig({ camera_entity: 'camera.office' });
|
||||
|
||||
createEngine().executePTZAction(hass, cameraConfig, actionName, {
|
||||
phase: 'stop',
|
||||
});
|
||||
|
||||
expect(hass.callService).toBeCalledWith('frigate', 'ptz', {
|
||||
entity_id: 'camera.office',
|
||||
action: 'stop',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -331,10 +331,4 @@ describe('GenericCameraManagerEngine', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute PTZ action', () => {
|
||||
const hass = createHASS();
|
||||
createEngine().executePTZAction(hass, createCameraConfig(), 'left');
|
||||
expect(hass.callService).not.toBeCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,6 +29,7 @@ import { sortMedia } from '../../src/camera-manager/utils/sort-media.js';
|
||||
import { CardController } from '../../src/card-controller/controller.js';
|
||||
import { CameraConfig } from '../../src/config/schema/cameras.js';
|
||||
import { HomeAssistant } from '../../src/ha/types.js';
|
||||
import { PTZMovementType } from '../../src/types.js';
|
||||
import { ViewMedia } from '../../src/view/media.js';
|
||||
import {
|
||||
TestViewMedia,
|
||||
@@ -1069,7 +1070,7 @@ describe('CameraManager', async () => {
|
||||
snapshots: true,
|
||||
|
||||
ptz: {
|
||||
left: ['continuous'],
|
||||
left: [PTZMovementType.Continuous],
|
||||
},
|
||||
}),
|
||||
},
|
||||
@@ -1098,25 +1099,10 @@ describe('CameraManager', async () => {
|
||||
|
||||
manager.executePTZAction('id', 'left', {});
|
||||
|
||||
expect(engine.executePTZAction).not.toBeCalled();
|
||||
// No visible action.
|
||||
});
|
||||
|
||||
it('without hass', async () => {
|
||||
const api = createCardAPI();
|
||||
const engine = mock<CameraManagerEngine>();
|
||||
const manager = createCameraManager(api, engine);
|
||||
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
|
||||
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
||||
manager.executePTZAction('id', 'left');
|
||||
|
||||
expect(engine.executePTZAction).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('successfully from config', async () => {
|
||||
it('successfully', async () => {
|
||||
const api = createCardAPI();
|
||||
const engine = mock<CameraManagerEngine>();
|
||||
const hass = createHASS();
|
||||
@@ -1140,29 +1126,7 @@ describe('CameraManager', async () => {
|
||||
|
||||
manager.executePTZAction('another', 'left');
|
||||
|
||||
expect(api.getActionsManager().executeActions).toBeCalledWith(action);
|
||||
expect(engine.executePTZAction).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('successfully from engine', async () => {
|
||||
const api = createCardAPI();
|
||||
const engine = mock<CameraManagerEngine>();
|
||||
const hass = createHASS();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||
const manager = createCameraManager(api, engine);
|
||||
|
||||
expect(await manager.initializeCamerasFromConfig()).toBeTruthy();
|
||||
|
||||
const action = 'left';
|
||||
const options = {};
|
||||
manager.executePTZAction('id', action, options);
|
||||
|
||||
expect(engine.executePTZAction).toBeCalledWith(
|
||||
hass,
|
||||
expect.anything(),
|
||||
action,
|
||||
options,
|
||||
);
|
||||
expect(api.getActionsManager().executeActions).toBeCalledWith({ actions: action });
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -3,12 +3,79 @@ import { mock } from 'vitest-mock-extended';
|
||||
import { CameraManagerEngine } from '../../../src/camera-manager/engine';
|
||||
import { ReolinkCamera } from '../../../src/camera-manager/reolink/camera';
|
||||
import { CameraProxyConfig } from '../../../src/camera-manager/types';
|
||||
import { ActionsExecutor } from '../../../src/card-controller/actions/types';
|
||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { ProxyConfig } from '../../../src/config/schema/cameras';
|
||||
import { EntityRegistryManager } from '../../../src/utils/ha/registry/entity';
|
||||
import { EntityRegistryManagerLive } from '../../../src/utils/ha/registry/entity';
|
||||
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
|
||||
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
|
||||
|
||||
describe('ReolinkCamera', () => {
|
||||
const cameraEntity = createRegistryEntity({
|
||||
entity_id: 'camera.office_reolink',
|
||||
unique_id: '85270002TS7D4RUP_0_main',
|
||||
platform: 'reolink',
|
||||
});
|
||||
const buttonEntityPTZLeft = createRegistryEntity({
|
||||
entity_id: 'button.office_reolink_ptz_left',
|
||||
unique_id: '85270002TS7D4RUP_0_ptz_left',
|
||||
platform: 'reolink',
|
||||
});
|
||||
const buttonEntityPTZRight = createRegistryEntity({
|
||||
entity_id: 'button.office_reolink_ptz_right',
|
||||
unique_id: '85270002TS7D4RUP_0_ptz_right',
|
||||
platform: 'reolink',
|
||||
});
|
||||
const buttonEntityPTZUp = createRegistryEntity({
|
||||
entity_id: 'button.office_reolink_ptz_up',
|
||||
unique_id: '85270002TS7D4RUP_0_ptz_up',
|
||||
platform: 'reolink',
|
||||
});
|
||||
const buttonEntityPTZDown = createRegistryEntity({
|
||||
entity_id: 'button.office_reolink_ptz_down',
|
||||
unique_id: '85270002TS7D4RUP_0_ptz_down',
|
||||
platform: 'reolink',
|
||||
});
|
||||
const buttonEntityPTZZoomIn = createRegistryEntity({
|
||||
entity_id: 'button.office_reolink_ptz_zoom_in',
|
||||
unique_id: '85270002TS7D4RUP_0_ptz_zoom_in',
|
||||
platform: 'reolink',
|
||||
});
|
||||
const buttonEntityPTZZoomOut = createRegistryEntity({
|
||||
entity_id: 'button.office_reolink_ptz_zoom_out',
|
||||
unique_id: '85270002TS7D4RUP_0_ptz_zoom_out',
|
||||
platform: 'reolink',
|
||||
});
|
||||
const buttonEntityPTZStop = createRegistryEntity({
|
||||
entity_id: 'button.office_reolink_ptz_stop',
|
||||
unique_id: '85270002TS7D4RUP_0_ptz_stop',
|
||||
platform: 'reolink',
|
||||
});
|
||||
|
||||
const ptzPopulatedEntityRegistryManager = new EntityRegistryManagerMock([
|
||||
cameraEntity,
|
||||
buttonEntityPTZLeft,
|
||||
buttonEntityPTZRight,
|
||||
buttonEntityPTZUp,
|
||||
buttonEntityPTZDown,
|
||||
buttonEntityPTZZoomIn,
|
||||
buttonEntityPTZZoomOut,
|
||||
buttonEntityPTZStop,
|
||||
|
||||
// Unrelated button.
|
||||
createRegistryEntity({
|
||||
entity_id: 'button.office_reolink_ptz_foo',
|
||||
unique_id: '85270002TS7D4RUP_0_ptz_foo',
|
||||
platform: 'reolink',
|
||||
}),
|
||||
|
||||
// Unrelated button without unique_id.
|
||||
createRegistryEntity({
|
||||
entity_id: 'button.office_reolink_ptz_bar',
|
||||
platform: 'reolink',
|
||||
}),
|
||||
]);
|
||||
|
||||
describe('should initialize config', () => {
|
||||
describe('should detect channel', () => {
|
||||
it('without a camera_entity', async () => {
|
||||
@@ -19,7 +86,7 @@ describe('ReolinkCamera', () => {
|
||||
async () =>
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: mock<EntityRegistryManager>(),
|
||||
entityRegistryManager: mock<EntityRegistryManagerLive>(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
}),
|
||||
).rejects.toThrowError('Could not find camera entity');
|
||||
@@ -31,17 +98,18 @@ describe('ReolinkCamera', () => {
|
||||
});
|
||||
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
const entity = createRegistryEntity({
|
||||
platform: 'reolink',
|
||||
});
|
||||
entityRegistryManager.getEntity.mockResolvedValue(entity);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity({
|
||||
entity_id: 'camera.office_reolink',
|
||||
platform: 'reolink',
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(
|
||||
async () =>
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
}),
|
||||
).rejects.toThrowError('Could not initialize Reolink camera');
|
||||
@@ -52,19 +120,19 @@ describe('ReolinkCamera', () => {
|
||||
camera_entity: 'camera.office_reolink',
|
||||
});
|
||||
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
const entity = createRegistryEntity({
|
||||
unique_id: 'invalid',
|
||||
platform: 'reolink',
|
||||
});
|
||||
entityRegistryManager.getEntity.mockResolvedValue(entity);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([
|
||||
createRegistryEntity({
|
||||
entity_id: 'camera.office_reolink',
|
||||
unique_id: 'invalid',
|
||||
platform: 'reolink',
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(
|
||||
async () =>
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
}),
|
||||
).rejects.toThrowError('Could not initialize Reolink camera');
|
||||
@@ -76,22 +144,71 @@ describe('ReolinkCamera', () => {
|
||||
camera_entity: 'camera.office_reolink',
|
||||
});
|
||||
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
const entity = createRegistryEntity({
|
||||
unique_id: '85270002TS7D4RUP_0_main',
|
||||
platform: 'reolink',
|
||||
});
|
||||
entityRegistryManager.getEntity.mockResolvedValue(entity);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([cameraEntity]);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: entityRegistryManager,
|
||||
entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
|
||||
expect(camera.getChannel()).toBe(0);
|
||||
});
|
||||
|
||||
describe('successfully with PTZ', () => {
|
||||
it('should find PTZ button entities', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.office_reolink',
|
||||
});
|
||||
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
left: ['continuous'],
|
||||
right: ['continuous'],
|
||||
up: ['continuous'],
|
||||
down: ['continuous'],
|
||||
zoomIn: ['continuous'],
|
||||
zoomOut: ['continuous'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow configured PTZ actions to override', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.office_reolink',
|
||||
ptz: {
|
||||
actions_left: {
|
||||
action: 'perform-action',
|
||||
perform_action: 'homeassistant.toggle',
|
||||
target: {
|
||||
entity_id: 'switch.camera_move_left',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
left: ['relative'],
|
||||
right: ['continuous'],
|
||||
up: ['continuous'],
|
||||
down: ['continuous'],
|
||||
zoomIn: ['continuous'],
|
||||
zoomOut: ['continuous'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should get proxy config', () => {
|
||||
@@ -213,4 +330,103 @@ describe('ReolinkCamera', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe('should execute PTZ action', () => {
|
||||
it('should ignore actions without matching button', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.office_reolink',
|
||||
});
|
||||
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
await camera.executePTZAction(executor, 'left');
|
||||
await camera.executePTZAction(executor, 'left', { phase: 'start' });
|
||||
|
||||
expect(executor.executeActions).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should ignore actions with configured action', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.office_reolink',
|
||||
ptz: {
|
||||
actions_left_start: {
|
||||
action: 'perform-action',
|
||||
perform_action: 'button.press',
|
||||
target: {
|
||||
entity_id: 'button.foo',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([
|
||||
cameraEntity,
|
||||
buttonEntityPTZLeft,
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
await camera.executePTZAction(executor, 'left', { phase: 'start' });
|
||||
|
||||
expect(executor.executeActions).toBeCalledTimes(1);
|
||||
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||
actions: {
|
||||
action: 'perform-action',
|
||||
perform_action: 'button.press',
|
||||
target: {
|
||||
entity_id: 'button.foo',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should execute action with matching button', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.office_reolink',
|
||||
});
|
||||
const camera = new ReolinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
await camera.executePTZAction(executor, 'left', { phase: 'start' });
|
||||
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||
actions: [
|
||||
{
|
||||
action: 'perform-action',
|
||||
perform_action: 'button.press',
|
||||
target: {
|
||||
entity_id: 'button.office_reolink_ptz_left',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await camera.executePTZAction(executor, 'left', { phase: 'stop' });
|
||||
expect(executor.executeActions).toHaveBeenLastCalledWith({
|
||||
actions: [
|
||||
{
|
||||
action: 'perform-action',
|
||||
perform_action: 'button.press',
|
||||
target: {
|
||||
entity_id: 'button.office_reolink_ptz_stop',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,7 +29,7 @@ import {
|
||||
BrowseMedia,
|
||||
browseMediaSchema,
|
||||
} from '../../../src/utils/ha/browse-media/types';
|
||||
import { EntityRegistryManager } from '../../../src/utils/ha/registry/entity';
|
||||
import { EntityRegistryManager } from '../../../src/utils/ha/registry/entity/types';
|
||||
import { ResolvedMediaCache } from '../../../src/utils/ha/resolved-media';
|
||||
import { homeAssistantWSRequest } from '../../../src/utils/ha/ws-request';
|
||||
import {
|
||||
@@ -39,6 +39,7 @@ import {
|
||||
createRegistryEntity,
|
||||
createStore,
|
||||
} from '../../test-utils';
|
||||
import { EntityRegistryManagerMock } from '../../utils/ha/registry/entity/mock';
|
||||
|
||||
vi.mock('../../../src/utils/ha/ws-request');
|
||||
|
||||
@@ -184,7 +185,7 @@ const createEngine = (options?: {
|
||||
entityRegistryManager?: EntityRegistryManager;
|
||||
}): ReolinkCameraManagerEngine => {
|
||||
return new ReolinkCameraManagerEngine(
|
||||
options?.entityRegistryManager ?? mock<EntityRegistryManager>(),
|
||||
options?.entityRegistryManager ?? new EntityRegistryManagerMock(),
|
||||
mock<StateWatcher>(),
|
||||
options?.browseMediaManager ?? new BrowseMediaManager(),
|
||||
new ResolvedMediaCache(),
|
||||
@@ -192,15 +193,15 @@ const createEngine = (options?: {
|
||||
);
|
||||
};
|
||||
|
||||
const createPopulatedEngine = (): ReolinkCameraManagerEngine => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
const entity = createRegistryEntity({
|
||||
unique_id: '85270002TS7D4RUP_0_main',
|
||||
platform: 'reolink',
|
||||
config_entry_id: '01J8XHYTNH77WE3C654K03KX1F',
|
||||
});
|
||||
vi.mocked(entityRegistryManager.getEntity).mockResolvedValue(entity);
|
||||
const cameraEntity = createRegistryEntity({
|
||||
entity_id: 'camera.office',
|
||||
unique_id: '85270002TS7D4RUP_0_main',
|
||||
platform: 'reolink',
|
||||
config_entry_id: '01J8XHYTNH77WE3C654K03KX1F',
|
||||
});
|
||||
|
||||
const createPopulatedEngine = (): ReolinkCameraManagerEngine => {
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([cameraEntity]);
|
||||
return createEngine({ entityRegistryManager });
|
||||
};
|
||||
|
||||
@@ -243,14 +244,7 @@ describe('ReolinkCameraManagerEngine', () => {
|
||||
});
|
||||
|
||||
it('should create camera', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
const entity = createRegistryEntity({
|
||||
unique_id: '85270002TS7D4RUP_0_main',
|
||||
platform: 'reolink',
|
||||
});
|
||||
vi.mocked(entityRegistryManager.getEntity).mockResolvedValue(entity);
|
||||
|
||||
const engine = createEngine({ entityRegistryManager });
|
||||
const engine = createPopulatedEngine();
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.office',
|
||||
unique_id: 'office',
|
||||
@@ -589,15 +583,15 @@ describe('ReolinkCameraManagerEngine', () => {
|
||||
|
||||
describe('should ignore invalid cameras', () => {
|
||||
it('camera without a config entry id', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
const entity = createRegistryEntity({
|
||||
entity_id: 'camera.office',
|
||||
unique_id: '85270002TS7D4RUP_0_main',
|
||||
platform: 'reolink',
|
||||
|
||||
// Cannot fetch events without a config_entry_id.
|
||||
config_entry_id: null,
|
||||
});
|
||||
vi.mocked(entityRegistryManager.getEntity).mockResolvedValue(entity);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([entity]);
|
||||
|
||||
const engine = createEngine({ entityRegistryManager });
|
||||
const store = await createStoreWithReolinkCamera(engine);
|
||||
@@ -1098,15 +1092,15 @@ describe('ReolinkCameraManagerEngine', () => {
|
||||
|
||||
describe('should ignore invalid cameras', () => {
|
||||
it('should get no metdata for camera without a config entry id', async () => {
|
||||
const entityRegistryManager = mock<EntityRegistryManager>();
|
||||
const entity = createRegistryEntity({
|
||||
entity_id: 'camera.office',
|
||||
unique_id: '85270002TS7D4RUP_0_main',
|
||||
platform: 'reolink',
|
||||
|
||||
// Cannot fetch events without a config_entry_id.
|
||||
config_entry_id: null,
|
||||
});
|
||||
vi.mocked(entityRegistryManager.getEntity).mockResolvedValue(entity);
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([entity]);
|
||||
|
||||
const engine = createEngine({ entityRegistryManager });
|
||||
const store = await createStoreWithReolinkCamera(engine);
|
||||
|
||||
@@ -6,7 +6,7 @@ import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-fact
|
||||
import { CameraManagerStore } from '../../src/camera-manager/store.js';
|
||||
import { Engine } from '../../src/camera-manager/types.js';
|
||||
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
|
||||
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity/index.js';
|
||||
import { EntityRegistryManager } from '../../src/utils/ha/registry/entity/types.js';
|
||||
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media.js';
|
||||
import { TestViewMedia, createCameraConfig } from '../test-utils.js';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user