feat: Trigger cameras on HA bus events (#2512)

This commit is contained in:
Dermot Duffy
2026-06-30 17:45:13 -07:00
committed by dermotduffy
parent 406176eebc
commit 0a36358394
49 changed files with 1376 additions and 269 deletions
@@ -1,25 +1,27 @@
import { describe, expect, it } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { BrowseMediaCameraManagerEngine } from '../../../src/camera-manager/browse-media/engine-browse-media';
import { CameraManagerReadOnlyConfigStore } from '../../../src/camera-manager/store';
import {
CameraManagerRequestCache,
CameraQuery,
QueryType,
} from '../../../src/camera-manager/types';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
import { BROWSE_MEDIA_CACHE_SECONDS } from '../../../src/ha/browse-media/types';
import { BrowseMediaWalker } from '../../../src/ha/browse-media/walker';
import { ResolvedMediaCache } from '../../../src/ha/resolved-media';
import { QuerySource } from '../../../src/query-source';
import { ViewMedia } from '../../../src/view/item';
import { CameraManagerReadOnlyConfigStore } from '../../../src/camera-manager/store';
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
import { createCameraConfig, createHASS } from '../../test-utils';
const createEngine = (): BrowseMediaCameraManagerEngine => {
return new BrowseMediaCameraManagerEngine(
new EntityRegistryManagerMock(),
mock<StateWatcher>(),
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
new BrowseMediaWalker(),
new ResolvedMediaCache(),
new CameraManagerRequestCache(),
+247 -34
View File
@@ -3,10 +3,12 @@ import { mock } from 'vitest-mock-extended';
import { Camera } from '../../src/camera-manager/camera.js';
import { GenericCameraManagerEngine } from '../../src/camera-manager/generic/engine-generic.js';
import { CameraProxyConfig } from '../../src/camera-manager/types.js';
import { EventWatcherSubscriptionInterface } from '../../src/card-controller/hass/event-watcher.js';
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
import { liveProviderSupports2WayAudio } from '../../src/utils/live-provider.js';
import { EntityRegistryManagerMock } from '../ha/registry/entity/mock.js';
import {
callEventWatcherCallback,
callStateWatcherCallback,
createCameraConfig,
createCapabilities,
@@ -23,7 +25,10 @@ describe('Camera', () => {
const config = createCameraConfig();
const camera = new Camera(
config,
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
expect(camera.getConfig()).toBe(config);
});
@@ -33,7 +38,10 @@ describe('Camera', () => {
const capabilities = createCapabilities();
const camera = await createInitializedCamera(
createCameraConfig(),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
capabilities,
);
expect(camera.getCapabilities()).toBe(capabilities);
@@ -42,7 +50,10 @@ describe('Camera', () => {
it('when unpopulated', async () => {
const camera = new Camera(
createCameraConfig(),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
expect(camera.getCapabilities()).toBeNull();
});
@@ -51,6 +62,7 @@ describe('Camera', () => {
it('should get engine', async () => {
const engine = new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
);
const camera = new Camera(createCameraConfig(), engine);
expect(camera.getEngine()).toBe(engine);
@@ -59,7 +71,10 @@ describe('Camera', () => {
it('should set and get id', async () => {
const camera = new Camera(
createCameraConfig(),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
camera.setID('foo');
expect(camera.getID()).toBe('foo');
@@ -69,7 +84,10 @@ describe('Camera', () => {
it('should throw without id', async () => {
const camera = new Camera(
createCameraConfig(),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
expect(() => camera.getID()).toThrowError(
'Could not determine camera id for the following ' +
@@ -89,13 +107,17 @@ describe('Camera', () => {
entities: ['camera.foo'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
const stateWatcher = mock<StateWatcherSubscriptionInterface>();
await camera.initialize({
hass: createHASS(),
stateWatcher: stateWatcher,
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
capabilityOptions: { capabilities: createCapabilities({ trigger: true }) },
});
@@ -116,7 +138,10 @@ describe('Camera', () => {
stream: 'stream',
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true);
@@ -124,6 +149,7 @@ describe('Camera', () => {
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).toHaveBeenCalledWith(
@@ -154,7 +180,10 @@ describe('Camera', () => {
stream: 'stream',
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(false);
@@ -162,6 +191,7 @@ describe('Camera', () => {
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getCapabilities()?.has('2-way-audio')).toBe(false);
@@ -176,7 +206,10 @@ describe('Camera', () => {
metadata_fetch_timeout_seconds: 20,
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true);
@@ -184,6 +217,7 @@ describe('Camera', () => {
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).toHaveBeenCalledWith(
@@ -206,7 +240,10 @@ describe('Camera', () => {
live: true,
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true);
@@ -217,6 +254,7 @@ describe('Camera', () => {
await camera.initialize({
hass,
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).toHaveBeenCalledWith(
@@ -243,7 +281,10 @@ describe('Camera', () => {
createCameraConfig({
proxy: { live: true },
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
expect(camera.getLiveProxyConfig()).toEqual(
expect.objectContaining({ enabled: true, enforce: true }),
@@ -255,7 +296,10 @@ describe('Camera', () => {
createCameraConfig({
proxy: { media: true },
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
expect(camera.getMediaProxyConfig()).toEqual(
expect.objectContaining({ enabled: true, enforce: true }),
@@ -268,7 +312,10 @@ describe('Camera', () => {
live_provider: 'go2rtc',
go2rtc: { url: 'http://go2rtc' },
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
expect(camera.getLiveProxyConfig()).toEqual(
expect.objectContaining({ enabled: true, enforce: false }),
@@ -280,7 +327,10 @@ describe('Camera', () => {
createCameraConfig({
proxy: { media: 'auto' },
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
expect(camera.getMediaProxyConfig()).toEqual(
expect.objectContaining({ enabled: false, enforce: false }),
@@ -294,12 +344,16 @@ describe('Camera', () => {
force: ['2-way-audio'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
@@ -314,12 +368,16 @@ describe('Camera', () => {
force: ['2-way-audio'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
@@ -334,12 +392,16 @@ describe('Camera', () => {
force: ['2-way-audio'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
@@ -353,12 +415,16 @@ describe('Camera', () => {
disable: ['2-way-audio'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
@@ -372,12 +438,16 @@ describe('Camera', () => {
disable_except: ['substream'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).not.toHaveBeenCalled();
@@ -391,13 +461,17 @@ describe('Camera', () => {
disable_except: ['substream', '2-way-audio'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).toHaveBeenCalled();
@@ -411,13 +485,17 @@ describe('Camera', () => {
disable_except: [],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
vi.mocked(liveProviderSupports2WayAudio).mockResolvedValue(true);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(liveProviderSupports2WayAudio).toHaveBeenCalled();
@@ -432,12 +510,16 @@ describe('Camera', () => {
});
const camera = new Camera(
createCameraConfig({ camera_entity: 'camera.front_door' }),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
});
@@ -447,12 +529,16 @@ describe('Camera', () => {
it('should leave entity null when camera_entity is unset', async () => {
const camera = new Camera(
createCameraConfig(),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
entityRegistryManager: new EntityRegistryManagerMock(),
});
@@ -462,12 +548,16 @@ describe('Camera', () => {
it('should leave entity null when entityRegistryManager is not provided', async () => {
const camera = new Camera(
createCameraConfig({ camera_entity: 'camera.front_door' }),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getEntity()).toBeNull();
@@ -504,7 +594,10 @@ describe('Camera', () => {
...(options?.userEntities && { entities: options.userEntities }),
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
const stateWatcher = mock<StateWatcherSubscriptionInterface>();
const hass = createHASS(
@@ -518,6 +611,7 @@ describe('Camera', () => {
await camera.initialize({
hass,
stateWatcher,
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
...(!options?.omitRegistryManager && {
entityRegistryManager: new EntityRegistryManagerMock(
options?.registryEntities ?? [cameraEntity, doorbellEntity],
@@ -643,7 +737,10 @@ describe('Camera', () => {
entities: ['binary_sensor.foo'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
{
eventCallback: eventCallback,
},
@@ -653,6 +750,7 @@ describe('Camera', () => {
await camera.initialize({
hass: createHASS(),
stateWatcher: stateWatcher,
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
capabilityOptions: { capabilities: createCapabilities({ trigger: true }) },
});
@@ -673,6 +771,100 @@ describe('Camera', () => {
},
);
it('should subscribe to configured triggers.events and dispatch momentary events', async () => {
const eventCallback = vi.fn();
const camera = new Camera(
createCameraConfig({
id: 'camera_1',
triggers: {
events: [{ event_type: 'zha_event' }],
},
}),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
{ eventCallback },
);
const eventWatcher = mock<EventWatcherSubscriptionInterface>();
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher,
capabilityOptions: { capabilities: createCapabilities({ trigger: true }) },
});
expect(eventWatcher.subscribe).toBeCalledTimes(1);
const request = vi.mocked(eventWatcher.subscribe).mock.calls[0][1];
expect(request.event_type).toBe('zha_event');
expect(request.matcher).toBeUndefined();
callEventWatcherCallback(eventWatcher, { command: 'press' });
expect(eventCallback).toBeCalledWith({
cameraID: 'camera_1',
id: 'event:zha_event',
type: 'momentary',
});
await camera.destroy();
expect(eventWatcher.unsubscribe).toBeCalled();
});
it('should attach a matcher when triggers.events entry has data filter', async () => {
const camera = new Camera(
createCameraConfig({
id: 'camera_1',
triggers: {
events: [{ event_type: 'zha_event', event_data: { command: 'press' } }],
},
}),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
const eventWatcher = mock<EventWatcherSubscriptionInterface>();
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher,
capabilityOptions: { capabilities: createCapabilities({ trigger: true }) },
});
const matcher = vi.mocked(eventWatcher.subscribe).mock.calls[0][1].matcher;
expect(matcher).toBeDefined();
expect(matcher?.({ command: 'press', extra: 1 })).toBe(true);
expect(matcher?.({ command: 'release' })).toBe(false);
});
it('should not subscribe to events when trigger capability is disabled', async () => {
const camera = new Camera(
createCameraConfig({
id: 'camera_1',
triggers: {
events: [{ event_type: 'zha_event' }],
},
}),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
const eventWatcher = mock<EventWatcherSubscriptionInterface>();
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher,
capabilityOptions: { capabilities: createCapabilities({ trigger: false }) },
});
expect(eventWatcher.subscribe).not.toBeCalled();
});
it('should not dispatch when the helper returns null', async () => {
vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
@@ -684,7 +876,10 @@ describe('Camera', () => {
entities: ['event.front_door_doorbell'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
{
eventCallback: eventCallback,
},
@@ -694,6 +889,7 @@ describe('Camera', () => {
await camera.initialize({
hass: createHASS(),
stateWatcher: stateWatcher,
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
capabilityOptions: { capabilities: createCapabilities({ trigger: true }) },
});
@@ -706,7 +902,7 @@ describe('Camera', () => {
expect(eventCallback).not.toBeCalled();
});
it('should dispatch a signal for an event entity fire', async () => {
it('should dispatch a momentary event for an event entity fire', async () => {
vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
const eventCallback = vi.fn();
@@ -717,7 +913,10 @@ describe('Camera', () => {
entities: ['event.front_door_doorbell'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
{
eventCallback: eventCallback,
},
@@ -727,6 +926,7 @@ describe('Camera', () => {
await camera.initialize({
hass: createHASS(),
stateWatcher: stateWatcher,
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
capabilityOptions: { capabilities: createCapabilities({ trigger: true }) },
});
@@ -740,7 +940,7 @@ describe('Camera', () => {
expect(eventCallback).toBeCalledWith({
cameraID: 'camera_1',
id: 'event.front_door_doorbell',
type: 'signal',
type: 'momentary',
});
});
@@ -753,7 +953,10 @@ describe('Camera', () => {
entities: ['binary_sensor.foo'],
},
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
{
eventCallback: eventCallback,
},
@@ -763,6 +966,7 @@ describe('Camera', () => {
await camera.initialize({
hass: createHASS(),
stateWatcher: stateWatcher,
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
capabilityOptions: { capabilities: createCapabilities({ trigger: false }) },
});
@@ -924,7 +1128,10 @@ describe('Camera', () => {
(_name: string, cameraConfig: unknown, expectedResult: CameraProxyConfig) => {
const camera = new Camera(
createCameraConfig(cameraConfig),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
expect(camera.getProxyConfig()).toEqual(expectedResult);
},
@@ -938,7 +1145,10 @@ describe('Camera', () => {
go2rtc: { stream: '' },
camera_entity: '',
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
expect(camera.getEndpoints()).toBeNull();
});
@@ -952,7 +1162,10 @@ describe('Camera', () => {
},
camera_entity: 'camera.foo',
}),
new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>()),
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
),
);
expect(camera.getEndpoints()).toEqual({
+7 -1
View File
@@ -7,10 +7,11 @@ import { MotionEyeCameraManagerEngine } from '../../src/camera-manager/motioneye
import { ReolinkCameraManagerEngine } from '../../src/camera-manager/reolink/engine-reolink.js';
import { TPLinkCameraManagerEngine } from '../../src/camera-manager/tplink/engine-tplink.js';
import { Engine } from '../../src/camera-manager/types.js';
import { EventWatcherSubscriptionInterface } from '../../src/card-controller/hass/event-watcher.js';
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
import { CardWideConfig } from '../../src/config/schema/types.js';
import { EntityRegistryManager } from '../../src/ha/registry/entity/types.js';
import { DeviceRegistryManager } from '../../src/ha/registry/device';
import { EntityRegistryManager } from '../../src/ha/registry/entity/types.js';
import { ResolvedMediaCache } from '../../src/ha/resolved-media.js';
import { EntityRegistryManagerMock } from '../ha/registry/entity/mock.js';
import {
@@ -232,6 +233,7 @@ describe('createEngine()', () => {
expect(
await createFactory().createEngine(Engine.Generic, {
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
resolvedMediaCache: mock<ResolvedMediaCache>(),
}),
).toBeInstanceOf(GenericCameraManagerEngine);
@@ -240,6 +242,7 @@ describe('createEngine()', () => {
expect(
await createFactory().createEngine(Engine.Frigate, {
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
resolvedMediaCache: mock<ResolvedMediaCache>(),
}),
).toBeInstanceOf(FrigateCameraManagerEngine);
@@ -248,6 +251,7 @@ describe('createEngine()', () => {
expect(
await createFactory().createEngine(Engine.MotionEye, {
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
resolvedMediaCache: mock<ResolvedMediaCache>(),
}),
).toBeInstanceOf(MotionEyeCameraManagerEngine);
@@ -256,6 +260,7 @@ describe('createEngine()', () => {
expect(
await createFactory().createEngine(Engine.Reolink, {
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
resolvedMediaCache: mock<ResolvedMediaCache>(),
}),
).toBeInstanceOf(ReolinkCameraManagerEngine);
@@ -264,6 +269,7 @@ describe('createEngine()', () => {
expect(
await createFactory().createEngine(Engine.TPLink, {
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
resolvedMediaCache: mock<ResolvedMediaCache>(),
}),
).toBeInstanceOf(TPLinkCameraManagerEngine);
+116 -63
View File
@@ -18,9 +18,10 @@ import {
FrigateReviewWatcher,
} from '../../../src/camera-manager/frigate/watcher';
import { ActionsExecutor } from '../../../src/card-controller/actions/types';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
import { PTZAction } from '../../../src/config/schema/actions/custom/ptz';
import { CameraTriggerEventType } from '../../../src/config/schema/cameras';
import { CameraTriggerMediaEventType } from '../../../src/config/schema/cameras';
import { Entity, EntityRegistryManager } from '../../../src/ha/registry/entity/types';
import { ViewMediaType } from '../../../src/view/item';
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
@@ -69,7 +70,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -91,7 +93,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
}),
@@ -116,7 +119,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -141,7 +145,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -166,7 +171,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -191,7 +197,8 @@ describe('FrigateCamera', () => {
}),
}),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -214,7 +221,8 @@ describe('FrigateCamera', () => {
}),
}),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -231,7 +239,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -254,7 +263,8 @@ describe('FrigateCamera', () => {
}),
}),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -278,7 +288,8 @@ describe('FrigateCamera', () => {
}),
}),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -301,7 +312,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -329,7 +341,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -361,7 +374,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -391,7 +405,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -428,7 +443,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -460,7 +476,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -496,7 +513,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -814,7 +832,7 @@ describe('FrigateCamera', () => {
camera_name: 'CAMERA',
},
triggers: {
events: ['events'],
media_events: ['events'],
},
}),
mock<CameraManagerEngine>(),
@@ -825,7 +843,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -845,7 +864,7 @@ describe('FrigateCamera', () => {
camera_name: 'CAMERA',
},
triggers: {
events: [],
media_events: [],
},
}),
mock<CameraManagerEngine>(),
@@ -856,7 +875,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -882,7 +902,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -896,7 +917,7 @@ describe('FrigateCamera', () => {
client_id: 'CLIENT_ID',
},
triggers: {
events: ['events'],
media_events: ['events'],
},
}),
mock<CameraManagerEngine>(),
@@ -907,7 +928,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -919,7 +941,7 @@ describe('FrigateCamera', () => {
createCameraConfig({
frigate: { camera_name: 'front_door' },
triggers: {
events: ['events'],
media_events: ['events'],
},
}),
mock<CameraManagerEngine>(),
@@ -932,7 +954,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -1002,7 +1025,7 @@ describe('FrigateCamera', () => {
])(
'with events %s when snapshot %s and clip %s',
async (
events: CameraTriggerEventType[],
events: CameraTriggerMediaEventType[],
hasSnapshot: boolean,
hasClip: boolean,
call: boolean,
@@ -1015,7 +1038,7 @@ describe('FrigateCamera', () => {
camera_name: 'camera.front_door',
},
triggers: {
events: events,
media_events: events,
},
}),
mock<CameraManagerEngine>(),
@@ -1029,7 +1052,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -1087,7 +1111,7 @@ describe('FrigateCamera', () => {
zones: ['front_steps'],
},
triggers: {
events: ['events'],
media_events: ['events'],
},
}),
mock<CameraManagerEngine>(),
@@ -1101,7 +1125,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -1146,7 +1171,7 @@ describe('FrigateCamera', () => {
labels: ['person'],
},
triggers: {
events: ['events'],
media_events: ['events'],
},
}),
mock<CameraManagerEngine>(),
@@ -1160,7 +1185,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -1202,7 +1228,7 @@ describe('FrigateCamera', () => {
camera_name: 'camera.front_door',
},
triggers: {
events: ['events'],
media_events: ['events'],
},
}),
mock<CameraManagerEngine>(),
@@ -1216,7 +1242,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: eventWatcher,
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -1269,7 +1296,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1302,7 +1330,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1331,7 +1360,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1365,7 +1395,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1429,7 +1460,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1501,7 +1533,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1573,7 +1606,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1631,7 +1665,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1688,7 +1723,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1749,7 +1785,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1807,7 +1844,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1873,7 +1911,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: reviewWatcher,
});
@@ -1951,7 +1990,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -1981,7 +2021,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2005,7 +2046,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: new EntityRegistryManagerMock(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2030,7 +2072,8 @@ describe('FrigateCamera', () => {
camera.initialize({
hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
}),
@@ -2056,7 +2099,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2081,7 +2125,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2105,7 +2150,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: new EntityRegistryManagerMock(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2138,7 +2184,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2173,7 +2220,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2193,7 +2241,8 @@ describe('FrigateCamera', () => {
await camera.initialize({
hass: hass,
entityRegistryManager: mock<EntityRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2226,7 +2275,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: new EntityRegistryManagerMock([
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2264,7 +2314,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: new EntityRegistryManagerMock([
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2317,7 +2368,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: new EntityRegistryManagerMock([
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -2353,7 +2405,8 @@ describe('FrigateCamera', () => {
entityRegistryManager: new EntityRegistryManagerMock([
createRegistryEntity({ entity_id: 'camera.office_frigate' }),
]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
frigateEventWatcher: mock<FrigateEventWatcher>(),
frigateReviewWatcher: mock<FrigateReviewWatcher>(),
});
@@ -1,4 +1,5 @@
import { afterEach, assert, describe, expect, it, vi } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { RecordingSegmentsCache } from '../../../src/camera-manager/cache';
import { Camera } from '../../../src/camera-manager/camera';
import {
@@ -32,6 +33,7 @@ import {
QueryResultsType,
QueryType,
} from '../../../src/camera-manager/types';
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { CameraConfig } from '../../../src/config/schema/cameras';
import { RawAdvancedCameraCardConfig } from '../../../src/config/types';
@@ -58,6 +60,7 @@ const createEngine = (options?: {
return new FrigateCameraManagerEngine(
new EntityRegistryManagerMock(),
new StateWatcher(),
mock<EventWatcherSubscriptionInterface>(),
options?.cache ?? new RecordingSegmentsCache(),
options?.requestCache ?? new CameraManagerRequestCache(),
);
@@ -2,10 +2,11 @@ import { describe, expect, it } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { GenericCameraManagerEngine } from '../../../src/camera-manager/generic/engine-generic';
import { Engine, QueryResultsType, QueryType } from '../../../src/camera-manager/types';
import { QuerySource } from '../../../src/query-source';
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
import { CameraConfig } from '../../../src/config/schema/cameras';
import { RawAdvancedCameraCardConfig } from '../../../src/config/types';
import { QuerySource } from '../../../src/query-source';
import {
TestViewMedia,
createCameraConfig,
@@ -15,7 +16,10 @@ import {
} from '../../test-utils';
const createEngine = (): GenericCameraManagerEngine => {
return new GenericCameraManagerEngine(mock<StateWatcherSubscriptionInterface>());
return new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
);
};
const createGenericCameraConfig = (
+4 -4
View File
@@ -574,15 +574,15 @@ describe('CameraManager', () => {
config: createCameraConfig({
...baseCameraConfig,
triggers: {
events: ['snapshots'],
media_events: ['snapshots'],
},
}),
},
]);
await manager.initializeCamerasFromConfig();
expect(manager.getStore().getCamera('id')?.getConfig().triggers.events).toEqual([
'snapshots',
]);
expect(
manager.getStore().getCamera('id')?.getConfig().triggers.media_events,
).toEqual(['snapshots']);
});
});
+10 -5
View File
@@ -2,7 +2,8 @@ import { describe, expect, it } from 'vitest';
import { mock } from 'vitest-mock-extended';
import { CameraManagerEngine } from '../../../src/camera-manager/engine';
import { MotionEyeCamera } from '../../../src/camera-manager/motioneye/camera';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
@@ -48,7 +49,8 @@ describe('MotionEyeCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const endpoints = camera.getEndpoints();
@@ -65,7 +67,8 @@ describe('MotionEyeCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const endpoints = camera.getEndpoints();
@@ -82,7 +85,8 @@ describe('MotionEyeCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const capabilities = camera.getCapabilities();
@@ -107,7 +111,8 @@ describe('MotionEyeCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const capabilities = camera.getCapabilities();
@@ -14,7 +14,8 @@ import {
QueryResultsType,
QueryType,
} from '../../../src/camera-manager/types';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
import { BrowseMediaMetadata } from '../../../src/ha/browse-media/types';
import { BrowseMediaStep, BrowseMediaWalker } from '../../../src/ha/browse-media/walker';
import { Entity } from '../../../src/ha/registry/entity/types';
@@ -50,7 +51,8 @@ const createEngine = (options?: {
}): MotionEyeCameraManagerEngine => {
return new MotionEyeCameraManagerEngine(
new EntityRegistryManagerMock(options?.entities ?? [createEntity()]),
mock<StateWatcher>(),
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
options?.walker ?? new BrowseMediaWalker(),
new ResolvedMediaCache(),
options?.requestCache ?? new CameraManagerRequestCache(),
@@ -73,7 +75,8 @@ const createMotionEyeStore = async (
await camera.initialize({
hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock([entity]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
camera.setID(options?.cameraID ?? 'camera-1');
const store = new CameraManagerStore();
@@ -497,7 +500,8 @@ describe('MotionEyeCameraManagerEngine', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock([createEntity()]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
camera.setID('camera-1');
const store = new CameraManagerStore();
@@ -545,7 +549,8 @@ describe('MotionEyeCameraManagerEngine', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock([createEntity()]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
camera.setID('camera-1');
const store = new CameraManagerStore();
@@ -599,7 +604,8 @@ describe('MotionEyeCameraManagerEngine', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock([createEntity()]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
camera.setID('camera-1');
const store = new CameraManagerStore();
+72 -36
View File
@@ -4,7 +4,8 @@ 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 { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
import { DeviceRegistryManager } from '../../../src/ha/registry/device';
import { EntityRegistryManagerLive } from '../../../src/ha/registry/entity';
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
@@ -105,7 +106,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager: mock<EntityRegistryManagerLive>(),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
}),
).rejects.toThrowError('Could not find camera entity');
});
@@ -129,7 +131,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
}),
).rejects.toThrowError('Could not initialize Reolink camera');
});
@@ -153,7 +156,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
}),
).rejects.toThrowError('Could not initialize Reolink camera');
});
@@ -169,7 +173,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getChannel()).toBe(0);
@@ -201,7 +206,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getChannel()).toBe(3);
@@ -224,7 +230,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getChannel()).toBe(7);
@@ -247,7 +254,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getChannel()).toBe(0);
@@ -270,7 +278,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getChannel()).toBe(7);
@@ -295,7 +304,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getChannel()).toBe(0);
@@ -327,7 +337,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getChannel()).toBe(0);
@@ -359,7 +370,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getChannel()).toBe(0);
@@ -391,7 +403,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager,
deviceRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getChannel()).toBe(0);
@@ -410,7 +423,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
@@ -449,7 +463,8 @@ describe('ReolinkCamera', () => {
}),
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
@@ -475,7 +490,8 @@ describe('ReolinkCamera', () => {
}),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
@@ -508,7 +524,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
@@ -695,7 +712,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -727,7 +745,8 @@ describe('ReolinkCamera', () => {
buttonEntityPTZLeft,
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
await camera.executePTZAction(executor, 'left', { phase: 'start' });
@@ -754,7 +773,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -795,7 +815,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -821,7 +842,8 @@ describe('ReolinkCamera', () => {
}),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -852,7 +874,8 @@ describe('ReolinkCamera', () => {
hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -875,7 +898,8 @@ describe('ReolinkCamera', () => {
numberEntityZoom,
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
@@ -902,7 +926,8 @@ describe('ReolinkCamera', () => {
}),
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getCapabilities()?.getPTZCapabilities()).toBeNull();
@@ -924,7 +949,8 @@ describe('ReolinkCamera', () => {
numberEntityZoom,
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
@@ -946,7 +972,8 @@ describe('ReolinkCamera', () => {
numberEntityZoom,
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -984,7 +1011,8 @@ describe('ReolinkCamera', () => {
numberEntityZoom,
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -1022,7 +1050,8 @@ describe('ReolinkCamera', () => {
numberEntityZoom,
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -1060,7 +1089,8 @@ describe('ReolinkCamera', () => {
numberEntityZoom,
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -1098,7 +1128,8 @@ describe('ReolinkCamera', () => {
numberEntityZoom,
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -1127,7 +1158,8 @@ describe('ReolinkCamera', () => {
numberEntityZoom,
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -1155,7 +1187,8 @@ describe('ReolinkCamera', () => {
numberEntityZoom,
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -1180,7 +1213,8 @@ describe('ReolinkCamera', () => {
numberEntityZoom,
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -1211,7 +1245,8 @@ describe('ReolinkCamera', () => {
buttonEntityPTZStop,
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -1233,7 +1268,8 @@ describe('ReolinkCamera', () => {
numberEntityZoom,
]),
deviceRegistryManager: mock<DeviceRegistryManager>(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -23,19 +23,20 @@ import {
QueryReturnType,
QueryType,
} from '../../../src/camera-manager/types';
import { QuerySource } from '../../../src/query-source';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
import { BrowseMedia, browseMediaSchema } from '../../../src/ha/browse-media/types';
import { BrowseMediaWalker } from '../../../src/ha/browse-media/walker';
import { DeviceRegistryManager } from '../../../src/ha/registry/device';
import { EntityRegistryManager } from '../../../src/ha/registry/entity/types';
import { ResolvedMediaCache } from '../../../src/ha/resolved-media';
import { homeAssistantWSRequest } from '../../../src/ha/ws-request';
import { QuerySource } from '../../../src/query-source';
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
import {
createInitializedCamera,
createCameraConfig,
createHASS,
createInitializedCamera,
createRegistryEntity,
createStore,
} from '../../test-utils';
@@ -186,7 +187,8 @@ const createEngine = (options?: {
return new ReolinkCameraManagerEngine(
options?.entityRegistryManager ?? new EntityRegistryManagerMock(),
mock<DeviceRegistryManager>(),
mock<StateWatcher>(),
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
options?.browseMediaManager ?? new BrowseMediaWalker(),
new ResolvedMediaCache(),
new CameraManagerRequestCache(),
+3
View File
@@ -5,6 +5,7 @@ import { Capabilities } from '../../src/camera-manager/capabilities.js';
import { CameraManagerEngineFactory } from '../../src/camera-manager/engine-factory.js';
import { CameraManagerStore } from '../../src/camera-manager/store.js';
import { Engine } from '../../src/camera-manager/types.js';
import { EventWatcherSubscriptionInterface } from '../../src/card-controller/hass/event-watcher.js';
import { StateWatcherSubscriptionInterface } from '../../src/card-controller/hass/state-watcher.js';
import { DeviceRegistryManager } from '../../src/ha/registry/device/index.js';
import { EntityRegistryManager } from '../../src/ha/registry/entity/types.js';
@@ -31,10 +32,12 @@ describe('CameraManagerStore', async () => {
const engineGeneric = await engineFactory.createEngine(Engine.Generic, {
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
resolvedMediaCache: mock<ResolvedMediaCache>(),
});
const engineFrigate = await engineFactory.createEngine(Engine.Frigate, {
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
resolvedMediaCache: mock<ResolvedMediaCache>(),
});
+34 -17
View File
@@ -3,7 +3,8 @@ 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 { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
@@ -78,7 +79,8 @@ describe('TPLinkCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
}),
).rejects.toThrowError('Could not find camera entity');
});
@@ -94,7 +96,8 @@ describe('TPLinkCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock(),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
}),
).rejects.toThrowError('Could not find camera entity');
});
@@ -112,7 +115,8 @@ describe('TPLinkCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getEntity()).toBe(cameraEntity);
@@ -129,7 +133,8 @@ describe('TPLinkCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getEntity()).toBe(cameraEntity);
@@ -146,7 +151,8 @@ describe('TPLinkCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
@@ -175,7 +181,8 @@ describe('TPLinkCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
expect(camera.getCapabilities()?.getPTZCapabilities()).toEqual({
@@ -198,7 +205,8 @@ describe('TPLinkCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: new EntityRegistryManagerMock([cameraEntity]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -219,7 +227,8 @@ describe('TPLinkCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -242,7 +251,8 @@ describe('TPLinkCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -262,7 +272,8 @@ describe('TPLinkCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -290,7 +301,8 @@ describe('TPLinkCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -318,7 +330,8 @@ describe('TPLinkCamera', () => {
await camera.initialize({
hass: createHASS(),
entityRegistryManager: ptzPopulatedEntityRegistryManager,
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -357,7 +370,8 @@ describe('TPLinkCamera', () => {
cameraEntity,
buttonEntityPanLeft,
]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
await camera.executePTZAction(executor, 'left', { phase: 'start' });
@@ -395,7 +409,8 @@ describe('TPLinkCamera', () => {
cameraWithDifferentUniqueId,
buttonEntityPanLeft,
]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
// Should not find PTZ entities since unique_id doesn't end with _live_view
@@ -420,7 +435,8 @@ describe('TPLinkCamera', () => {
cameraWithoutUniqueId,
buttonEntityPanLeft,
]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
// Should not find PTZ entities since camera has no unique_id
@@ -442,7 +458,8 @@ describe('TPLinkCamera', () => {
cameraEntity,
buttonEntityPanLeft, // Only left button available
]),
stateWatcher: mock<StateWatcher>(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
});
const executor = mock<ActionsExecutor>();
@@ -2,7 +2,8 @@ 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 { EventWatcherSubscriptionInterface } from '../../../src/card-controller/hass/event-watcher';
import { StateWatcherSubscriptionInterface } from '../../../src/card-controller/hass/state-watcher';
import { EntityRegistryManagerMock } from '../../ha/registry/entity/mock';
import { createCameraConfig, createHASS, createRegistryEntity } from '../../test-utils';
@@ -11,7 +12,8 @@ const createEngine = (options?: {
}): TPLinkCameraManagerEngine => {
return new TPLinkCameraManagerEngine(
options?.entityRegistryManager ?? new EntityRegistryManagerMock(),
mock<StateWatcher>(),
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
);
};
@@ -0,0 +1,133 @@
import { HassEvent } from 'home-assistant-js-websocket';
import { describe, expect, it, vi } from 'vitest';
import { EventWatcher } from '../../../src/card-controller/hass/event-watcher';
import { HomeAssistant } from '../../../src/ha/types';
import { createHASS } from '../../test-utils';
const fireEvent = (hass: HomeAssistant, event: HassEvent, n = 0): void => {
const mock = vi.mocked(hass.connection.subscribeEvents).mock;
expect(mock.calls.length).greaterThan(n);
// subscribeEvents(callback, event_type) -- callback is the first argument.
mock.calls[n][0]?.(event);
};
const createHassEvent = (event_type: string, data: object = {}): HassEvent => ({
event_type,
data: data as { [key: string]: string },
origin: 'LOCAL',
time_fired: '2026-05-25T00:00:00Z',
context: { id: 'ctx', user_id: null, parent_id: null },
});
describe('EventWatcher', () => {
it('opens a single WS subscription per event_type regardless of subscribers', async () => {
const watcher = new EventWatcher();
const hass = createHASS();
await watcher.subscribe(hass, { event_type: 'zha_event', callback: vi.fn() });
await watcher.subscribe(hass, { event_type: 'zha_event', callback: vi.fn() });
expect(hass.connection.subscribeEvents).toBeCalledTimes(1);
expect(vi.mocked(hass.connection.subscribeEvents).mock.calls[0][1]).toBe(
'zha_event',
);
});
it('opens separate WS subscriptions for distinct event_types', async () => {
const watcher = new EventWatcher();
const hass = createHASS();
await watcher.subscribe(hass, { event_type: 'zha_event', callback: vi.fn() });
await watcher.subscribe(hass, { event_type: 'deconz_event', callback: vi.fn() });
expect(hass.connection.subscribeEvents).toBeCalledTimes(2);
});
it('only tears down the WS subscription when the last subscriber unsubscribes', async () => {
const watcher = new EventWatcher();
const hass = createHASS();
const unsub = vi.fn();
vi.mocked(hass.connection.subscribeEvents).mockResolvedValue(unsub);
const req1 = { event_type: 'zha_event', callback: vi.fn() };
const req2 = { event_type: 'zha_event', callback: vi.fn() };
await watcher.subscribe(hass, req1);
await watcher.subscribe(hass, req2);
await watcher.unsubscribe(req1);
expect(unsub).not.toBeCalled();
await watcher.unsubscribe(req2);
expect(unsub).toBeCalledTimes(1);
});
it('dispatches to all subscribers whose event_type matches', async () => {
const watcher = new EventWatcher();
const hass = createHASS();
const cb1 = vi.fn();
const cb2 = vi.fn();
await watcher.subscribe(hass, { event_type: 'zha_event', callback: cb1 });
await watcher.subscribe(hass, { event_type: 'zha_event', callback: cb2 });
fireEvent(hass, createHassEvent('zha_event', { command: 'press' }));
expect(cb1).toBeCalledWith({ command: 'press' });
expect(cb2).toBeCalledWith({ command: 'press' });
});
it('drops events whose event_type does not match the request', async () => {
const watcher = new EventWatcher();
const hass = createHASS();
const cb = vi.fn();
await watcher.subscribe(hass, { event_type: 'zha_event', callback: cb });
// Inject an unrelated event into the shared dispatcher.
fireEvent(hass, createHassEvent('other_event', { x: 1 }));
expect(cb).not.toBeCalled();
});
it('gates dispatch on the request matcher when provided', async () => {
const watcher = new EventWatcher();
const hass = createHASS();
const cb = vi.fn();
const matcher = vi.fn((data: unknown) => (data as { x?: number }).x === 1);
await watcher.subscribe(hass, { event_type: 'zha_event', matcher, callback: cb });
fireEvent(hass, createHassEvent('zha_event', { x: 1 }));
fireEvent(hass, createHassEvent('zha_event', { x: 2 }));
expect(matcher).toBeCalledTimes(2);
expect(cb).toBeCalledTimes(1);
expect(cb).toBeCalledWith({ x: 1 });
});
it('handles unsubscribe during a still-pending subscribe without leaking', async () => {
const watcher = new EventWatcher();
const hass = createHASS();
const unsub = vi.fn();
let resolveSubscription: ((cb: () => Promise<void>) => void) | undefined;
const subscriptionPromise = new Promise<() => Promise<void>>((resolve) => {
resolveSubscription = resolve;
});
vi.mocked(hass.connection.subscribeEvents).mockReturnValue(subscriptionPromise);
const req = { event_type: 'zha_event', callback: vi.fn() };
const subscribePromise = watcher.subscribe(hass, req);
// Unsubscribe before the underlying connection has resolved.
const unsubscribePromise = watcher.unsubscribe(req);
// Resolve the connection -- the watcher should now have the unsub fn and
// call it as part of completing the unsubscribe.
resolveSubscription?.(unsub);
await subscribePromise;
await unsubscribePromise;
expect(unsub).toBeCalledTimes(1);
});
});
@@ -1,5 +1,6 @@
import { STATE_RUNNING, STATE_STARTING } from 'home-assistant-js-websocket';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { EventWatcher } from '../../../src/card-controller/hass/event-watcher';
import { HASSManager } from '../../../src/card-controller/hass/hass-manager';
import { StateWatcher } from '../../../src/card-controller/hass/state-watcher';
import {
@@ -29,6 +30,11 @@ describe('HASSManager', () => {
expect(manager.getStateWatcher()).toEqual(expect.any(StateWatcher));
});
it('should get event watcher', () => {
const manager = new HASSManager(createCardAPI());
expect(manager.getEventWatcher()).toEqual(expect.any(EventWatcher));
});
it('should get hass after set', () => {
const manager = new HASSManager(createCardAPI());
const hass = createHASS();
+16 -16
View File
@@ -25,7 +25,7 @@ vi.mock('lodash-es', async () => ({
const baseTriggersConfig: TriggersOptions = {
untrigger_delay_seconds: 10,
untrigger_force_seconds: 0,
signal_hold_seconds: 0,
event_hold_seconds: 0,
filter_selected_camera: false,
show_trigger_status: false,
actions: {
@@ -810,7 +810,7 @@ describe('TriggersManager', () => {
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalledTimes(1);
});
it('should auto-untrigger after the delay on a signal event', async () => {
it('should auto-untrigger after the delay on a momentary event', async () => {
const api = createTriggerAPI({
config: {
actions: { trigger: 'none', untrigger: 'default' },
@@ -821,7 +821,7 @@ describe('TriggersManager', () => {
await manager.handleCameraEvent({
cameraID: 'camera_1',
id: 'event.doorbell',
type: 'signal',
type: 'momentary',
});
expect(manager.isTriggered()).toBeTruthy();
@@ -835,7 +835,7 @@ describe('TriggersManager', () => {
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
});
it('should auto-untrigger immediately on a signal when delay is 0', async () => {
it('should auto-untrigger immediately on a momentary event when delay is 0', async () => {
const api = createTriggerAPI({
config: {
untrigger_delay_seconds: 0,
@@ -847,7 +847,7 @@ describe('TriggersManager', () => {
await manager.handleCameraEvent({
cameraID: 'camera_1',
id: 'event.doorbell',
type: 'signal',
type: 'momentary',
});
await flushPromises();
@@ -855,7 +855,7 @@ describe('TriggersManager', () => {
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
});
it('should not auto-untrigger from a signal while a continuous source remains active', async () => {
it('should not auto-untrigger from a momentary event while a continuous source remains active', async () => {
const api = createTriggerAPI({
config: {
actions: { trigger: 'none', untrigger: 'default' },
@@ -874,7 +874,7 @@ describe('TriggersManager', () => {
await manager.handleCameraEvent({
cameraID: 'camera_1',
id: 'event.doorbell',
type: 'signal',
type: 'momentary',
});
vi.setSystemTime(add(start, { seconds: 10 }));
@@ -886,11 +886,11 @@ describe('TriggersManager', () => {
expect(api.getViewManager().setViewDefaultWithNewQuery).not.toBeCalled();
});
it('should add signal_hold_seconds on top of untrigger_delay_seconds for signals', async () => {
it('should add event_hold_seconds on top of untrigger_delay_seconds for momentary events', async () => {
const api = createTriggerAPI({
config: {
untrigger_delay_seconds: 5,
signal_hold_seconds: 30,
event_hold_seconds: 30,
actions: { trigger: 'none', untrigger: 'default' },
},
});
@@ -899,7 +899,7 @@ describe('TriggersManager', () => {
await manager.handleCameraEvent({
cameraID: 'camera_1',
id: 'event.doorbell',
type: 'signal',
type: 'momentary',
});
// At 34s the additive 35s window is still active.
@@ -916,11 +916,11 @@ describe('TriggersManager', () => {
expect(api.getViewManager().setViewDefaultWithNewQuery).toBeCalled();
});
it('should not apply signal_hold_seconds to non-signal events', async () => {
it('should not apply event_hold_seconds to non-momentary events', async () => {
const api = createTriggerAPI({
config: {
untrigger_delay_seconds: 5,
signal_hold_seconds: 30,
event_hold_seconds: 30,
actions: { trigger: 'none', untrigger: 'default' },
},
});
@@ -1305,9 +1305,9 @@ describe('TriggersManager', () => {
expect(manager.isTriggered()).toBeTruthy();
});
it('should not reset the untrigger timer when a filtered-out signal arrives', async () => {
// Guards the `if (handled)` branch on the signal synthesis: a
// filter-rejected signal must not call the internal 'end', which would
it('should not reset the untrigger timer when a filtered-out momentary event arrives', async () => {
// Guards the `if (handled)` branch on the momentary-event synthesis: a
// filter-rejected momentary event must not call the internal 'end', which would
// otherwise reset an already-running untrigger-delay timer.
const api = createTriggerAPI({
config: {
@@ -1341,7 +1341,7 @@ describe('TriggersManager', () => {
await manager.handleCameraEvent({
cameraID: 'camera_1',
id: 'event.doorbell',
type: 'signal',
type: 'momentary',
});
// At t=10 (the original delay's deadline), the timer should fire.
+148
View File
@@ -4052,6 +4052,154 @@ describe('should handle version specific upgrades', () => {
});
});
describe('cameras[].triggers.events string[] → triggers.media_events', () => {
it('migrates a legacy string array to media_events', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [
{
camera_entity: 'camera.office',
triggers: { events: ['events', 'clips'] },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.cameras[0].triggers).toEqual({
media_events: ['events', 'clips'],
});
postUpgradeChecks(config);
});
it('migrates an empty legacy array', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office', triggers: { events: [] } }],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.cameras[0].triggers).toEqual({ media_events: [] });
postUpgradeChecks(config);
});
it('leaves the new object-array shape untouched', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [
{
camera_entity: 'camera.office',
triggers: { events: [{ event_type: 'zha_event' }] },
},
],
};
// The new shape might still trigger OTHER upgrades to fire on the
// config, so we don't assert the overall upgrade result -- only that
// this specific field is not touched.
upgradeConfig(config);
expect(config.cameras[0].triggers).toEqual({
events: [{ event_type: 'zha_event' }],
});
});
it('drops legacy events but keeps an existing media_events untouched', () => {
// Both fields present is implausible in real user configs, but if it
// happens we still must remove the legacy `events: string[]` because
// the new schema would reject it; the explicit `media_events` wins.
const config = {
type: 'custom:advanced-camera-card',
cameras: [
{
camera_entity: 'camera.office',
triggers: {
events: ['snapshots'],
media_events: ['clips'],
},
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.cameras[0].triggers).toEqual({
media_events: ['clips'],
});
postUpgradeChecks(config);
});
it('is idempotent across repeated runs', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [
{
camera_entity: 'camera.office',
triggers: { events: ['events'] },
},
],
};
expect(upgradeConfig(config)).toBeTruthy();
expect(upgradeConfig(config)).toBeFalsy();
expect(config.cameras[0].triggers).toEqual({ media_events: ['events'] });
});
it('migrates per camera independently', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [
{ camera_entity: 'camera.a', triggers: { events: ['clips'] } },
{
camera_entity: 'camera.b',
triggers: { events: [{ event_type: 'zha_event' }] },
},
{ camera_entity: 'camera.c' },
],
};
upgradeConfig(config);
expect(config.cameras[0].triggers).toEqual({ media_events: ['clips'] });
expect(config.cameras[1].triggers).toEqual({
events: [{ event_type: 'zha_event' }],
});
expect(config.cameras[2].triggers).toBeUndefined();
});
it('migrates a legacy cameras_global.triggers.events string array', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
cameras_global: {
triggers: {
events: ['clips', 'snapshots'],
},
},
};
expect(upgradeConfig(config)).toBeTruthy();
expect(config.cameras_global.triggers).toEqual({
media_events: ['clips', 'snapshots'],
});
postUpgradeChecks(config);
});
it('leaves cameras_global new-shape events untouched', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office' }],
cameras_global: {
triggers: {
events: [{ event_type: 'zha_event' }],
},
},
};
upgradeConfig(config);
expect(config.cameras_global.triggers).toEqual({
events: [{ event_type: 'zha_event' }],
});
});
it('is a no-op when triggers is not an object (malformed user config)', () => {
const config = {
type: 'custom:advanced-camera-card',
cameras: [{ camera_entity: 'camera.office', triggers: 'not-an-object' }],
};
upgradeConfig(config);
expect(config.cameras[0].triggers).toBe('not-an-object');
});
});
describe('live_substream_{on,off,select} → substream_{on,off}', () => {
it('rewrites live_substream_on to substream_on in an automation', () => {
const config = {
+2 -1
View File
@@ -57,6 +57,7 @@ describe('config defaults', () => {
doorbell: false,
entities: [],
events: [],
media_events: [],
motion: false,
occupancy: false,
reviews: {
@@ -550,7 +551,7 @@ describe('config defaults', () => {
},
filter_selected_camera: true,
show_trigger_status: false,
signal_hold_seconds: 30,
event_hold_seconds: 30,
untrigger_delay_seconds: 0,
untrigger_force_seconds: 0,
},
+75
View File
@@ -0,0 +1,75 @@
import { describe, expect, it } from 'vitest';
import { matchesEventData } from '../../src/ha/event-data-match';
describe('matchesEventData', () => {
describe('non-object data', () => {
it.each([['string'], [42], [true], [null], [undefined]])(
'rejects non-object data (%s) with a non-empty filter',
(data) => {
expect(matchesEventData({ a: 1 }, data)).toBe(false);
},
);
});
it('matches when every filter key matches', () => {
expect(matchesEventData({ command: 'press' }, { command: 'press', extra: 1 })).toBe(
true,
);
});
it('rejects when a filter value differs', () => {
expect(matchesEventData({ command: 'press' }, { command: 'release' })).toBe(false);
});
it('rejects when a filter key is missing from data', () => {
expect(matchesEventData({ command: 'press' }, { other: 'press' })).toBe(false);
});
it('ignores extra keys in data', () => {
expect(matchesEventData({ a: 1 }, { a: 1, b: 2, c: 3 })).toBe(true);
});
it('matches nested objects as a subset', () => {
expect(
matchesEventData(
{ device: { id: 'abc' } },
{ device: { id: 'abc', name: 'Front Door' } },
),
).toBe(true);
});
it('rejects nested objects when a nested key differs', () => {
expect(matchesEventData({ device: { id: 'abc' } }, { device: { id: 'xyz' } })).toBe(
false,
);
});
it('matches arrays element-wise', () => {
expect(matchesEventData({ tags: ['a', 'b'] }, { tags: ['a', 'b'] })).toBe(true);
});
it('matches arrays as a subset by index (filter shorter than data)', () => {
// Same partial-match semantics lodash uses for objects: a shorter filter
// array matches if every index it specifies matches in data. Useful when
// the user only cares about the first N values.
expect(matchesEventData({ tags: ['a'] }, { tags: ['a', 'b'] })).toBe(true);
});
it('rejects arrays with mismatched elements at the same index', () => {
expect(matchesEventData({ tags: ['a', 'c'] }, { tags: ['a', 'b'] })).toBe(false);
});
it('rejects when filter array is longer than data array', () => {
expect(matchesEventData({ tags: ['a', 'b'] }, { tags: ['a'] })).toBe(false);
});
it('distinguishes null from undefined', () => {
expect(matchesEventData({ a: null }, { a: null })).toBe(true);
expect(matchesEventData({ a: null }, { a: 0 })).toBe(false);
});
it('accepts an empty filter against any object', () => {
expect(matchesEventData({}, {})).toBe(true);
expect(matchesEventData({}, { anything: 1, nested: { x: 2 } })).toBe(true);
});
});
+4 -4
View File
@@ -36,14 +36,14 @@ describe('getTriggerEventType', () => {
});
describe('for event entities', () => {
it('returns "signal" for a transition between two real timestamps', () => {
it('returns "momentary" for a transition between two real timestamps', () => {
expect(
getTriggerEventType({
entityID: 'event.front_door_doorbell',
oldState: createStateEntity({ state: '2026-05-24T12:00:00.000+00:00' }),
newState: createStateEntity({ state: '2026-05-24T12:00:05.123+00:00' }),
}),
).toBe('signal');
).toBe('momentary');
});
it('returns null when there is no old state', () => {
@@ -65,14 +65,14 @@ describe('getTriggerEventType', () => {
).toBeNull();
});
it('returns "signal" when the old state is unknown (first fire after startup)', () => {
it('returns "momentary" when the old state is unknown (first fire after startup)', () => {
expect(
getTriggerEventType({
entityID: 'event.front_door_doorbell',
oldState: createStateEntity({ state: 'unknown' }),
newState: createStateEntity({ state: '2026-05-24T12:00:05.123+00:00' }),
}),
).toBe('signal');
).toBe('momentary');
});
it.each([['unavailable'], ['unknown']])(
+14
View File
@@ -38,6 +38,7 @@ import { FoldersManager } from '../src/card-controller/folders/manager';
import { FolderQuery } from '../src/card-controller/folders/types';
import { FullscreenManager } from '../src/card-controller/fullscreen/fullscreen-manager';
import { HASSManager } from '../src/card-controller/hass/hass-manager';
import { EventWatcherSubscriptionInterface } from '../src/card-controller/hass/event-watcher';
import { StateWatcherSubscriptionInterface } from '../src/card-controller/hass/state-watcher';
import { InitializationManager } from '../src/card-controller/initialization-manager';
import { InteractionManager } from '../src/card-controller/interaction-manager';
@@ -124,6 +125,7 @@ export const createInitializedCamera = async (
await camera.initialize({
hass: createHASS(),
stateWatcher: mock<StateWatcherSubscriptionInterface>(),
eventWatcher: mock<EventWatcherSubscriptionInterface>(),
...(capabilities ? { capabilityOptions: { capabilities } } : {}),
});
return camera;
@@ -143,6 +145,7 @@ export const createHASS = (states?: HassEntities, user?: CurrentUser): HomeAssis
// startup state still represent a "ready" instance.
hass.config.state = STATE_RUNNING;
hass.connection.subscribeMessage = vi.fn();
hass.connection.subscribeEvents = vi.fn();
// ha-nunjucks calls sendMessagePromise to fetch label registry; return empty array to prevent crash.
hass.connection.sendMessagePromise = vi.fn().mockResolvedValue([]);
@@ -282,6 +285,7 @@ export const createStore = (
cameraProps.engine ??
new GenericCameraManagerEngine(
mock<StateWatcherSubscriptionInterface>(),
mock<EventWatcherSubscriptionInterface>(),
mock<EntityRegistryManager>(),
eventCallback,
),
@@ -737,6 +741,16 @@ export const callStateWatcherCallback = (
mock.calls[n][0](diff);
};
export const callEventWatcherCallback = (
eventWatcher: EventWatcherSubscriptionInterface,
data: unknown,
n = 0,
): void => {
const mock = vi.mocked(eventWatcher.subscribe).mock;
expect(mock.calls.length).greaterThan(n);
mock.calls[n][1].callback(data);
};
/**
* Flush resolved promises.
*/