Add new method of casting live cameras based on dashboards.

This commit is contained in:
Dermot Duffy
2023-10-09 22:04:59 -07:00
parent aa9d061331
commit bc71709cc8
10 changed files with 355 additions and 62 deletions
@@ -143,52 +143,10 @@ describe('MediaPlayerManager', () => {
describe('should play', () => {
describe('live', () => {
it('successfully', async () => {
it('without camera config', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({
camera_entity: 'camera.foo',
}),
);
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
title: 'camera title',
icon: 'icon',
});
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
const hass = createHASS({
'camera.foo': createStateEntity({
attributes: {
entity_picture: 'http://thumbnail',
},
}),
});
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const manager = new MediaPlayerManager(api);
await manager.playLive('media_player.foo', 'camera');
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
'media_player',
'play_media',
{
entity_id: 'media_player.foo',
media_content_id: 'media-source://camera/camera.foo',
media_content_type: 'application/vnd.apple.mpegurl',
extra: {
title: 'camera title',
thumb: 'http://thumbnail',
},
},
);
});
it('without camera_entity', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({}),
);
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(null);
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new MediaPlayerManager(api);
@@ -198,30 +156,176 @@ describe('MediaPlayerManager', () => {
expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled();
});
it('without title and thumbnail', async () => {
describe('using standard method', () => {
it('successfully', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({
camera_entity: 'camera.foo',
}),
);
vi.mocked(cameraManager.getCameraMetadata).mockReturnValue({
title: 'camera title',
icon: 'icon',
});
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
const hass = createHASS({
'camera.foo': createStateEntity({
attributes: {
entity_picture: 'http://thumbnail',
},
}),
});
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
const manager = new MediaPlayerManager(api);
await manager.playLive('media_player.foo', 'camera');
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
'media_player',
'play_media',
{
entity_id: 'media_player.foo',
media_content_id: 'media-source://camera/camera.foo',
media_content_type: 'application/vnd.apple.mpegurl',
extra: {
title: 'camera title',
thumb: 'http://thumbnail',
},
},
);
});
it('without camera_entity', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({}),
);
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new MediaPlayerManager(api);
await manager.playLive('media_player.foo', 'camera');
expect(api.getHASSManager().getHASS()?.callService).not.toBeCalled();
});
it('without title and thumbnail', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({
camera_entity: 'camera.foo',
}),
);
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new MediaPlayerManager(api);
await manager.playLive('media_player.foo', 'camera');
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
'media_player',
'play_media',
{
entity_id: 'media_player.foo',
media_content_id: 'media-source://camera/camera.foo',
media_content_type: 'application/vnd.apple.mpegurl',
extra: {},
},
);
});
});
describe('using dashboard method', () => {
it('successfully', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({
camera_entity: 'camera.foo',
cast: {
method: 'dashboard',
dashboard: {
dashboard_path: 'dashboard_path',
view_path: 'view_path',
},
},
}),
);
vi.mocked(api.getQueryStringManager().generateQueryString).mockReturnValue('');
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new MediaPlayerManager(api);
await manager.playLive('media_player.foo', 'camera');
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
'cast',
'show_lovelace_view',
{
entity_id: 'media_player.foo',
dashboard_path: 'dashboard_path',
view_path: 'view_path',
},
);
});
it('without hass', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({
camera_entity: 'camera.foo',
cast: {
method: 'dashboard',
dashboard: {
dashboard_path: 'dashboard_path',
view_path: 'view_path',
},
},
}),
);
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
const manager = new MediaPlayerManager(api);
await manager.playLive('media_player.foo', 'camera');
// No actual test can be performed here as nothing observable happens.
// This test serves only as code-coverage long-tail.
});
});
it('without required configuration', async () => {
const api = createCardAPI();
const cameraManager = createCameraManager();
vi.mocked(cameraManager.getStore().getCameraConfig).mockReturnValue(
createCameraConfig({
camera_entity: 'camera.foo',
cast: {
method: 'dashboard',
},
}),
);
vi.mocked(api.getCameraManager).mockReturnValue(cameraManager);
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
const manager = new MediaPlayerManager(api);
await manager.playLive('media_player.foo', 'camera');
expect(api.getHASSManager().getHASS()?.callService).toBeCalledWith(
'media_player',
'play_media',
{
entity_id: 'media_player.foo',
media_content_id: 'media-source://camera/camera.foo',
media_content_type: 'application/vnd.apple.mpegurl',
extra: {},
},
);
expect(
vi.mocked(api.getMessageManager().setMessageIfHigherPriority),
).toBeCalledWith({
type: 'error',
icon: 'mdi:cast',
message:
"Both 'dashboard_path' and 'view_path' parameters are required " +
"for the 'dashboard' cast method",
});
});
});
@@ -1,7 +1,11 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { createCardAPI } from '../test-utils';
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
import { mock } from 'vitest-mock-extended';
import { QueryStringManager } from '../../src/card-controller/query-string-manager';
import {
FrigateCardGeneralAction,
FrigateCardUserSpecifiedView,
} from '../../src/config/types';
import { createCardAPI } from '../test-utils';
const setQueryString = (qs: string): void => {
const location: Location = mock<Location>();
@@ -295,4 +299,74 @@ describe('QueryStringManager', () => {
expect(api.getActionsManager().executeAction).not.toBeCalled();
});
});
describe('should generate query string', () => {
describe('that require no arguments', () => {
it.each([
['camera_ui' as const],
['clip' as const],
['clips' as const],
['default' as const],
['diagnostics' as const],
['download' as const],
['expand' as const],
['image' as const],
['live' as const],
['menu_toggle' as const],
['recording' as const],
['recordings' as const],
['snapshot' as const],
['snapshots' as const],
['timeline' as const],
])('%s', (actionName: FrigateCardGeneralAction | FrigateCardUserSpecifiedView) => {
const manager = new QueryStringManager(createCardAPI());
expect(
manager.generateQueryString({
action: 'fire-dom-event',
frigate_card_action: actionName,
}),
).toBe(`frigate-card-action.${actionName}=`);
});
});
describe('that require camera argument', () => {
it.each([['camera_select' as const], ['live_substream_select' as const]])(
'%s',
(actionName: 'camera_select' | 'live_substream_select') => {
const manager = new QueryStringManager(createCardAPI());
expect(
manager.generateQueryString({
action: 'fire-dom-event',
frigate_card_action: actionName,
camera: 'camera',
}),
).toBe(`frigate-card-action.${actionName}=camera`);
},
);
});
it('that include a card_id', () => {
const manager = new QueryStringManager(createCardAPI());
expect(
manager.generateQueryString({
action: 'fire-dom-event',
frigate_card_action: 'clips',
card_id: 'card-id',
}),
).toBe(`frigate-card-action.card-id.clips=`);
});
it('that include an unsupported action', () => {
const spy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
const manager = new QueryStringManager(createCardAPI());
expect(
manager.generateQueryString({
action: 'fire-dom-event',
frigate_card_action: 'microphone_unmute',
}),
).toBeNull();
expect(spy).toBeCalled();
});
});
});