feat: Add TPLink camera engine support (#2257)
* 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 -->
This commit is contained in:
@@ -0,0 +1,462 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { mock } from 'vitest-mock-extended';
|
||||
import { CameraManagerEngine } from '../../../src/camera-manager/engine';
|
||||
import { TPLinkCamera } from '../../../src/camera-manager/tplink/camera';
|
||||
import { ActionsExecutor } from '../../../src/card-controller/actions/types';
|
||||
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
|
||||
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
|
||||
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
|
||||
|
||||
describe('TPLinkCamera', () => {
|
||||
// Entity patterns from: https://github.com/dermotduffy/advanced-camera-card/issues/2183
|
||||
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',
|
||||
device_id: 'tplink_device_1',
|
||||
});
|
||||
const buttonEntityPanLeft = createRegistryEntity({
|
||||
entity_id: 'button.tapo_c520ws_39d3_pan_left',
|
||||
unique_id: '80115E1CF270233D6FC2FCD4028181A7206CDB30-pan_left',
|
||||
platform: 'tplink',
|
||||
config_entry_id: 'tplink_config_entry_1',
|
||||
device_id: 'tplink_device_1',
|
||||
});
|
||||
const buttonEntityPanRight = createRegistryEntity({
|
||||
entity_id: 'button.tapo_c520ws_39d3_pan_right',
|
||||
unique_id: '80115E1CF270233D6FC2FCD4028181A7206CDB30-pan_right',
|
||||
platform: 'tplink',
|
||||
config_entry_id: 'tplink_config_entry_1',
|
||||
device_id: 'tplink_device_1',
|
||||
});
|
||||
const buttonEntityTiltUp = createRegistryEntity({
|
||||
entity_id: 'button.tapo_c520ws_39d3_tilt_up',
|
||||
unique_id: '80115E1CF270233D6FC2FCD4028181A7206CDB30-tilt_up',
|
||||
platform: 'tplink',
|
||||
config_entry_id: 'tplink_config_entry_1',
|
||||
device_id: 'tplink_device_1',
|
||||
});
|
||||
const buttonEntityTiltDown = createRegistryEntity({
|
||||
entity_id: 'button.tapo_c520ws_39d3_tilt_down',
|
||||
unique_id: '80115E1CF270233D6FC2FCD4028181A7206CDB30-tilt_down',
|
||||
platform: 'tplink',
|
||||
config_entry_id: 'tplink_config_entry_1',
|
||||
device_id: 'tplink_device_1',
|
||||
});
|
||||
|
||||
const ptzPopulatedEntityRegistryManager = new EntityRegistryManagerMock([
|
||||
cameraEntity,
|
||||
buttonEntityPanLeft,
|
||||
buttonEntityPanRight,
|
||||
buttonEntityTiltUp,
|
||||
buttonEntityTiltDown,
|
||||
|
||||
// Unrelated button for a different config entry
|
||||
createRegistryEntity({
|
||||
entity_id: 'button.other_device_pan_left',
|
||||
unique_id: 'other_device_pan_left',
|
||||
platform: 'tplink',
|
||||
config_entry_id: 'different_config_entry',
|
||||
}),
|
||||
|
||||
// Unrelated button without unique_id
|
||||
createRegistryEntity({
|
||||
entity_id: 'button.tapo_c520ws_39d3_something',
|
||||
platform: 'tplink',
|
||||
config_entry_id: 'tplink_config_entry_1',
|
||||
}),
|
||||
]);
|
||||
|
||||
describe('should initialize config', () => {
|
||||
it('without a camera_entity', async () => {
|
||||
const config = createCameraConfig();
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
expect(
|
||||
async () =>
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
}),
|
||||
).rejects.toThrowError('Could not find camera entity');
|
||||
});
|
||||
|
||||
it('without a matching entity in registry', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
expect(
|
||||
async () =>
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock(),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
}),
|
||||
).rejects.toThrowError('Could not find camera entity');
|
||||
});
|
||||
|
||||
it('successfully with webrtc_card.entity fallback', async () => {
|
||||
const config = createCameraConfig({
|
||||
webrtc_card: {
|
||||
entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
},
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([cameraEntity]);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
|
||||
expect(camera.getEntity()).toBe(cameraEntity);
|
||||
});
|
||||
|
||||
it('successfully without PTZ entities', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
const entityRegistryManager = new EntityRegistryManagerMock([cameraEntity]);
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
|
||||
expect(camera.getEntity()).toBe(cameraEntity);
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toBeNull();
|
||||
});
|
||||
|
||||
describe('successfully with PTZ', () => {
|
||||
it('should find PTZ button entities', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
left: ['relative'],
|
||||
right: ['relative'],
|
||||
up: ['relative'],
|
||||
down: ['relative'],
|
||||
});
|
||||
});
|
||||
|
||||
it('should allow configured PTZ actions to override', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
ptz: {
|
||||
actions_left: {
|
||||
action: 'perform-action',
|
||||
perform_action: 'homeassistant.toggle',
|
||||
target: {
|
||||
entity_id: 'switch.camera_move_left',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
|
||||
left: ['relative'],
|
||||
right: ['relative'],
|
||||
up: ['relative'],
|
||||
down: ['relative'],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should execute PTZ action', () => {
|
||||
it('should ignore actions without matching button', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
expect(await camera.executePTZAction(executor, 'left')).toBeFalsy();
|
||||
expect(
|
||||
await camera.executePTZAction(executor, 'left', { phase: 'start' }),
|
||||
).toBeFalsy();
|
||||
|
||||
expect(executor.executeActions).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should ignore zoom actions (not supported by TPLink)', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
expect(
|
||||
await camera.executePTZAction(executor, 'zoom_in', { phase: 'start' }),
|
||||
).toBeFalsy();
|
||||
expect(
|
||||
await camera.executePTZAction(executor, 'zoom_out', { phase: 'start' }),
|
||||
).toBeFalsy();
|
||||
|
||||
expect(executor.executeActions).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should ignore preset actions (not supported by TPLink)', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
expect(
|
||||
await camera.executePTZAction(executor, 'preset', { preset: 'home' }),
|
||||
).toBeFalsy();
|
||||
|
||||
expect(executor.executeActions).not.toBeCalled();
|
||||
});
|
||||
|
||||
it('should handle stop phase (no-op for TPLink)', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
// Stop should return true (handled) but not execute any actions
|
||||
expect(
|
||||
await camera.executePTZAction(executor, 'left', { phase: 'stop' }),
|
||||
).toBeTruthy();
|
||||
|
||||
expect(executor.executeActions).not.toBeCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
['left', 'button.tapo_c520ws_39d3_pan_left'],
|
||||
['right', 'button.tapo_c520ws_39d3_pan_right'],
|
||||
['up', 'button.tapo_c520ws_39d3_tilt_up'],
|
||||
['down', 'button.tapo_c520ws_39d3_tilt_down'],
|
||||
] as const)(
|
||||
'should execute %s action (relative movement)',
|
||||
async (action, entityId) => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: ptzPopulatedEntityRegistryManager,
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
await camera.executePTZAction(executor, action);
|
||||
expect(executor.executeActions).toHaveBeenCalledWith({
|
||||
actions: [
|
||||
{
|
||||
action: 'perform-action',
|
||||
perform_action: 'button.press',
|
||||
target: {
|
||||
entity_id: entityId,
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it('should also work with phase start for compatibility', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
});
|
||||
const camera = new TPLinkCamera(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).toHaveBeenCalledWith({
|
||||
actions: [
|
||||
{
|
||||
action: 'perform-action',
|
||||
perform_action: 'button.press',
|
||||
target: {
|
||||
entity_id: 'button.tapo_c520ws_39d3_pan_left',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should use configured action when provided', async () => {
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
ptz: {
|
||||
actions_left_start: {
|
||||
action: 'perform-action',
|
||||
perform_action: 'button.press',
|
||||
target: {
|
||||
entity_id: 'button.custom_left',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([
|
||||
cameraEntity,
|
||||
buttonEntityPanLeft,
|
||||
]),
|
||||
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.custom_left',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle camera without PTZ unique_id pattern', () => {
|
||||
it('should not find PTZ entities when unique_id has no _live_view suffix', async () => {
|
||||
const cameraWithDifferentUniqueId = createRegistryEntity({
|
||||
entity_id: 'camera.tapo_c520ws_39d3',
|
||||
unique_id: 'c520ws_39d3_something_else',
|
||||
platform: 'tplink',
|
||||
config_entry_id: 'tplink_config_entry_1',
|
||||
});
|
||||
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3',
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([
|
||||
cameraWithDifferentUniqueId,
|
||||
buttonEntityPanLeft,
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
|
||||
// Should not find PTZ entities since unique_id doesn't end with _live_view
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toBeNull();
|
||||
});
|
||||
|
||||
it('should not find PTZ entities when camera has no unique_id', async () => {
|
||||
const cameraWithoutUniqueId = createRegistryEntity({
|
||||
entity_id: 'camera.tapo_c520ws_39d3_live_view',
|
||||
platform: 'tplink',
|
||||
config_entry_id: 'tplink_config_entry_1',
|
||||
});
|
||||
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([
|
||||
cameraWithoutUniqueId,
|
||||
buttonEntityPanLeft,
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
|
||||
// Should not find PTZ entities since camera has no unique_id
|
||||
expect(camera.getCapabilities()?.getPTZCapabilities()).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle partial PTZ button availability', () => {
|
||||
it('should return false for direction without matching button', async () => {
|
||||
// Only has pan_left button, trying to execute pan_right
|
||||
const config = createCameraConfig({
|
||||
camera_entity: 'camera.tapo_c520ws_39d3_live_view',
|
||||
});
|
||||
const camera = new TPLinkCamera(config, mock<CameraManagerEngine>());
|
||||
|
||||
await camera.initialize({
|
||||
hass: createHASS(),
|
||||
entityRegistryManager: new EntityRegistryManagerMock([
|
||||
cameraEntity,
|
||||
buttonEntityPanLeft, // Only left button available
|
||||
]),
|
||||
stateWatcher: mock<StateWatcher>(),
|
||||
});
|
||||
const executor = mock<ActionsExecutor>();
|
||||
|
||||
// Left should work
|
||||
expect(
|
||||
await camera.executePTZAction(executor, 'left', { phase: 'start' }),
|
||||
).toBeTruthy();
|
||||
expect(executor.executeActions).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Right should fail (no button for it)
|
||||
expect(
|
||||
await camera.executePTZAction(executor, 'right', { phase: 'start' }),
|
||||
).toBeFalsy();
|
||||
expect(executor.executeActions).toHaveBeenCalledTimes(1); // Still only 1 call
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user