* Related #2183 <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduce `tplink` camera engine with relative PTZ via button entities, add icons/docs support, refactor to `EntityCamera`, and extend tests/schema/factory for auto-detection. > > - **Engines** > - **New `tplink` engine**: > - Add `Engine.TPLink`, schema support (`engine: 'tplink'`). > - Implement `TPLinkCameraManagerEngine` and `TPLinkCamera` (relative PTZ via `button.*` entities; no presets/zoom; stop is no-op). > - Update `engine-factory` to create/auto-detect `tplink` (platform `tplink`). > - **Refactor** > - Replace `BrowseMediaCamera` with `EntityCamera`; update `motioneye` and `reolink` cameras and `motioneye` engine type checks. > - **UI/Icons** > - Add `tplink.svg`; wire into `IconController`; update docs icon copy script. > - **Docs** > - Add `tplink` to engine capability and live-provider matrices; new `tplink` section incl. PTZ note; add custom icon entry. > - **Tests** > - Add TPLink camera/engine tests and factory tests; remove obsolete `BrowseMediaCamera` test. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 74a688c3bd602912d63a22c3c6fa2f4b60ec14f9. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
107 lines
3.4 KiB
TypeScript
107 lines
3.4 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
|
import { mock } from 'vitest-mock-extended';
|
|
import { TPLinkCameraManagerEngine } from '../../../src/camera-manager/tplink/engine-tplink';
|
|
import { Engine } from '../../../src/camera-manager/types';
|
|
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
|
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
|
|
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
|
|
|
|
const createEngine = (options?: {
|
|
entityRegistryManager?: EntityRegistryManagerMock;
|
|
}): TPLinkCameraManagerEngine => {
|
|
return new TPLinkCameraManagerEngine(
|
|
options?.entityRegistryManager ?? new EntityRegistryManagerMock(),
|
|
mock<StateWatcher>(),
|
|
);
|
|
};
|
|
|
|
const cameraEntity = createRegistryEntity({
|
|
entity_id: 'camera.tapo_c520ws_39d3_live_view',
|
|
unique_id: '80115E1CF270233D6FC2FCD4028181A7206CDB30-live_view',
|
|
platform: 'tplink',
|
|
config_entry_id: 'tplink_config_entry_1',
|
|
});
|
|
|
|
const createPopulatedEngine = (): TPLinkCameraManagerEngine => {
|
|
const entityRegistryManager = new EntityRegistryManagerMock([cameraEntity]);
|
|
return createEngine({ entityRegistryManager });
|
|
};
|
|
|
|
describe('TPLinkCameraManagerEngine', () => {
|
|
it('should get correct engine type', () => {
|
|
const engine = createEngine();
|
|
expect(engine.getEngineType()).toBe(Engine.TPLink);
|
|
});
|
|
|
|
it('should create camera', async () => {
|
|
const engine = createPopulatedEngine();
|
|
const config = createCameraConfig({
|
|
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
|
id: 'tapo_office',
|
|
});
|
|
|
|
const camera = await engine.createCamera(createHASS(), config);
|
|
|
|
expect(camera.getConfig()).toBe(config);
|
|
expect(camera.getEngine()).toBe(engine);
|
|
expect(camera.getCapabilities()?.getRawCapabilities()).toEqual({
|
|
'favorite-events': false,
|
|
'favorite-recordings': false,
|
|
clips: false,
|
|
'remote-control-entity': true,
|
|
live: true,
|
|
menu: true,
|
|
recordings: false,
|
|
seek: false,
|
|
snapshots: false,
|
|
substream: true,
|
|
trigger: true,
|
|
});
|
|
});
|
|
|
|
it('should get camera metadata with tplink icon', () => {
|
|
const cameraConfig = createCameraConfig({
|
|
title: 'Tapo Office',
|
|
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
|
icon: 'mdi:camera',
|
|
});
|
|
const engine = createEngine();
|
|
expect(engine.getCameraMetadata(createHASS(), cameraConfig)).toEqual({
|
|
engineIcon: 'tplink',
|
|
icon: {
|
|
icon: 'mdi:camera',
|
|
entity: 'camera.tapo_c520ws_39d3_live_view',
|
|
fallback: 'mdi:video',
|
|
},
|
|
title: 'Tapo Office',
|
|
});
|
|
});
|
|
|
|
it('should get camera metadata with auto-detected title', () => {
|
|
const cameraConfig = createCameraConfig({
|
|
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
|
});
|
|
const hass = createHASS({
|
|
'camera.tapo_c520ws_39d3_live_view': {
|
|
entity_id: 'camera.tapo_c520ws_39d3_live_view',
|
|
state: 'idle',
|
|
attributes: {
|
|
friendly_name: 'Tapo C520WS Live View',
|
|
},
|
|
last_changed: '',
|
|
last_updated: '',
|
|
context: { id: '', user_id: null, parent_id: null },
|
|
},
|
|
});
|
|
const engine = createEngine();
|
|
expect(engine.getCameraMetadata(hass, cameraConfig)).toEqual({
|
|
engineIcon: 'tplink',
|
|
icon: {
|
|
entity: 'camera.tapo_c520ws_39d3_live_view',
|
|
fallback: 'mdi:video',
|
|
},
|
|
title: 'Tapo C520WS Live View',
|
|
});
|
|
});
|
|
});
|