feat: Add support for Frigate reviews / detections [initial PR] (#2315)
- Add support for Frigate reviews / detections. - Add support for GenAI metadata. - Significant internal refactor to more flexible "UnifiedQuery" to allow mixing cameras with simple metadata and review metadata (e.g. a timeline view of a Frigate camera with reviews, and a Reolink camera with simple metadata). - Add support for folder media as camera media. There are a few more PRs to commit prior to this going live, but commiting this for now due to the scale of the change. BREAKING CHANGE: `media_type` and `events_type` are retired under `live`, `viewer` and `timeline` configuration sections, instead media type is associated (once) with the camera under `media`.
This commit is contained in:
@@ -0,0 +1,152 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { TemplateRenderer } from '../../../src/card-controller/templates/index';
|
||||
import { createHASS } from '../../test-utils';
|
||||
|
||||
describe('TemplateRenderer', () => {
|
||||
describe('renderRecursively', () => {
|
||||
it('should render string templates with camera context', () => {
|
||||
const renderer = new TemplateRenderer();
|
||||
const hass = createHASS();
|
||||
|
||||
const result = renderer.renderRecursively(hass, 'Camera: {{ acc.camera }}', {
|
||||
conditionState: { camera: 'camera.office' },
|
||||
});
|
||||
expect(result).toBe('Camera: camera.office');
|
||||
});
|
||||
|
||||
it('should render string templates with view context', () => {
|
||||
const renderer = new TemplateRenderer();
|
||||
const hass = createHASS();
|
||||
|
||||
const result = renderer.renderRecursively(hass, 'View: {{ acc.view }}', {
|
||||
conditionState: { view: 'live' },
|
||||
});
|
||||
expect(result).toBe('View: live');
|
||||
});
|
||||
|
||||
it('should render string templates with full advanced_camera_card context', () => {
|
||||
const renderer = new TemplateRenderer();
|
||||
const hass = createHASS();
|
||||
|
||||
const result = renderer.renderRecursively(
|
||||
hass,
|
||||
'{{ advanced_camera_card.camera }} - {{ advanced_camera_card.view }}',
|
||||
{
|
||||
conditionState: { camera: 'camera.front', view: 'clips' },
|
||||
},
|
||||
);
|
||||
expect(result).toBe('camera.front - clips');
|
||||
});
|
||||
|
||||
it('should render templates in arrays', () => {
|
||||
const renderer = new TemplateRenderer();
|
||||
const hass = createHASS();
|
||||
|
||||
const result = renderer.renderRecursively(
|
||||
hass,
|
||||
['{{ acc.camera }}', 'static', '{{ acc.view }}'],
|
||||
{ conditionState: { camera: 'camera.office', view: 'live' } },
|
||||
);
|
||||
expect(result).toEqual(['camera.office', 'static', 'live']);
|
||||
});
|
||||
|
||||
it('should render templates in object values', () => {
|
||||
const renderer = new TemplateRenderer();
|
||||
const hass = createHASS();
|
||||
|
||||
const result = renderer.renderRecursively(
|
||||
hass,
|
||||
{ camera: '{{ acc.camera }}', view: '{{ acc.view }}', static: 'value' },
|
||||
{ conditionState: { camera: 'camera.office', view: 'live' } },
|
||||
);
|
||||
expect(result).toEqual({ camera: 'camera.office', view: 'live', static: 'value' });
|
||||
});
|
||||
|
||||
it('should render templates in nested objects', () => {
|
||||
const renderer = new TemplateRenderer();
|
||||
const hass = createHASS();
|
||||
|
||||
const result = renderer.renderRecursively(
|
||||
hass,
|
||||
{ outer: { inner: '{{ acc.camera }}' } },
|
||||
{ conditionState: { camera: 'camera.office' } },
|
||||
);
|
||||
expect(result).toEqual({ outer: { inner: 'camera.office' } });
|
||||
});
|
||||
|
||||
it('should return non-string/array/object values unchanged', () => {
|
||||
const renderer = new TemplateRenderer();
|
||||
const hass = createHASS();
|
||||
|
||||
expect(renderer.renderRecursively(hass, 42)).toBe(42);
|
||||
expect(renderer.renderRecursively(hass, true)).toBe(true);
|
||||
expect(renderer.renderRecursively(hass, null)).toBe(null);
|
||||
});
|
||||
|
||||
it('should render strings without templates unchanged', () => {
|
||||
const renderer = new TemplateRenderer();
|
||||
const hass = createHASS();
|
||||
|
||||
expect(renderer.renderRecursively(hass, 'hello world')).toBe('hello world');
|
||||
});
|
||||
|
||||
it('should render with triggerData context', () => {
|
||||
const renderer = new TemplateRenderer();
|
||||
const hass = createHASS();
|
||||
|
||||
const result = renderer.renderRecursively(hass, '{{ acc.trigger.camera.to }}', {
|
||||
triggerData: { camera: { from: 'camera.front', to: 'camera.backyard' } },
|
||||
});
|
||||
expect(result).toBe('camera.backyard');
|
||||
});
|
||||
|
||||
it('should render with mediaData context', () => {
|
||||
const renderer = new TemplateRenderer();
|
||||
const hass = createHASS();
|
||||
|
||||
const result = renderer.renderRecursively(
|
||||
hass,
|
||||
'Title: {{ acc.media.title }}, Folder: {{ acc.media.is_folder }}',
|
||||
{
|
||||
mediaData: { title: 'Test Media', is_folder: false },
|
||||
},
|
||||
);
|
||||
expect(result).toBe('Title: Test Media, Folder: false');
|
||||
});
|
||||
|
||||
it('should render with combined context options', () => {
|
||||
const renderer = new TemplateRenderer();
|
||||
const hass = createHASS();
|
||||
|
||||
const result = renderer.renderRecursively(
|
||||
hass,
|
||||
'{{ acc.camera }} - {{ acc.media.title }}',
|
||||
{
|
||||
conditionState: { camera: 'camera.office' },
|
||||
mediaData: { title: 'My Video', is_folder: false },
|
||||
},
|
||||
);
|
||||
expect(result).toBe('camera.office - My Video');
|
||||
});
|
||||
|
||||
it('should return undefined context when no options provided', () => {
|
||||
const renderer = new TemplateRenderer();
|
||||
const hass = createHASS();
|
||||
|
||||
// Without options, templates referencing acc should render empty.
|
||||
const result = renderer.renderRecursively(hass, 'Value: {{ acc.camera }}');
|
||||
expect(result).toBe('Value:');
|
||||
});
|
||||
|
||||
it('should return undefined context when options have no relevant data', () => {
|
||||
const renderer = new TemplateRenderer();
|
||||
const hass = createHASS();
|
||||
|
||||
// Empty conditionState without camera or view should not create context.
|
||||
const result = renderer.renderRecursively(hass, 'Value: {{ acc.camera }}', {
|
||||
conditionState: {},
|
||||
});
|
||||
expect(result).toBe('Value:');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user