Merge pull request #1279 from dermotduffy/card.ts-refactor
Refactor `card.ts` and `types.ts` and add tests
This commit is contained in:
@@ -180,6 +180,29 @@ export const actionHandler = directive(
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export interface ActionEventTarget extends EventTarget {
|
||||||
|
addEventListener(
|
||||||
|
event: '@action',
|
||||||
|
listener: (this: ActionEventTarget, ev: CustomEvent<ActionHandlerDetail>) => void,
|
||||||
|
options?: AddEventListenerOptions | boolean,
|
||||||
|
): void;
|
||||||
|
addEventListener(
|
||||||
|
type: string,
|
||||||
|
callback: EventListenerOrEventListenerObject,
|
||||||
|
options?: AddEventListenerOptions | boolean,
|
||||||
|
): void;
|
||||||
|
removeEventListener(
|
||||||
|
event: '@action',
|
||||||
|
listener: (this: ActionEventTarget, ev: CustomEvent<ActionHandlerDetail>) => void,
|
||||||
|
options?: boolean | EventListenerOptions,
|
||||||
|
): void;
|
||||||
|
removeEventListener(
|
||||||
|
type: string,
|
||||||
|
callback: EventListenerOrEventListenerObject,
|
||||||
|
options?: boolean | EventListenerOptions,
|
||||||
|
): void;
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'action-handler-frigate-card': ActionHandler;
|
'action-handler-frigate-card': ActionHandler;
|
||||||
|
|||||||
@@ -1,50 +0,0 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
|
||||||
import { AutomationActions, FrigateCardError } from './types.js';
|
|
||||||
import { ConditionController } from './conditions.js';
|
|
||||||
import { Automation, Automations } from './types.js';
|
|
||||||
import { frigateCardHandleAction } from './utils/action.js';
|
|
||||||
import { localize } from './localize/localize.js';
|
|
||||||
|
|
||||||
const MAX_NESTED_AUTOMATION_EXECUTIONS = 10;
|
|
||||||
|
|
||||||
export class AutomationsControllerError extends FrigateCardError {}
|
|
||||||
|
|
||||||
export class AutomationsController {
|
|
||||||
protected _automations: Automations;
|
|
||||||
protected _priorEvaluations: Map<Automation, boolean> = new Map();
|
|
||||||
|
|
||||||
// A counter to avoid infinite loops, increases every time actions are run,
|
|
||||||
// decreases every time actions are complete.
|
|
||||||
protected _nestedAutomationExecutions = 0;
|
|
||||||
|
|
||||||
constructor(automations: Automations) {
|
|
||||||
this._automations = automations;
|
|
||||||
}
|
|
||||||
|
|
||||||
public execute(
|
|
||||||
element: HTMLElement,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
conditionController: ConditionController,
|
|
||||||
): void {
|
|
||||||
const actionsToRun: AutomationActions[] = [];
|
|
||||||
for (const automation of this._automations ?? []) {
|
|
||||||
const shouldExecute = conditionController.evaluateCondition(automation.conditions);
|
|
||||||
const actions = shouldExecute ? automation.actions : automation.actions_not;
|
|
||||||
const priorEvaluation = this._priorEvaluations.get(automation);
|
|
||||||
this._priorEvaluations.set(automation, shouldExecute);
|
|
||||||
if (shouldExecute !== priorEvaluation && actions) {
|
|
||||||
actionsToRun.push(actions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
++this._nestedAutomationExecutions;
|
|
||||||
if (this._nestedAutomationExecutions > MAX_NESTED_AUTOMATION_EXECUTIONS) {
|
|
||||||
throw new AutomationsControllerError(localize('error.too_many_automations'));
|
|
||||||
}
|
|
||||||
|
|
||||||
actionsToRun.forEach((actions) => {
|
|
||||||
frigateCardHandleAction(element, hass, {}, actions);
|
|
||||||
});
|
|
||||||
--this._nestedAutomationExecutions;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,8 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { CameraConfig, ExtendedHomeAssistant } from '../../types';
|
import { CameraConfig } from '../../config/types';
|
||||||
import { ViewMedia } from '../../view/media';
|
|
||||||
import {
|
|
||||||
CameraManagerMediaCapabilities,
|
|
||||||
DataQuery,
|
|
||||||
EventQuery,
|
|
||||||
PartialEventQuery,
|
|
||||||
CameraConfigs,
|
|
||||||
CameraManagerCameraCapabilities,
|
|
||||||
QueryType,
|
|
||||||
CameraEndpoint,
|
|
||||||
} from '../types';
|
|
||||||
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
|
||||||
import { CameraManagerEngine } from '../engine';
|
|
||||||
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
|
||||||
import { CameraInitializationError } from '../error';
|
|
||||||
import { localize } from '../../localize/localize';
|
import { localize } from '../../localize/localize';
|
||||||
import { Entity } from '../../utils/ha/entity-registry/types';
|
import { ExtendedHomeAssistant } from '../../types';
|
||||||
|
import { canonicalizeHAURL } from '../../utils/ha';
|
||||||
import { BrowseMediaManager } from '../../utils/ha/browse-media/browse-media-manager';
|
import { BrowseMediaManager } from '../../utils/ha/browse-media/browse-media-manager';
|
||||||
import {
|
import {
|
||||||
BROWSE_MEDIA_CACHE_SECONDS,
|
BROWSE_MEDIA_CACHE_SECONDS,
|
||||||
@@ -24,12 +10,27 @@ import {
|
|||||||
MEDIA_CLASS_VIDEO,
|
MEDIA_CLASS_VIDEO,
|
||||||
RichBrowseMedia,
|
RichBrowseMedia,
|
||||||
} from '../../utils/ha/browse-media/types';
|
} from '../../utils/ha/browse-media/types';
|
||||||
import { BrowseMediaMetadata } from './types';
|
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
||||||
import { rangesOverlap } from '../range';
|
import { Entity } from '../../utils/ha/entity-registry/types';
|
||||||
import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media';
|
import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media';
|
||||||
import { canonicalizeHAURL } from '../../utils/ha';
|
import { ViewMedia } from '../../view/media';
|
||||||
import { RequestCache } from '../cache';
|
import { RequestCache } from '../cache';
|
||||||
|
import { CameraManagerEngine } from '../engine';
|
||||||
|
import { CameraInitializationError } from '../error';
|
||||||
|
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
||||||
|
import { rangesOverlap } from '../range';
|
||||||
|
import {
|
||||||
|
CameraConfigs,
|
||||||
|
CameraEndpoint,
|
||||||
|
CameraManagerCameraCapabilities,
|
||||||
|
CameraManagerMediaCapabilities,
|
||||||
|
DataQuery,
|
||||||
|
EventQuery,
|
||||||
|
PartialEventQuery,
|
||||||
|
QueryType,
|
||||||
|
} from '../types';
|
||||||
import { BrowseMediaViewMediaFactory } from './media';
|
import { BrowseMediaViewMediaFactory } from './media';
|
||||||
|
import { BrowseMediaMetadata } from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A utility method to determine if a browse media object matches against a
|
* A utility method to determine if a browse media object matches against a
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { CameraConfig } from '../config/types';
|
||||||
import { localize } from '../localize/localize';
|
import { localize } from '../localize/localize';
|
||||||
import { CameraConfig, CardWideConfig } from '../types';
|
|
||||||
import { BrowseMediaManager } from '../utils/ha/browse-media/browse-media-manager';
|
import { BrowseMediaManager } from '../utils/ha/browse-media/browse-media-manager';
|
||||||
import { BrowseMedia } from '../utils/ha/browse-media/types';
|
import { BrowseMedia } from '../utils/ha/browse-media/types';
|
||||||
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||||
@@ -15,20 +15,17 @@ import { getCameraEntityFromConfig } from './util';
|
|||||||
export class CameraManagerEngineFactory {
|
export class CameraManagerEngineFactory {
|
||||||
protected _entityRegistryManager: EntityRegistryManager;
|
protected _entityRegistryManager: EntityRegistryManager;
|
||||||
protected _resolvedMediaCache: ResolvedMediaCache;
|
protected _resolvedMediaCache: ResolvedMediaCache;
|
||||||
protected _cardWideConfig: CardWideConfig;
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
entityRegistryManager: EntityRegistryManager,
|
entityRegistryManager: EntityRegistryManager,
|
||||||
resolvedMediaCache: ResolvedMediaCache,
|
resolvedMediaCache: ResolvedMediaCache,
|
||||||
cardWideConfig: CardWideConfig,
|
|
||||||
) {
|
) {
|
||||||
this._entityRegistryManager = entityRegistryManager;
|
this._entityRegistryManager = entityRegistryManager;
|
||||||
this._cardWideConfig = cardWideConfig;
|
|
||||||
this._resolvedMediaCache = resolvedMediaCache;
|
this._resolvedMediaCache = resolvedMediaCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createEngine(engine: Engine): Promise<CameraManagerEngine | null> {
|
public async createEngine(engine: Engine): Promise<CameraManagerEngine> {
|
||||||
let cameraManagerEngine: CameraManagerEngine | null = null;
|
let cameraManagerEngine: CameraManagerEngine;
|
||||||
switch (engine) {
|
switch (engine) {
|
||||||
case Engine.Generic:
|
case Engine.Generic:
|
||||||
const { GenericCameraManagerEngine } = await import('./generic/engine-generic');
|
const { GenericCameraManagerEngine } = await import('./generic/engine-generic');
|
||||||
@@ -37,7 +34,6 @@ export class CameraManagerEngineFactory {
|
|||||||
case Engine.Frigate:
|
case Engine.Frigate:
|
||||||
const { FrigateCameraManagerEngine } = await import('./frigate/engine-frigate');
|
const { FrigateCameraManagerEngine } = await import('./frigate/engine-frigate');
|
||||||
cameraManagerEngine = new FrigateCameraManagerEngine(
|
cameraManagerEngine = new FrigateCameraManagerEngine(
|
||||||
this._cardWideConfig,
|
|
||||||
new RecordingSegmentsCache(),
|
new RecordingSegmentsCache(),
|
||||||
new RequestCache(),
|
new RequestCache(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,23 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { CameraConfig, ExtendedHomeAssistant } from '../types';
|
import { CameraConfig } from '../config/types';
|
||||||
|
import { ExtendedHomeAssistant } from '../types';
|
||||||
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
import {
|
import {
|
||||||
|
CameraConfigs,
|
||||||
|
CameraEndpoint,
|
||||||
|
CameraEndpoints,
|
||||||
|
CameraEndpointsContext,
|
||||||
|
CameraManagerCameraCapabilities,
|
||||||
|
CameraManagerCameraMetadata,
|
||||||
|
CameraManagerMediaCapabilities,
|
||||||
DataQuery,
|
DataQuery,
|
||||||
|
Engine,
|
||||||
|
EngineOptions,
|
||||||
EventQuery,
|
EventQuery,
|
||||||
EventQueryResultsMap,
|
EventQueryResultsMap,
|
||||||
|
MediaMetadataQuery,
|
||||||
|
MediaMetadataQueryResultsMap,
|
||||||
PartialEventQuery,
|
PartialEventQuery,
|
||||||
PartialRecordingQuery,
|
PartialRecordingQuery,
|
||||||
PartialRecordingSegmentsQuery,
|
PartialRecordingSegmentsQuery,
|
||||||
@@ -14,17 +26,6 @@ import {
|
|||||||
RecordingQueryResultsMap,
|
RecordingQueryResultsMap,
|
||||||
RecordingSegmentsQuery,
|
RecordingSegmentsQuery,
|
||||||
RecordingSegmentsQueryResultsMap,
|
RecordingSegmentsQueryResultsMap,
|
||||||
CameraManagerCameraCapabilities,
|
|
||||||
CameraManagerMediaCapabilities,
|
|
||||||
CameraManagerCameraMetadata,
|
|
||||||
CameraEndpointsContext,
|
|
||||||
CameraConfigs,
|
|
||||||
Engine,
|
|
||||||
CameraEndpoints,
|
|
||||||
MediaMetadataQuery,
|
|
||||||
MediaMetadataQueryResultsMap,
|
|
||||||
EngineOptions,
|
|
||||||
CameraEndpoint,
|
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export const CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT = 10000;
|
export const CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT = 10000;
|
||||||
|
|||||||
@@ -1,24 +1,53 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import add from 'date-fns/add';
|
import add from 'date-fns/add';
|
||||||
import endOfHour from 'date-fns/endOfHour';
|
import endOfHour from 'date-fns/endOfHour';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||||
import startOfHour from 'date-fns/startOfHour';
|
import startOfHour from 'date-fns/startOfHour';
|
||||||
import { CameraConfig, CardWideConfig, ExtendedHomeAssistant } from '../../types';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import orderBy from 'lodash-es/orderBy';
|
||||||
|
import throttle from 'lodash-es/throttle';
|
||||||
|
import uniq from 'lodash-es/uniq';
|
||||||
|
import uniqWith from 'lodash-es/uniqWith';
|
||||||
|
import { CameraConfig } from '../../config/types';
|
||||||
|
import { localize } from '../../localize/localize';
|
||||||
|
import { ExtendedHomeAssistant } from '../../types';
|
||||||
|
import {
|
||||||
|
allPromises,
|
||||||
|
formatDate,
|
||||||
|
prettifyTitle,
|
||||||
|
runWhenIdleIfSupported,
|
||||||
|
} from '../../utils/basic';
|
||||||
|
import { getEntityTitle } from '../../utils/ha';
|
||||||
|
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
||||||
|
import { Entity } from '../../utils/ha/entity-registry/types';
|
||||||
import { ViewMedia } from '../../view/media';
|
import { ViewMedia } from '../../view/media';
|
||||||
|
import { ViewMediaClassifier } from '../../view/media-classifier';
|
||||||
import { RecordingSegmentsCache, RequestCache } from '../cache';
|
import { RecordingSegmentsCache, RequestCache } from '../cache';
|
||||||
import {
|
import {
|
||||||
CameraManagerEngine,
|
|
||||||
CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
|
CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
|
||||||
|
CameraManagerEngine,
|
||||||
} from '../engine';
|
} from '../engine';
|
||||||
|
import { CameraInitializationError } from '../error';
|
||||||
|
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
||||||
import { DateRange } from '../range';
|
import { DateRange } from '../range';
|
||||||
import {
|
import {
|
||||||
CameraManagerCameraMetadata,
|
CameraConfigs,
|
||||||
|
CameraEndpoint,
|
||||||
|
CameraEndpoints,
|
||||||
|
CameraEndpointsContext,
|
||||||
CameraManagerCameraCapabilities,
|
CameraManagerCameraCapabilities,
|
||||||
|
CameraManagerCameraMetadata,
|
||||||
CameraManagerMediaCapabilities,
|
CameraManagerMediaCapabilities,
|
||||||
DataQuery,
|
DataQuery,
|
||||||
Engine,
|
Engine,
|
||||||
|
EngineOptions,
|
||||||
EventQuery,
|
EventQuery,
|
||||||
EventQueryResults,
|
EventQueryResults,
|
||||||
EventQueryResultsMap,
|
EventQueryResultsMap,
|
||||||
|
MediaMetadataQuery,
|
||||||
|
MediaMetadataQueryResults,
|
||||||
|
MediaMetadataQueryResultsMap,
|
||||||
PartialEventQuery,
|
PartialEventQuery,
|
||||||
PartialRecordingQuery,
|
PartialRecordingQuery,
|
||||||
PartialRecordingSegmentsQuery,
|
PartialRecordingSegmentsQuery,
|
||||||
@@ -32,56 +61,26 @@ import {
|
|||||||
RecordingSegment,
|
RecordingSegment,
|
||||||
RecordingSegmentsQuery,
|
RecordingSegmentsQuery,
|
||||||
RecordingSegmentsQueryResultsMap,
|
RecordingSegmentsQueryResultsMap,
|
||||||
CameraEndpointsContext,
|
|
||||||
CameraConfigs,
|
|
||||||
CameraEndpoints,
|
|
||||||
CameraEndpoint,
|
|
||||||
MediaMetadataQuery,
|
|
||||||
MediaMetadataQueryResults,
|
|
||||||
MediaMetadataQueryResultsMap,
|
|
||||||
EngineOptions,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
import { getCameraEntityFromConfig } from '../util';
|
||||||
|
import frigateLogo from './assets/frigate-logo-dark.svg';
|
||||||
|
import { FrigateViewMediaFactory } from './media';
|
||||||
|
import { FrigateViewMediaClassifier } from './media-classifier';
|
||||||
import {
|
import {
|
||||||
FrigateEventQueryResults,
|
|
||||||
FrigateRecordingQueryResults,
|
|
||||||
FrigateRecordingSegmentsQueryResults,
|
|
||||||
FrigateRecording,
|
|
||||||
} from './types';
|
|
||||||
import {
|
|
||||||
getEvents,
|
|
||||||
getEventSummary,
|
|
||||||
getRecordingSegments,
|
|
||||||
getRecordingsSummary,
|
|
||||||
NativeFrigateEventQuery,
|
NativeFrigateEventQuery,
|
||||||
NativeFrigateRecordingSegmentsQuery,
|
NativeFrigateRecordingSegmentsQuery,
|
||||||
|
getEventSummary,
|
||||||
|
getEvents,
|
||||||
|
getRecordingSegments,
|
||||||
|
getRecordingsSummary,
|
||||||
retainEvent,
|
retainEvent,
|
||||||
} from './requests';
|
} from './requests';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
|
||||||
import orderBy from 'lodash-es/orderBy';
|
|
||||||
import throttle from 'lodash-es/throttle';
|
|
||||||
import uniqWith from 'lodash-es/uniqWith';
|
|
||||||
import {
|
import {
|
||||||
allPromises,
|
FrigateEventQueryResults,
|
||||||
formatDate,
|
FrigateRecording,
|
||||||
prettifyTitle,
|
FrigateRecordingQueryResults,
|
||||||
runWhenIdleIfSupported,
|
FrigateRecordingSegmentsQueryResults,
|
||||||
} from '../../utils/basic';
|
} from './types';
|
||||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
|
||||||
import sum from 'lodash-es/sum';
|
|
||||||
import { FrigateViewMediaClassifier } from './media-classifier';
|
|
||||||
import { ViewMediaClassifier } from '../../view/media-classifier';
|
|
||||||
import { FrigateViewMediaFactory } from './media';
|
|
||||||
import { log } from '../../utils/debug';
|
|
||||||
import { getEntityTitle } from '../../utils/ha';
|
|
||||||
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
|
||||||
import { Entity } from '../../utils/ha/entity-registry/types';
|
|
||||||
import { CameraInitializationError } from '../error';
|
|
||||||
import { localize } from '../../localize/localize';
|
|
||||||
import uniq from 'lodash-es/uniq';
|
|
||||||
import format from 'date-fns/format';
|
|
||||||
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
|
||||||
import frigateLogo from './assets/frigate-logo-dark.svg';
|
|
||||||
import { getCameraEntityFromConfig } from '../util';
|
|
||||||
|
|
||||||
const EVENT_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
const EVENT_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
||||||
const RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
const RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
||||||
@@ -120,7 +119,6 @@ export class FrigateCameraManagerEngine
|
|||||||
{
|
{
|
||||||
protected _recordingSegmentsCache: RecordingSegmentsCache;
|
protected _recordingSegmentsCache: RecordingSegmentsCache;
|
||||||
protected _requestCache: RequestCache;
|
protected _requestCache: RequestCache;
|
||||||
protected _cardWideConfig: CardWideConfig;
|
|
||||||
|
|
||||||
// Garbage collect segments at most once an hour.
|
// Garbage collect segments at most once an hour.
|
||||||
protected _throttledSegmentGarbageCollector = throttle(
|
protected _throttledSegmentGarbageCollector = throttle(
|
||||||
@@ -130,12 +128,10 @@ export class FrigateCameraManagerEngine
|
|||||||
);
|
);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
cardWideConfig: CardWideConfig,
|
|
||||||
recordingSegmentsCache: RecordingSegmentsCache,
|
recordingSegmentsCache: RecordingSegmentsCache,
|
||||||
requestCache: RequestCache,
|
requestCache: RequestCache,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
this._cardWideConfig = cardWideConfig;
|
|
||||||
this._recordingSegmentsCache = recordingSegmentsCache;
|
this._recordingSegmentsCache = recordingSegmentsCache;
|
||||||
this._requestCache = requestCache;
|
this._requestCache = requestCache;
|
||||||
}
|
}
|
||||||
@@ -989,12 +985,6 @@ export class FrigateCameraManagerEngine
|
|||||||
type: QueryType.Recording,
|
type: QueryType.Recording,
|
||||||
};
|
};
|
||||||
|
|
||||||
const countSegments = () =>
|
|
||||||
sum(
|
|
||||||
cameraIDs.map((cameraID) => this._recordingSegmentsCache.getSize(cameraID) ?? 0),
|
|
||||||
);
|
|
||||||
const segmentsStart = countSegments();
|
|
||||||
|
|
||||||
// Performance: _recordingSegments is potentially very large (e.g. 10K - 1M
|
// Performance: _recordingSegments is potentially very large (e.g. 10K - 1M
|
||||||
// items) and each item must be examined, so care required here to stick to
|
// items) and each item must be examined, so care required here to stick to
|
||||||
// nothing worse than O(n) performance.
|
// nothing worse than O(n) performance.
|
||||||
@@ -1029,12 +1019,6 @@ export class FrigateCameraManagerEngine
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
log(
|
|
||||||
this._cardWideConfig,
|
|
||||||
'Frigate Card recording segment garbage collection: ' +
|
|
||||||
`Released ${segmentsStart - countSegments()} segment(s)`,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import fromUnixTime from 'date-fns/fromUnixTime';
|
import fromUnixTime from 'date-fns/fromUnixTime';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import { CameraConfig } from '../../types';
|
import { CameraConfig } from '../../config/types';
|
||||||
import {
|
import {
|
||||||
ViewMedia,
|
|
||||||
EventViewMedia,
|
EventViewMedia,
|
||||||
RecordingViewMedia,
|
RecordingViewMedia,
|
||||||
ViewMediaType,
|
|
||||||
VideoContentType,
|
VideoContentType,
|
||||||
|
ViewMedia,
|
||||||
|
ViewMediaType,
|
||||||
} from '../../view/media';
|
} from '../../view/media';
|
||||||
import { FrigateEvent, FrigateRecording } from './types';
|
import { FrigateEvent, FrigateRecording } from './types';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import utcToZonedTime from 'date-fns-tz/utcToZonedTime';
|
import utcToZonedTime from 'date-fns-tz/utcToZonedTime';
|
||||||
import { CameraConfig, ClipsOrSnapshots } from '../../types';
|
import { CameraConfig } from '../../config/types';
|
||||||
|
import { ClipsOrSnapshots } from '../../types';
|
||||||
import { formatDateAndTime, prettifyTitle } from '../../utils/basic';
|
import { formatDateAndTime, prettifyTitle } from '../../utils/basic';
|
||||||
import { FrigateEvent, FrigateRecording } from './types';
|
import { FrigateEvent, FrigateRecording } from './types';
|
||||||
|
|
||||||
|
|||||||
@@ -1,35 +1,36 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { CameraConfig, ExtendedHomeAssistant } from '../../types';
|
import { CameraConfig } from '../../config/types';
|
||||||
|
import { ExtendedHomeAssistant } from '../../types';
|
||||||
|
import { getEntityIcon, getEntityTitle } from '../../utils/ha';
|
||||||
|
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
||||||
import { ViewMedia } from '../../view/media';
|
import { ViewMedia } from '../../view/media';
|
||||||
|
import { CameraManagerEngine } from '../engine';
|
||||||
import {
|
import {
|
||||||
|
CameraConfigs,
|
||||||
|
CameraEndpoint,
|
||||||
|
CameraEndpoints,
|
||||||
|
CameraEndpointsContext,
|
||||||
|
CameraManagerCameraCapabilities,
|
||||||
CameraManagerCameraMetadata,
|
CameraManagerCameraMetadata,
|
||||||
CameraManagerMediaCapabilities,
|
CameraManagerMediaCapabilities,
|
||||||
DataQuery,
|
DataQuery,
|
||||||
|
Engine,
|
||||||
|
EngineOptions,
|
||||||
EventQuery,
|
EventQuery,
|
||||||
EventQueryResultsMap,
|
EventQueryResultsMap,
|
||||||
|
MediaMetadataQuery,
|
||||||
|
MediaMetadataQueryResultsMap,
|
||||||
PartialEventQuery,
|
PartialEventQuery,
|
||||||
PartialRecordingQuery,
|
PartialRecordingQuery,
|
||||||
PartialRecordingSegmentsQuery,
|
PartialRecordingSegmentsQuery,
|
||||||
|
QueryReturnType,
|
||||||
|
RecordingQuery,
|
||||||
RecordingQueryResultsMap,
|
RecordingQueryResultsMap,
|
||||||
RecordingSegmentsQuery,
|
RecordingSegmentsQuery,
|
||||||
RecordingSegmentsQueryResultsMap,
|
RecordingSegmentsQueryResultsMap,
|
||||||
CameraEndpointsContext,
|
|
||||||
CameraConfigs,
|
|
||||||
RecordingQuery,
|
|
||||||
QueryReturnType,
|
|
||||||
CameraManagerCameraCapabilities,
|
|
||||||
Engine,
|
|
||||||
CameraEndpoints,
|
|
||||||
MediaMetadataQuery,
|
|
||||||
MediaMetadataQueryResultsMap,
|
|
||||||
EngineOptions,
|
|
||||||
CameraEndpoint,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { getEntityIcon, getEntityTitle } from '../../utils/ha';
|
|
||||||
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
|
||||||
import { CameraManagerEngine } from '../engine';
|
|
||||||
|
|
||||||
export class GenericCameraManagerEngine implements CameraManagerEngine {
|
export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||||
public getEngineType(): Engine {
|
public getEngineType(): Engine {
|
||||||
|
|||||||
+119
-90
@@ -1,22 +1,38 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import {
|
import add from 'date-fns/add';
|
||||||
CameraConfig,
|
import cloneDeep from 'lodash-es/cloneDeep';
|
||||||
CamerasConfig,
|
import merge from 'lodash-es/merge.js';
|
||||||
CardWideConfig,
|
import sum from 'lodash-es/sum';
|
||||||
ExtendedHomeAssistant,
|
import { CameraConfig, CamerasConfig } from '../config/types.js';
|
||||||
} from '../types.js';
|
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
||||||
|
import { localize } from '../localize/localize.js';
|
||||||
import { allPromises, arrayify, setify } from '../utils/basic.js';
|
import { allPromises, arrayify, setify } from '../utils/basic.js';
|
||||||
|
import { getCameraID } from '../utils/camera.js';
|
||||||
|
import { CardCameraAPI } from '../card-controller/types.js';
|
||||||
|
import { log } from '../utils/debug.js';
|
||||||
|
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
||||||
|
import { ViewMedia } from '../view/media.js';
|
||||||
|
import { CameraManagerEngineFactory } from './engine-factory.js';
|
||||||
|
import { CameraManagerEngine } from './engine.js';
|
||||||
|
import { CameraInitializationError } from './error.js';
|
||||||
|
import { CameraManagerReadOnlyConfigStore, CameraManagerStore } from './store.js';
|
||||||
import {
|
import {
|
||||||
|
CameraEndpoint,
|
||||||
|
CameraEndpoints,
|
||||||
|
CameraEndpointsContext,
|
||||||
CameraManagerCameraCapabilities,
|
CameraManagerCameraCapabilities,
|
||||||
CameraManagerCameraMetadata,
|
CameraManagerCameraMetadata,
|
||||||
CameraManagerCapabilities,
|
CameraManagerCapabilities,
|
||||||
CameraManagerMediaCapabilities,
|
CameraManagerMediaCapabilities,
|
||||||
CameraEndpointsContext,
|
|
||||||
DataQuery,
|
DataQuery,
|
||||||
|
Engine,
|
||||||
|
EngineOptions,
|
||||||
EventQuery,
|
EventQuery,
|
||||||
EventQueryResults,
|
EventQueryResults,
|
||||||
EventQueryResultsMap,
|
EventQueryResultsMap,
|
||||||
MediaMetadata,
|
MediaMetadata,
|
||||||
|
MediaMetadataQuery,
|
||||||
|
MediaMetadataQueryResults,
|
||||||
MediaQuery,
|
MediaQuery,
|
||||||
PartialDataQuery,
|
PartialDataQuery,
|
||||||
PartialEventQuery,
|
PartialEventQuery,
|
||||||
@@ -34,26 +50,7 @@ import {
|
|||||||
RecordingSegmentsQueryResults,
|
RecordingSegmentsQueryResults,
|
||||||
RecordingSegmentsQueryResultsMap,
|
RecordingSegmentsQueryResultsMap,
|
||||||
ResultsMap,
|
ResultsMap,
|
||||||
CameraEndpoints,
|
|
||||||
Engine,
|
|
||||||
MediaMetadataQuery,
|
|
||||||
MediaMetadataQueryResults,
|
|
||||||
EngineOptions,
|
|
||||||
CameraEndpoint,
|
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { CameraManagerEngineFactory } from './engine-factory.js';
|
|
||||||
import { ViewMedia } from '../view/media.js';
|
|
||||||
import { CameraManagerEngine } from './engine.js';
|
|
||||||
import sum from 'lodash-es/sum';
|
|
||||||
import add from 'date-fns/add';
|
|
||||||
import { log } from '../utils/debug.js';
|
|
||||||
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
|
||||||
import { getCameraID } from '../utils/camera.js';
|
|
||||||
import { localize } from '../localize/localize.js';
|
|
||||||
import { CameraInitializationError } from './error.js';
|
|
||||||
import { CameraManagerReadOnlyConfigStore, CameraManagerStore } from './store.js';
|
|
||||||
import cloneDeep from 'lodash-es/cloneDeep';
|
|
||||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
|
||||||
import { sortMedia } from './util.js';
|
import { sortMedia } from './util.js';
|
||||||
|
|
||||||
class QueryClassifier {
|
class QueryClassifier {
|
||||||
@@ -112,25 +109,53 @@ interface InitializedCamera {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class CameraManager {
|
export class CameraManager {
|
||||||
|
protected _api: CardCameraAPI;
|
||||||
protected _engineFactory: CameraManagerEngineFactory;
|
protected _engineFactory: CameraManagerEngineFactory;
|
||||||
protected _cardWideConfig?: CardWideConfig;
|
protected _store = new CameraManagerStore();
|
||||||
protected _store: CameraManagerStore;
|
|
||||||
|
|
||||||
constructor(
|
constructor(api: CardCameraAPI) {
|
||||||
engineFactory: CameraManagerEngineFactory,
|
this._api = api;
|
||||||
cardWideConfig?: CardWideConfig,
|
this._engineFactory = new CameraManagerEngineFactory(
|
||||||
) {
|
this._api.getEntityRegistryManager(),
|
||||||
this._engineFactory = engineFactory;
|
this._api.getResolvedMediaCache(),
|
||||||
this._cardWideConfig = cardWideConfig;
|
);
|
||||||
this._store = new CameraManagerStore();
|
}
|
||||||
|
|
||||||
|
public async initializeCamerasFromConfig(): Promise<void> {
|
||||||
|
const config = this._api.getConfigManager().getConfig();
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
|
if (!config || !hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._store.reset();
|
||||||
|
|
||||||
|
// For each camera merge the config (which has no defaults) into the camera
|
||||||
|
// global config (which does have defaults). The merging must happen in this
|
||||||
|
// order, to ensure that the defaults in the cameras global config do not
|
||||||
|
// override the values specified in the per-camera config.
|
||||||
|
const cameras = config.cameras.map((camera) =>
|
||||||
|
merge(cloneDeep(config?.cameras_global), camera),
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this._initializeCameras(cameras);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
this._api.getMessageManager().setErrorIfHigherPriority(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _getEnginesForCameras(
|
protected async _getEnginesForCameras(
|
||||||
hass: HomeAssistant,
|
|
||||||
camerasConfig: CamerasConfig,
|
camerasConfig: CamerasConfig,
|
||||||
): Promise<Map<CameraConfig, CameraManagerEngine>> {
|
): Promise<Map<CameraConfig, CameraManagerEngine>> {
|
||||||
const output: Map<CameraConfig, CameraManagerEngine> = new Map();
|
const output: Map<CameraConfig, CameraManagerEngine> = new Map();
|
||||||
const engines: Map<Engine, CameraManagerEngine> = new Map();
|
const engines: Map<Engine, CameraManagerEngine> = new Map();
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
|
if (!hass) {
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
const getEngineTypes = async (configs: CameraConfig[]) => {
|
const getEngineTypes = async (configs: CameraConfig[]) => {
|
||||||
return await allPromises(configs, (config) =>
|
return await allPromises(configs, (config) =>
|
||||||
@@ -142,7 +167,7 @@ export class CameraManager {
|
|||||||
for (const [index, cameraConfig] of camerasConfig.entries()) {
|
for (const [index, cameraConfig] of camerasConfig.entries()) {
|
||||||
const engineType = engineTypes[index];
|
const engineType = engineTypes[index];
|
||||||
const engine = engineType
|
const engine = engineType
|
||||||
? engines.get(engineType) ?? await this._engineFactory.createEngine(engineType)
|
? engines.get(engineType) ?? (await this._engineFactory.createEngine(engineType))
|
||||||
: null;
|
: null;
|
||||||
if (!engine || !engineType) {
|
if (!engine || !engineType) {
|
||||||
throw new CameraInitializationError(
|
throw new CameraInitializationError(
|
||||||
@@ -177,12 +202,13 @@ export class CameraManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async initializeCameras(
|
protected async _initializeCameras(camerasConfig: CamerasConfig): Promise<void> {
|
||||||
hass: HomeAssistant,
|
|
||||||
entityRegistryManager: EntityRegistryManager,
|
|
||||||
camerasConfig: CamerasConfig,
|
|
||||||
): Promise<void> {
|
|
||||||
const initializationStartTime = new Date();
|
const initializationStartTime = new Date();
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
|
if (!hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const hasAutoTriggers = (config: CameraConfig): boolean => {
|
const hasAutoTriggers = (config: CameraConfig): boolean => {
|
||||||
return config.triggers.motion || config.triggers.occupancy;
|
return config.triggers.motion || config.triggers.occupancy;
|
||||||
@@ -195,18 +221,23 @@ export class CameraManager {
|
|||||||
// ... then we need to populate the entity cache by fetching all entities
|
// ... then we need to populate the entity cache by fetching all entities
|
||||||
// from Home Assistant. Do this once upfront, to avoid each camera doing
|
// from Home Assistant. Do this once upfront, to avoid each camera doing
|
||||||
// it.
|
// it.
|
||||||
await entityRegistryManager.fetchEntityList(hass);
|
await this._api.getEntityRegistryManager().fetchEntityList(hass);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Engines are created sequentially, to avoid duplicate creation of the same
|
// Engines are created sequentially, to avoid duplicate creation of the same
|
||||||
// engine. See: https://github.com/dermotduffy/frigate-hass-card/issues/941
|
// engine. See: https://github.com/dermotduffy/frigate-hass-card/issues/941
|
||||||
const engineByConfig = await this._getEnginesForCameras(hass, camerasConfig);
|
const engineByConfig = await this._getEnginesForCameras(camerasConfig);
|
||||||
|
|
||||||
// Configuration is initialized in parallel.
|
// Configuration is initialized in parallel.
|
||||||
const results = await allPromises(
|
const results = await allPromises(
|
||||||
engineByConfig.entries(),
|
engineByConfig.entries(),
|
||||||
async ([cameraConfig, engine]) =>
|
async ([cameraConfig, engine]) =>
|
||||||
await this._initializeCamera(hass, engine, entityRegistryManager, cameraConfig),
|
await this._initializeCamera(
|
||||||
|
hass,
|
||||||
|
engine,
|
||||||
|
this._api.getEntityRegistryManager(),
|
||||||
|
cameraConfig,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Do the additions based off the result-order, to ensure the map order is
|
// Do the additions based off the result-order, to ensure the map order is
|
||||||
@@ -236,7 +267,7 @@ export class CameraManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
log(
|
log(
|
||||||
this._cardWideConfig,
|
this._api.getConfigManager().getCardWideConfig(),
|
||||||
'Frigate Card CameraManager initialized (Cameras: ',
|
'Frigate Card CameraManager initialized (Cameras: ',
|
||||||
this._store.getCameras(),
|
this._store.getCameras(),
|
||||||
`, Duration: ${
|
`, Duration: ${
|
||||||
@@ -284,7 +315,7 @@ export class CameraManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMediaMetadata(hass: HomeAssistant): Promise<MediaMetadata | null> {
|
public async getMediaMetadata(): Promise<MediaMetadata | null> {
|
||||||
const tags: Set<string> = new Set();
|
const tags: Set<string> = new Set();
|
||||||
const what: Set<string> = new Set();
|
const what: Set<string> = new Set();
|
||||||
const where: Set<string> = new Set();
|
const where: Set<string> = new Set();
|
||||||
@@ -295,7 +326,7 @@ export class CameraManager {
|
|||||||
cameraIDs: this._store.getCameraIDs(),
|
cameraIDs: this._store.getCameraIDs(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const results = await this._handleQuery(hass, query);
|
const results = await this._handleQuery(query);
|
||||||
|
|
||||||
for (const result of results?.values() ?? []) {
|
for (const result of results?.values() ?? []) {
|
||||||
if (result.metadata.tags) {
|
if (result.metadata.tags) {
|
||||||
@@ -365,47 +396,46 @@ export class CameraManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getEvents(
|
public async getEvents(
|
||||||
hass: HomeAssistant,
|
|
||||||
query: EventQuery | EventQuery[],
|
query: EventQuery | EventQuery[],
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<EventQueryResultsMap> {
|
): Promise<EventQueryResultsMap> {
|
||||||
return await this._handleQuery(hass, query, engineOptions);
|
return await this._handleQuery(query, engineOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRecordings(
|
public async getRecordings(
|
||||||
hass: HomeAssistant,
|
|
||||||
query: RecordingQuery | RecordingQuery[],
|
query: RecordingQuery | RecordingQuery[],
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<RecordingQueryResultsMap> {
|
): Promise<RecordingQueryResultsMap> {
|
||||||
return await this._handleQuery(hass, query, engineOptions);
|
return await this._handleQuery(query, engineOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getRecordingSegments(
|
public async getRecordingSegments(
|
||||||
hass: HomeAssistant,
|
|
||||||
query: RecordingSegmentsQuery | RecordingSegmentsQuery[],
|
query: RecordingSegmentsQuery | RecordingSegmentsQuery[],
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<RecordingSegmentsQueryResultsMap> {
|
): Promise<RecordingSegmentsQueryResultsMap> {
|
||||||
return await this._handleQuery(hass, query, engineOptions);
|
return await this._handleQuery(query, engineOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async executeMediaQueries<T extends MediaQuery>(
|
public async executeMediaQueries<T extends MediaQuery>(
|
||||||
hass: HomeAssistant,
|
|
||||||
queries: T[],
|
queries: T[],
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<ViewMedia[] | null> {
|
): Promise<ViewMedia[] | null> {
|
||||||
return this._convertQueryResultsToMedia(
|
return this._convertQueryResultsToMedia(
|
||||||
hass,
|
await this._handleQuery(queries, engineOptions),
|
||||||
await this._handleQuery(hass, queries, engineOptions),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async extendMediaQueries<T extends MediaQuery>(
|
public async extendMediaQueries<T extends MediaQuery>(
|
||||||
hass: HomeAssistant,
|
|
||||||
queries: T[],
|
queries: T[],
|
||||||
results: ViewMedia[],
|
results: ViewMedia[],
|
||||||
direction: 'earlier' | 'later',
|
direction: 'earlier' | 'later',
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<ExtendedMediaQueryResult<T> | null> {
|
): Promise<ExtendedMediaQueryResult<T> | null> {
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
if (!hass) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const getTimeFromResults = (want: 'earliest' | 'latest'): Date | null => {
|
const getTimeFromResults = (want: 'earliest' | 'latest'): Date | null => {
|
||||||
let output: Date | null = null;
|
let output: Date | null = null;
|
||||||
for (const result of results) {
|
for (const result of results) {
|
||||||
@@ -423,8 +453,8 @@ export class CameraManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const chunkSize =
|
const chunkSize =
|
||||||
this._cardWideConfig?.performance?.features.media_chunk_size ??
|
this._api.getConfigManager().getCardWideConfig()?.performance?.features
|
||||||
MEDIA_CHUNK_SIZE_DEFAULT;
|
.media_chunk_size ?? MEDIA_CHUNK_SIZE_DEFAULT;
|
||||||
|
|
||||||
// The queries associated with the chunk to fetch.
|
// The queries associated with the chunk to fetch.
|
||||||
const newChunkQueries: T[] = [];
|
const newChunkQueries: T[] = [];
|
||||||
@@ -456,8 +486,7 @@ export class CameraManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const newChunkMedia = this._convertQueryResultsToMedia(
|
const newChunkMedia = this._convertQueryResultsToMedia(
|
||||||
hass,
|
await this._handleQuery(newChunkQueries, engineOptions),
|
||||||
await this._handleQuery(hass, newChunkQueries, engineOptions),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!newChunkMedia.length) {
|
if (!newChunkMedia.length) {
|
||||||
@@ -478,14 +507,12 @@ export class CameraManager {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMediaDownloadPath(
|
public async getMediaDownloadPath(media: ViewMedia): Promise<CameraEndpoint | null> {
|
||||||
hass: ExtendedHomeAssistant,
|
|
||||||
media: ViewMedia,
|
|
||||||
): Promise<CameraEndpoint | null> {
|
|
||||||
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
||||||
const engine = this._store.getEngineForMedia(media);
|
const engine = this._store.getEngineForMedia(media);
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
if (!cameraConfig || !engine) {
|
if (!cameraConfig || !engine || !hass) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return await engine.getMediaDownloadPath(hass, cameraConfig, media);
|
return await engine.getMediaDownloadPath(hass, cameraConfig, media);
|
||||||
@@ -499,15 +526,12 @@ export class CameraManager {
|
|||||||
return engine.getMediaCapabilities(media);
|
return engine.getMediaCapabilities(media);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async favoriteMedia(
|
public async favoriteMedia(media: ViewMedia, favorite: boolean): Promise<void> {
|
||||||
hass: HomeAssistant,
|
|
||||||
media: ViewMedia,
|
|
||||||
favorite: boolean,
|
|
||||||
): Promise<void> {
|
|
||||||
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
||||||
const engine = this._store.getEngineForMedia(media);
|
const engine = this._store.getEngineForMedia(media);
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
if (!cameraConfig || !engine) {
|
if (!cameraConfig || !engine || !hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -515,7 +539,7 @@ export class CameraManager {
|
|||||||
await engine.favoriteMedia(hass, cameraConfig, media, favorite);
|
await engine.favoriteMedia(hass, cameraConfig, media, favorite);
|
||||||
|
|
||||||
log(
|
log(
|
||||||
this._cardWideConfig,
|
this._api.getConfigManager().getCardWideConfig(),
|
||||||
'Frigate Card CameraManager favorite request (',
|
'Frigate Card CameraManager favorite request (',
|
||||||
`Duration: ${(new Date().getTime() - queryStartTime.getTime()) / 1000}s,`,
|
`Duration: ${(new Date().getTime() - queryStartTime.getTime()) / 1000}s,`,
|
||||||
'Media:',
|
'Media:',
|
||||||
@@ -550,16 +574,15 @@ export class CameraManager {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getMediaSeekTime(
|
public async getMediaSeekTime(media: ViewMedia, target: Date): Promise<number | null> {
|
||||||
hass: HomeAssistant,
|
|
||||||
media: ViewMedia,
|
|
||||||
target: Date,
|
|
||||||
): Promise<number | null> {
|
|
||||||
const startTime = media.getStartTime();
|
const startTime = media.getStartTime();
|
||||||
const endTime = media.getEndTime();
|
const endTime = media.getEndTime();
|
||||||
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
||||||
const engine = this._store.getEngineForMedia(media);
|
const engine = this._store.getEngineForMedia(media);
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
!hass ||
|
||||||
!cameraConfig ||
|
!cameraConfig ||
|
||||||
!engine ||
|
!engine ||
|
||||||
!startTime ||
|
!startTime ||
|
||||||
@@ -574,13 +597,17 @@ export class CameraManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected async _handleQuery<QT extends DataQuery>(
|
protected async _handleQuery<QT extends DataQuery>(
|
||||||
hass: HomeAssistant,
|
|
||||||
query: QT | QT[],
|
query: QT | QT[],
|
||||||
engineOptions?: EngineOptions,
|
engineOptions?: EngineOptions,
|
||||||
): Promise<Map<QT, QueryReturnType<QT>>> {
|
): Promise<Map<QT, QueryReturnType<QT>>> {
|
||||||
const _queries = arrayify(query);
|
const _queries = arrayify(query);
|
||||||
const results = new Map<QT, QueryReturnType<QT>>();
|
const results = new Map<QT, QueryReturnType<QT>>();
|
||||||
const queryStartTime = new Date();
|
const queryStartTime = new Date();
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
|
if (!hass) {
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
const processEngineQuery = async (
|
const processEngineQuery = async (
|
||||||
engine: CameraManagerEngine,
|
engine: CameraManagerEngine,
|
||||||
@@ -643,7 +670,7 @@ export class CameraManager {
|
|||||||
);
|
);
|
||||||
|
|
||||||
log(
|
log(
|
||||||
this._cardWideConfig,
|
this._api.getConfigManager().getCardWideConfig(),
|
||||||
'Frigate Card CameraManager request [Input queries:',
|
'Frigate Card CameraManager request [Input queries:',
|
||||||
_queries.length,
|
_queries.length,
|
||||||
', Cached output queries:',
|
', Cached output queries:',
|
||||||
@@ -662,10 +689,15 @@ export class CameraManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _convertQueryResultsToMedia<QT extends DataQuery>(
|
protected _convertQueryResultsToMedia<QT extends DataQuery>(
|
||||||
hass: HomeAssistant,
|
|
||||||
results: ResultsMap<QT>,
|
results: ResultsMap<QT>,
|
||||||
): ViewMedia[] {
|
): ViewMedia[] {
|
||||||
const mediaArray: ViewMedia[] = [];
|
const mediaArray: ViewMedia[] = [];
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
|
if (!hass) {
|
||||||
|
return mediaArray;
|
||||||
|
}
|
||||||
|
|
||||||
for (const [query, result] of results.entries()) {
|
for (const [query, result] of results.entries()) {
|
||||||
const engine = this._store.getEngineOfType(result.engine);
|
const engine = this._store.getEngineOfType(result.engine);
|
||||||
|
|
||||||
@@ -712,13 +744,12 @@ export class CameraManager {
|
|||||||
return engine.getCameraEndpoints(cameraConfig, context);
|
return engine.getCameraEndpoints(cameraConfig, context);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCameraMetadata(
|
public getCameraMetadata(cameraID: string): CameraManagerCameraMetadata | null {
|
||||||
hass: HomeAssistant,
|
|
||||||
cameraID: string,
|
|
||||||
): CameraManagerCameraMetadata | null {
|
|
||||||
const cameraConfig = this._store.getCameraConfig(cameraID);
|
const cameraConfig = this._store.getCameraConfig(cameraID);
|
||||||
const engine = this._store.getEngineForCameraID(cameraID);
|
const engine = this._store.getEngineForCameraID(cameraID);
|
||||||
if (!cameraConfig || !engine) {
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
|
if (!cameraConfig || !engine || !hass) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return engine.getCameraMetadata(hass, cameraConfig);
|
return engine.getCameraMetadata(hass, cameraConfig);
|
||||||
@@ -747,9 +778,7 @@ export class CameraManager {
|
|||||||
canFavoriteRecordings: perCameraCapabilities.some(
|
canFavoriteRecordings: perCameraCapabilities.some(
|
||||||
(cap) => cap?.canFavoriteRecordings,
|
(cap) => cap?.canFavoriteRecordings,
|
||||||
),
|
),
|
||||||
canSeek: perCameraCapabilities.some(
|
canSeek: perCameraCapabilities.some((cap) => cap?.canSeek),
|
||||||
(cap) => cap?.canSeek,
|
|
||||||
),
|
|
||||||
|
|
||||||
supportsClips: perCameraCapabilities.some((cap) => cap?.supportsClips),
|
supportsClips: perCameraCapabilities.some((cap) => cap?.supportsClips),
|
||||||
supportsRecordings: perCameraCapabilities.some((cap) => cap?.supportsRecordings),
|
supportsRecordings: perCameraCapabilities.some((cap) => cap?.supportsRecordings),
|
||||||
|
|||||||
@@ -1,6 +1,30 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { CameraConfig } from '../../types';
|
import add from 'date-fns/add';
|
||||||
|
import endOfDay from 'date-fns/endOfDay';
|
||||||
|
import parse from 'date-fns/parse';
|
||||||
|
import startOfDay from 'date-fns/startOfDay';
|
||||||
|
import orderBy from 'lodash-es/orderBy';
|
||||||
|
import { CameraConfig } from '../../config/types';
|
||||||
|
import { allPromises, formatDate, isValidDate } from '../../utils/basic';
|
||||||
|
import {
|
||||||
|
BrowseMediaStep,
|
||||||
|
BrowseMediaTarget,
|
||||||
|
} from '../../utils/ha/browse-media/browse-media-manager';
|
||||||
|
import {
|
||||||
|
BROWSE_MEDIA_CACHE_SECONDS,
|
||||||
|
BrowseMedia,
|
||||||
|
MEDIA_CLASS_IMAGE,
|
||||||
|
MEDIA_CLASS_VIDEO,
|
||||||
|
RichBrowseMedia,
|
||||||
|
} from '../../utils/ha/browse-media/types';
|
||||||
import { ViewMedia } from '../../view/media';
|
import { ViewMedia } from '../../view/media';
|
||||||
|
import {
|
||||||
|
BrowseMediaCameraManagerEngine,
|
||||||
|
getViewMediaFromBrowseMediaArray,
|
||||||
|
isMediaWithinDates,
|
||||||
|
} from '../browse-media/engine-browse-media';
|
||||||
|
import { BrowseMediaMetadata } from '../browse-media/types';
|
||||||
|
import { CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT } from '../engine';
|
||||||
import {
|
import {
|
||||||
CameraConfigs,
|
CameraConfigs,
|
||||||
CameraEndpoint,
|
CameraEndpoint,
|
||||||
@@ -19,32 +43,8 @@ import {
|
|||||||
QueryResultsType,
|
QueryResultsType,
|
||||||
QueryReturnType,
|
QueryReturnType,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT } from '../engine';
|
|
||||||
import {
|
|
||||||
BrowseMediaStep,
|
|
||||||
BrowseMediaTarget,
|
|
||||||
} from '../../utils/ha/browse-media/browse-media-manager';
|
|
||||||
import { allPromises, formatDate, isValidDate } from '../../utils/basic';
|
|
||||||
import endOfDay from 'date-fns/endOfDay';
|
|
||||||
import {
|
|
||||||
BROWSE_MEDIA_CACHE_SECONDS,
|
|
||||||
BrowseMedia,
|
|
||||||
MEDIA_CLASS_IMAGE,
|
|
||||||
MEDIA_CLASS_VIDEO,
|
|
||||||
RichBrowseMedia,
|
|
||||||
} from '../../utils/ha/browse-media/types';
|
|
||||||
import parse from 'date-fns/parse';
|
|
||||||
import { MotionEyeEventQueryResults } from './types';
|
|
||||||
import orderBy from 'lodash-es/orderBy';
|
|
||||||
import startOfDay from 'date-fns/startOfDay';
|
|
||||||
import add from 'date-fns/add';
|
|
||||||
import {
|
|
||||||
BrowseMediaCameraManagerEngine,
|
|
||||||
getViewMediaFromBrowseMediaArray,
|
|
||||||
isMediaWithinDates,
|
|
||||||
} from '../browse-media/engine-browse-media';
|
|
||||||
import { BrowseMediaMetadata } from '../browse-media/types';
|
|
||||||
import motioneyeLogo from './assets/motioneye-logo.svg';
|
import motioneyeLogo from './assets/motioneye-logo.svg';
|
||||||
|
import { MotionEyeEventQueryResults } from './types';
|
||||||
|
|
||||||
class MotionEyeQueryResultsClassifier {
|
class MotionEyeQueryResultsClassifier {
|
||||||
public static isMotionEyeEventQueryResults(
|
public static isMotionEyeEventQueryResults(
|
||||||
|
|||||||
+10
-11
@@ -1,4 +1,4 @@
|
|||||||
import { CameraConfig } from '../types';
|
import { CameraConfig } from '../config/types';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
import { CameraManagerEngine } from './engine';
|
import { CameraManagerEngine } from './engine';
|
||||||
import { CameraConfigs, Engine } from './types';
|
import { CameraConfigs, Engine } from './types';
|
||||||
@@ -41,6 +41,13 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
|||||||
this._enginesByType.set(engine.getEngineType(), engine);
|
this._enginesByType.set(engine.getEngineType(), engine);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public reset(): void {
|
||||||
|
this._allConfigs.clear();
|
||||||
|
this._visibleConfigs.clear();
|
||||||
|
this._enginesByCamera.clear();
|
||||||
|
this._enginesByType.clear();
|
||||||
|
}
|
||||||
|
|
||||||
public getCameraConfig(cameraID: string): CameraConfig | null {
|
public getCameraConfig(cameraID: string): CameraConfig | null {
|
||||||
return this._allConfigs.get(cameraID) ?? null;
|
return this._allConfigs.get(cameraID) ?? null;
|
||||||
}
|
}
|
||||||
@@ -74,11 +81,7 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getCameraConfigForMedia(media: ViewMedia): CameraConfig | null {
|
public getCameraConfigForMedia(media: ViewMedia): CameraConfig | null {
|
||||||
const cameraID = media.getCameraID();
|
return this.getCameraConfig(media.getCameraID());
|
||||||
if (!cameraID) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return this.getCameraConfig(cameraID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEngineOfType(engine: Engine): CameraManagerEngine | null {
|
public getEngineOfType(engine: Engine): CameraManagerEngine | null {
|
||||||
@@ -108,11 +111,7 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getEngineForMedia(media: ViewMedia): CameraManagerEngine | null {
|
public getEngineForMedia(media: ViewMedia): CameraManagerEngine | null {
|
||||||
const cameraID = media.getCameraID();
|
return this.getEngineForCameraID(media.getCameraID());
|
||||||
if (!cameraID) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return this.getEngineForCameraID(cameraID);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllEngines(): CameraManagerEngine[] {
|
public getAllEngines(): CameraManagerEngine[] {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { CameraConfig, FrigateCardView } from '../types';
|
import { CameraConfig, FrigateCardView } from '../config/types';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
|
|
||||||
// ====
|
// ====
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import startOfHour from 'date-fns/startOfHour';
|
|
||||||
import endOfHour from 'date-fns/endOfHour';
|
|
||||||
import startOfDay from 'date-fns/startOfDay';
|
|
||||||
import endOfDay from 'date-fns/endOfDay';
|
import endOfDay from 'date-fns/endOfDay';
|
||||||
|
import endOfHour from 'date-fns/endOfHour';
|
||||||
import endOfMinute from 'date-fns/endOfMinute';
|
import endOfMinute from 'date-fns/endOfMinute';
|
||||||
import { DateRange } from './range';
|
import startOfDay from 'date-fns/startOfDay';
|
||||||
|
import startOfHour from 'date-fns/startOfHour';
|
||||||
import orderBy from 'lodash-es/orderBy';
|
import orderBy from 'lodash-es/orderBy';
|
||||||
import uniqBy from 'lodash-es/uniqBy';
|
import uniqBy from 'lodash-es/uniqBy';
|
||||||
|
import { CameraConfig } from '../config/types';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
import { CameraConfig } from '../types';
|
import { DateRange } from './range';
|
||||||
|
|
||||||
export const convertRangeToCacheFriendlyTimes = (
|
export const convertRangeToCacheFriendlyTimes = (
|
||||||
range: DateRange,
|
range: DateRange,
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import {
|
||||||
|
Actions,
|
||||||
|
ActionsConfig,
|
||||||
|
FrigateCardCustomAction,
|
||||||
|
FRIGATE_CARD_VIEW_DEFAULT,
|
||||||
|
} from '../config/types.js';
|
||||||
|
import {
|
||||||
|
convertActionToFrigateCardCustomAction,
|
||||||
|
frigateCardHandleActionConfig,
|
||||||
|
getActionConfigGivenAction,
|
||||||
|
} from '../utils/action.js';
|
||||||
|
import { getStreamCameraID } from '../utils/substream.js';
|
||||||
|
import { CardActionsManagerAPI } from './types.js';
|
||||||
|
|
||||||
|
export class ActionsManager {
|
||||||
|
protected _api: CardActionsManagerAPI;
|
||||||
|
|
||||||
|
constructor(api: CardActionsManagerAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge card-wide and view-specific actions.
|
||||||
|
* @returns A combined set of action.
|
||||||
|
*/
|
||||||
|
public getMergedActions(): ActionsConfig {
|
||||||
|
const view = this._api.getViewManager().getView();
|
||||||
|
if (this._api.getMessageManager().hasMessage()) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const config = this._api.getConfigManager().getConfig();
|
||||||
|
let specificActions: Actions | undefined = undefined;
|
||||||
|
if (view?.is('live')) {
|
||||||
|
specificActions = config?.live.actions;
|
||||||
|
} else if (view?.isGalleryView()) {
|
||||||
|
specificActions = config?.media_gallery?.actions;
|
||||||
|
} else if (view?.isViewerView()) {
|
||||||
|
specificActions = config?.media_viewer.actions;
|
||||||
|
} else if (view?.is('image')) {
|
||||||
|
specificActions = config?.image?.actions;
|
||||||
|
} else {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return { ...config?.view.actions, ...specificActions };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle an human interaction called on an element (e.g. 'tap').
|
||||||
|
*/
|
||||||
|
public handleInteraction(interaction: string): void {
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
const config = this.getMergedActions();
|
||||||
|
const actionConfig = getActionConfigGivenAction(interaction, config);
|
||||||
|
if (
|
||||||
|
hass &&
|
||||||
|
config &&
|
||||||
|
interaction &&
|
||||||
|
// Don't call frigateCardHandleActionConfig() unless there is explicitly an
|
||||||
|
// action defined (as it uses a default that is unhelpful for views that
|
||||||
|
// have default tap/click actions).
|
||||||
|
actionConfig
|
||||||
|
) {
|
||||||
|
frigateCardHandleActionConfig(
|
||||||
|
this._api.getCardElementManager().getElement(),
|
||||||
|
hass,
|
||||||
|
config,
|
||||||
|
interaction,
|
||||||
|
actionConfig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleActionEvent = (ev: Event): void => {
|
||||||
|
if (!('detail' in ev)) {
|
||||||
|
// The event may not actually be a CustomEvent object, but may still have a
|
||||||
|
// detail field. See:
|
||||||
|
// https://github.com/custom-cards/custom-card-helpers/blob/master/src/fire-event.ts#L70
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail);
|
||||||
|
if (frigateCardAction) {
|
||||||
|
this.executeAction(frigateCardAction);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a card action.
|
||||||
|
* @param frigateCardAction
|
||||||
|
* @returns `true` if an action is executed.
|
||||||
|
*/
|
||||||
|
public async executeAction(frigateCardAction: FrigateCardCustomAction): Promise<void> {
|
||||||
|
const config = this._api.getConfigManager().getConfig();
|
||||||
|
const mediaLoadedInfoManager = this._api.getMediaLoadedInfoManager();
|
||||||
|
|
||||||
|
if (
|
||||||
|
// Command not intended for this card (e.g. query string command).
|
||||||
|
frigateCardAction.card_id &&
|
||||||
|
config?.card_id !== frigateCardAction.card_id
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: This function needs to process (view-related) commands even when
|
||||||
|
// _view has not yet been initialized (since it may be used to set a view
|
||||||
|
// via the querystring).
|
||||||
|
const view = this._api.getViewManager().getView();
|
||||||
|
|
||||||
|
const action = frigateCardAction.frigate_card_action;
|
||||||
|
|
||||||
|
switch (action) {
|
||||||
|
case 'default':
|
||||||
|
this._api.getViewManager().setViewDefault();
|
||||||
|
break;
|
||||||
|
case 'clip':
|
||||||
|
case 'clips':
|
||||||
|
case 'image':
|
||||||
|
case 'live':
|
||||||
|
case 'recording':
|
||||||
|
case 'recordings':
|
||||||
|
case 'snapshot':
|
||||||
|
case 'snapshots':
|
||||||
|
case 'timeline':
|
||||||
|
this._api.getViewManager().setViewByParameters({
|
||||||
|
viewName: action,
|
||||||
|
cameraID: view?.camera,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case 'download':
|
||||||
|
await this._api.getDownloadManager().downloadViewerMedia();
|
||||||
|
break;
|
||||||
|
case 'camera_ui':
|
||||||
|
this._api.getCameraURLManager().openURL();
|
||||||
|
break;
|
||||||
|
case 'expand':
|
||||||
|
this._api.getExpandManager().toggleExpanded();
|
||||||
|
break;
|
||||||
|
case 'fullscreen':
|
||||||
|
this._api.getFullscreenManager().toggleFullscreen();
|
||||||
|
break;
|
||||||
|
case 'menu_toggle':
|
||||||
|
// This is a rare code path: this would only be used if someone has a
|
||||||
|
// menu toggle action configured outside of the menu itself (e.g.
|
||||||
|
// picture elements).
|
||||||
|
this._api.getCardElementManager().toggleMenu();
|
||||||
|
break;
|
||||||
|
case 'camera_select':
|
||||||
|
const selectCameraID = frigateCardAction.camera;
|
||||||
|
if (view) {
|
||||||
|
const viewOnCameraSelect = config?.view.camera_select ?? 'current';
|
||||||
|
const targetViewName =
|
||||||
|
viewOnCameraSelect === 'current' ? view.view : viewOnCameraSelect;
|
||||||
|
const verifiedViewName = this._api
|
||||||
|
.getViewManager()
|
||||||
|
.isViewSupportedByCamera(selectCameraID, targetViewName)
|
||||||
|
? targetViewName
|
||||||
|
: FRIGATE_CARD_VIEW_DEFAULT;
|
||||||
|
this._api.getViewManager().setViewByParameters({
|
||||||
|
viewName: verifiedViewName,
|
||||||
|
cameraID: selectCameraID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'live_substream_select': {
|
||||||
|
this._api.getViewManager().setViewWithSubstream(frigateCardAction.camera);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'live_substream_off': {
|
||||||
|
this._api.getViewManager().setViewWithoutSubstream();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'live_substream_on': {
|
||||||
|
this._api.getViewManager().setViewWithSubstream();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'media_player':
|
||||||
|
const mediaPlayer = frigateCardAction.media_player;
|
||||||
|
const mediaPlayerController = this._api.getMediaPlayerManager();
|
||||||
|
const media = view?.queryResults?.getSelectedResult() ?? null;
|
||||||
|
|
||||||
|
if (frigateCardAction.media_player_action === 'stop') {
|
||||||
|
await mediaPlayerController.stop(mediaPlayer);
|
||||||
|
} else if (view?.is('live')) {
|
||||||
|
await mediaPlayerController.playLive(mediaPlayer, getStreamCameraID(view));
|
||||||
|
} else if (view?.isViewerView() && media) {
|
||||||
|
await mediaPlayerController.playMedia(mediaPlayer, media);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'diagnostics':
|
||||||
|
this._api.getViewManager().setViewByParameters({ viewName: 'diagnostics' });
|
||||||
|
break;
|
||||||
|
case 'microphone_mute':
|
||||||
|
this._api.getMicrophoneManager().mute();
|
||||||
|
break;
|
||||||
|
case 'microphone_unmute':
|
||||||
|
await this._api.getMicrophoneManager().unmute();
|
||||||
|
break;
|
||||||
|
case 'mute':
|
||||||
|
await mediaLoadedInfoManager.get()?.player?.mute();
|
||||||
|
break;
|
||||||
|
case 'unmute':
|
||||||
|
await mediaLoadedInfoManager.get()?.player?.unmute();
|
||||||
|
break;
|
||||||
|
case 'play':
|
||||||
|
await mediaLoadedInfoManager.get()?.player?.play();
|
||||||
|
break;
|
||||||
|
case 'pause':
|
||||||
|
await mediaLoadedInfoManager.get()?.player?.pause();
|
||||||
|
break;
|
||||||
|
case 'screenshot':
|
||||||
|
await this._api.getDownloadManager().downloadScreenshot();
|
||||||
|
break;
|
||||||
|
case 'display_mode_select':
|
||||||
|
this._api
|
||||||
|
.getViewManager()
|
||||||
|
.setViewWithNewDisplayMode(frigateCardAction.display_mode);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(`Frigate card received unknown card action: ${action}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
import { Timer } from '../utils/timer';
|
||||||
|
import { CardAutoRefreshAPI } from './types';
|
||||||
|
|
||||||
|
export class AutoUpdateManager {
|
||||||
|
protected _timer = new Timer();
|
||||||
|
protected _api: CardAutoRefreshAPI;
|
||||||
|
|
||||||
|
constructor(api: CardAutoRefreshAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the update timer to trigger an update refresh every
|
||||||
|
* `view.update_seconds`.
|
||||||
|
*/
|
||||||
|
public startDefaultViewTimer(): void {
|
||||||
|
this._timer.stop();
|
||||||
|
const updateSeconds = this._api.getConfigManager().getConfig()
|
||||||
|
?.view.update_seconds;
|
||||||
|
if (updateSeconds) {
|
||||||
|
this._timer.start(updateSeconds, () => {
|
||||||
|
if (this._isAutomatedUpdateAllowed()) {
|
||||||
|
this._api.getViewManager().setViewDefault();
|
||||||
|
} else {
|
||||||
|
// Not allowed to update this time around, but try again at the next
|
||||||
|
// interval.
|
||||||
|
this.startDefaultViewTimer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _isAutomatedUpdateAllowed(): boolean {
|
||||||
|
const triggers = this._api.getTriggersManager();
|
||||||
|
const config = this._api.getConfigManager().getConfig();
|
||||||
|
const interactionManager = this._api.getInteractionManager();
|
||||||
|
|
||||||
|
return (
|
||||||
|
!triggers.isTriggered() &&
|
||||||
|
(config?.view.update_force || !interactionManager.hasInteraction())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import { Automation, AutomationActions, Automations } from '../config/types.js';
|
||||||
|
import { localize } from '../localize/localize.js';
|
||||||
|
import { frigateCardHandleAction } from '../utils/action.js';
|
||||||
|
import { CardAutomationsAPI } from './types.js';
|
||||||
|
|
||||||
|
const MAX_NESTED_AUTOMATION_EXECUTIONS = 10;
|
||||||
|
|
||||||
|
export class AutomationsManager {
|
||||||
|
protected _api: CardAutomationsAPI;
|
||||||
|
|
||||||
|
protected _automations: Automations;
|
||||||
|
protected _priorEvaluations: Map<Automation, boolean> = new Map();
|
||||||
|
|
||||||
|
// A counter to avoid infinite loops, increases every time actions are run,
|
||||||
|
// decreases every time actions are complete.
|
||||||
|
protected _nestedAutomationExecutions = 0;
|
||||||
|
|
||||||
|
constructor(api: CardAutomationsAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setAutomationsFromConfig() {
|
||||||
|
this._automations = this._api
|
||||||
|
.getConfigManager()
|
||||||
|
.getNonOverriddenConfig()?.automations;
|
||||||
|
}
|
||||||
|
|
||||||
|
public execute(): void {
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
|
// Never execute automations if there's an error (as our automation loop
|
||||||
|
// avoidance -- which shows as an error -- would not work!).
|
||||||
|
if (!hass || this._api.getMessageManager().hasErrorMessage()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const actionsToRun: AutomationActions[] = [];
|
||||||
|
for (const automation of this._automations ?? []) {
|
||||||
|
const shouldExecute = this._api
|
||||||
|
.getConditionsManager()
|
||||||
|
.evaluateCondition(automation.conditions);
|
||||||
|
const actions = shouldExecute ? automation.actions : automation.actions_not;
|
||||||
|
const priorEvaluation = this._priorEvaluations.get(automation);
|
||||||
|
this._priorEvaluations.set(automation, shouldExecute);
|
||||||
|
if (shouldExecute !== priorEvaluation && actions) {
|
||||||
|
actionsToRun.push(actions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
++this._nestedAutomationExecutions;
|
||||||
|
if (this._nestedAutomationExecutions > MAX_NESTED_AUTOMATION_EXECUTIONS) {
|
||||||
|
this._api.getMessageManager().setMessageIfHigherPriority({
|
||||||
|
type: 'error',
|
||||||
|
message: localize('error.too_many_automations'),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
actionsToRun.forEach((actions) => {
|
||||||
|
frigateCardHandleAction(
|
||||||
|
this._api.getCardElementManager().getElement(),
|
||||||
|
hass,
|
||||||
|
{},
|
||||||
|
actions,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
--this._nestedAutomationExecutions;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import { CardCameraURLAPI } from './types';
|
||||||
|
|
||||||
|
export class CameraURLManager {
|
||||||
|
protected _api: CardCameraURLAPI;
|
||||||
|
|
||||||
|
constructor(api: CardCameraURLAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public openURL(): void {
|
||||||
|
const url = this.getCameraURL();
|
||||||
|
if (url) {
|
||||||
|
window.open(url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasCameraURL(): boolean {
|
||||||
|
return !!this.getCameraURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCameraURL(): string | null {
|
||||||
|
const view = this._api.getViewManager().getView();
|
||||||
|
const media = view?.queryResults?.getSelectedResult() ?? null;
|
||||||
|
const endpoints = view?.camera
|
||||||
|
? this._api.getCameraManager().getCameraEndpoints(view.camera, {
|
||||||
|
view: view.view,
|
||||||
|
...(media && { media: media }),
|
||||||
|
}) ?? null
|
||||||
|
: null;
|
||||||
|
return endpoints?.ui?.endpoint ?? null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,117 @@
|
|||||||
|
import { LitElement, ReactiveControllerHost } from 'lit';
|
||||||
|
import { ActionEventTarget } from '../action-handler-directive';
|
||||||
|
import { setOrRemoveAttribute } from '../utils/basic';
|
||||||
|
import { isCardInPanel } from '../utils/ha';
|
||||||
|
import { CardElementAPI } from './types';
|
||||||
|
|
||||||
|
export type ScrollCallback = () => void;
|
||||||
|
export type MenuToggleCallback = () => void;
|
||||||
|
|
||||||
|
export type CardHTMLElement = LitElement & ReactiveControllerHost & ActionEventTarget;
|
||||||
|
|
||||||
|
export class CardElementManager {
|
||||||
|
protected _api: CardElementAPI;
|
||||||
|
|
||||||
|
protected _element: CardHTMLElement;
|
||||||
|
protected _scrollCallback: ScrollCallback;
|
||||||
|
protected _menuToggleCallback: MenuToggleCallback;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
api: CardElementAPI,
|
||||||
|
element: CardHTMLElement,
|
||||||
|
scrollCallback: ScrollCallback,
|
||||||
|
menuToggleCallback: MenuToggleCallback,
|
||||||
|
) {
|
||||||
|
this._api = api;
|
||||||
|
|
||||||
|
this._element = element;
|
||||||
|
this._scrollCallback = scrollCallback;
|
||||||
|
this._menuToggleCallback = menuToggleCallback;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getElement(): HTMLElement {
|
||||||
|
return this._element;
|
||||||
|
}
|
||||||
|
|
||||||
|
public scrollReset(): void {
|
||||||
|
this._scrollCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleMenu(): void {
|
||||||
|
this._menuToggleCallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
public update(): void {
|
||||||
|
this._element.requestUpdate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasUpdated(): boolean {
|
||||||
|
return this._element.hasUpdated;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCardHeight(): number {
|
||||||
|
return this._element.getBoundingClientRect().height;
|
||||||
|
}
|
||||||
|
|
||||||
|
public elementConnected(): void {
|
||||||
|
// Whether or not the card is in panel mode on the dashboard.
|
||||||
|
setOrRemoveAttribute(this._element, isCardInPanel(this._element), 'panel');
|
||||||
|
|
||||||
|
this._api.getFullscreenManager().connect();
|
||||||
|
|
||||||
|
this._element.addEventListener(
|
||||||
|
'mousemove',
|
||||||
|
this._api.getInteractionManager().reportInteraction,
|
||||||
|
);
|
||||||
|
this._element.addEventListener(
|
||||||
|
'll-custom',
|
||||||
|
this._api.getActionsManager().handleActionEvent,
|
||||||
|
);
|
||||||
|
this._element.addEventListener(
|
||||||
|
'@action',
|
||||||
|
this._api.getInteractionManager().reportInteraction,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Listen for HA `navigate` actions.
|
||||||
|
// See: https://github.com/home-assistant/frontend/blob/273992c8e9c3062c6e49481b6d7d688a07067232/src/common/navigate.ts#L43
|
||||||
|
window.addEventListener(
|
||||||
|
'location-changed',
|
||||||
|
this._api.getQueryStringManager().executeAll,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Listen for history state changes (i.e. user using the browser
|
||||||
|
// back/forward controls).
|
||||||
|
window.addEventListener('popstate', this._api.getQueryStringManager().executeAll);
|
||||||
|
|
||||||
|
// Manually call the location change handler as the card will be
|
||||||
|
// disconnected/reconnected when dashboard 'tab' changes happen within HA.
|
||||||
|
this._api.getQueryStringManager().executeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public elementDisconnected(): void {
|
||||||
|
setOrRemoveAttribute(this._element, false, 'panel');
|
||||||
|
|
||||||
|
// When the dashboard 'tab' is changed, the media is effectively unloaded.
|
||||||
|
this._api.getMediaLoadedInfoManager().clear();
|
||||||
|
this._api.getFullscreenManager().disconnect();
|
||||||
|
|
||||||
|
this._element.removeEventListener(
|
||||||
|
'mousemove',
|
||||||
|
this._api.getInteractionManager().reportInteraction,
|
||||||
|
);
|
||||||
|
this._element.removeEventListener(
|
||||||
|
'll-custom',
|
||||||
|
this._api.getActionsManager().handleActionEvent,
|
||||||
|
);
|
||||||
|
this._element.removeEventListener(
|
||||||
|
'@action',
|
||||||
|
this._api.getInteractionManager().reportInteraction,
|
||||||
|
);
|
||||||
|
|
||||||
|
window.removeEventListener(
|
||||||
|
'location-changed',
|
||||||
|
this._api.getQueryStringManager().executeAll,
|
||||||
|
);
|
||||||
|
window.removeEventListener('popstate', this._api.getQueryStringManager().executeAll);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { HassEntities } from 'home-assistant-js-websocket';
|
import { HassEntities } from 'home-assistant-js-websocket';
|
||||||
import merge from 'lodash-es/merge';
|
import merge from 'lodash-es/merge';
|
||||||
import { copyConfig } from './config-mgmt';
|
import { copyConfig } from '../config-mgmt';
|
||||||
import {
|
import {
|
||||||
FrigateCardCondition,
|
FrigateCardCondition,
|
||||||
FrigateCardConfig,
|
|
||||||
frigateConditionalSchema,
|
frigateConditionalSchema,
|
||||||
OverrideConfigurationKey,
|
OverrideConfigurationKey,
|
||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig,
|
||||||
ViewDisplayMode
|
ViewDisplayMode
|
||||||
} from './types';
|
} from '../config/types';
|
||||||
|
import { CardConditionAPI } from './types';
|
||||||
|
|
||||||
interface ConditionState {
|
interface ConditionState {
|
||||||
view?: string;
|
view?: string;
|
||||||
@@ -69,7 +69,7 @@ type RawOverrides = {
|
|||||||
}[];
|
}[];
|
||||||
|
|
||||||
export function getOverriddenConfig(
|
export function getOverriddenConfig(
|
||||||
controller: Readonly<ConditionController>,
|
manager: Readonly<ConditionsManager>,
|
||||||
config: Readonly<RawFrigateCardConfig>,
|
config: Readonly<RawFrigateCardConfig>,
|
||||||
configOverrides?: Readonly<RawOverrides>,
|
configOverrides?: Readonly<RawOverrides>,
|
||||||
stateOverrides?: Partial<ConditionState>,
|
stateOverrides?: Partial<ConditionState>,
|
||||||
@@ -78,7 +78,7 @@ export function getOverriddenConfig(
|
|||||||
let overridden = false;
|
let overridden = false;
|
||||||
if (configOverrides) {
|
if (configOverrides) {
|
||||||
for (const override of configOverrides) {
|
for (const override of configOverrides) {
|
||||||
if (controller.evaluateCondition(override.conditions, stateOverrides)) {
|
if (manager.evaluateCondition(override.conditions, stateOverrides)) {
|
||||||
merge(output, override.overrides);
|
merge(output, override.overrides);
|
||||||
overridden = true;
|
overridden = true;
|
||||||
}
|
}
|
||||||
@@ -103,19 +103,23 @@ export function getOverridesByKey(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// A tiny wrapper interface to allow the same controller to be passed around
|
// A tiny wrapper interface to allow the same manager to be passed around
|
||||||
// immutably within objects that will not be equal (===). Every state change
|
// immutably within objects that will not be equal (===). Every state change
|
||||||
// generates a new epoch. This is used for Lit rendering to ensure changes to
|
// generates a new epoch. This is used for Lit rendering to ensure changes to
|
||||||
// condition state are recognized as changes even though the controller is the
|
// condition state are recognized as changes even though the manager is the
|
||||||
// same.
|
// same.
|
||||||
export interface ConditionControllerEpoch {
|
export interface ConditionsManagerEpoch {
|
||||||
controller: Readonly<ConditionController>;
|
manager: Readonly<ConditionsManager>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ConditionController {
|
export type ConditionsManagerListener = () => void;
|
||||||
|
|
||||||
|
export class ConditionsManager {
|
||||||
|
protected _api: CardConditionAPI;
|
||||||
|
|
||||||
protected _state: ConditionState = {};
|
protected _state: ConditionState = {};
|
||||||
protected _epoch: ConditionControllerEpoch = this._createEpoch();
|
protected _epoch: ConditionsManagerEpoch = this._createEpoch();
|
||||||
protected _stateListeners: (() => void)[] = [];
|
protected _listeners: ConditionsManagerListener[];
|
||||||
|
|
||||||
// Whether or not to include HA state in ConditionState. Doing so increases
|
// Whether or not to include HA state in ConditionState. Doing so increases
|
||||||
// CPU usage as HA state is pumped out very fast, so this is only enabled if
|
// CPU usage as HA state is pumped out very fast, so this is only enabled if
|
||||||
@@ -124,29 +128,59 @@ export class ConditionController {
|
|||||||
protected _mediaQueries: MediaQueryList[] = [];
|
protected _mediaQueries: MediaQueryList[] = [];
|
||||||
protected _mediaQueryTrigger = () => this._triggerChange();
|
protected _mediaQueryTrigger = () => this._triggerChange();
|
||||||
|
|
||||||
constructor(config?: FrigateCardConfig) {
|
constructor(api: CardConditionAPI, listener?: ConditionsManagerListener) {
|
||||||
if (config) {
|
this._api = api;
|
||||||
this._initConditions(config);
|
this._listeners = [
|
||||||
}
|
() => this._api.getConfigManager().computeOverrideConfig(),
|
||||||
|
() => this._api.getAutomationsManager().execute(),
|
||||||
|
...(listener ? [listener] : [])
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public addStateListener(callback: () => void): void {
|
public removeConditions(): void {
|
||||||
this._stateListeners.push(callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeStateListener(callback: () => void): void {
|
|
||||||
this._stateListeners = this._stateListeners.filter(
|
|
||||||
(listener) => listener != callback,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public destroy(): void {
|
|
||||||
this._mediaQueries.forEach((mql) =>
|
this._mediaQueries.forEach((mql) =>
|
||||||
mql.removeEventListener('change', this._mediaQueryTrigger),
|
mql.removeEventListener('change', this._mediaQueryTrigger),
|
||||||
);
|
);
|
||||||
this._mediaQueries = [];
|
this._mediaQueries = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public setConditionsFromConfig(): void {
|
||||||
|
this.removeConditions();
|
||||||
|
|
||||||
|
const getAllConditions = (): FrigateCardCondition[] => {
|
||||||
|
const config = this._api.getConfigManager().getConfig();
|
||||||
|
const conditions: FrigateCardCondition[] = [];
|
||||||
|
config?.overrides?.forEach((override) => conditions.push(override.conditions));
|
||||||
|
|
||||||
|
// Element conditions can be arbitrarily nested underneath conditionals and
|
||||||
|
// custom elements that this card may not known. Here we recursively parse
|
||||||
|
// down the elements tree, parsing as we go to find valid conditions.
|
||||||
|
const getElementsConditions = (data: unknown): void => {
|
||||||
|
const parseResult = frigateConditionalSchema.safeParse(data);
|
||||||
|
if (parseResult.success) {
|
||||||
|
conditions.push(parseResult.data.conditions);
|
||||||
|
parseResult.data.elements?.forEach(getElementsConditions);
|
||||||
|
} else if (data && typeof data === 'object') {
|
||||||
|
Object.keys(data).forEach((key) => getElementsConditions(data[key]));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
config?.elements?.forEach(getElementsConditions);
|
||||||
|
return conditions;
|
||||||
|
};
|
||||||
|
|
||||||
|
const conditions = getAllConditions();
|
||||||
|
this._hasHAStateConditions = conditions.some(
|
||||||
|
(condition) => !!condition.state?.length,
|
||||||
|
);
|
||||||
|
conditions.forEach((condition) => {
|
||||||
|
if (condition.media_query) {
|
||||||
|
const mql = window.matchMedia(condition.media_query);
|
||||||
|
mql.addEventListener('change', this._mediaQueryTrigger);
|
||||||
|
this._mediaQueries.push(mql);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public setState(state: Partial<ConditionState>): void {
|
public setState(state: Partial<ConditionState>): void {
|
||||||
this._state = {
|
this._state = {
|
||||||
...this._state,
|
...this._state,
|
||||||
@@ -155,11 +189,11 @@ export class ConditionController {
|
|||||||
this._triggerChange();
|
this._triggerChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
get hasHAStateConditions(): boolean {
|
public hasHAStateConditions(): boolean {
|
||||||
return this._hasHAStateConditions;
|
return this._hasHAStateConditions;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEpoch(): ConditionControllerEpoch {
|
public getEpoch(): ConditionsManagerEpoch {
|
||||||
return this._epoch;
|
return this._epoch;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -212,46 +246,12 @@ export class ConditionController {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _createEpoch(): ConditionControllerEpoch {
|
protected _createEpoch(): ConditionsManagerEpoch {
|
||||||
return { controller: this };
|
return { manager: this };
|
||||||
}
|
}
|
||||||
|
|
||||||
protected _triggerChange(): void {
|
protected _triggerChange(): void {
|
||||||
this._epoch = this._createEpoch();
|
this._epoch = this._createEpoch();
|
||||||
this._stateListeners.forEach((listener) => listener());
|
this._listeners.forEach((listener) => listener());
|
||||||
}
|
|
||||||
|
|
||||||
protected _initConditions(config: FrigateCardConfig): void {
|
|
||||||
const getAllConditions = (config: FrigateCardConfig): FrigateCardCondition[] => {
|
|
||||||
const conditions: FrigateCardCondition[] = [];
|
|
||||||
config.overrides?.forEach((override) => conditions.push(override.conditions));
|
|
||||||
|
|
||||||
// Element conditions can be arbitrarily nested underneath conditionals and
|
|
||||||
// custom elements that this card may not known. Here we recursively parse
|
|
||||||
// down the elements tree, parsing as we go to find valid conditions.
|
|
||||||
const getElementsConditions = (data: unknown): void => {
|
|
||||||
const parseResult = frigateConditionalSchema.safeParse(data);
|
|
||||||
if (parseResult.success) {
|
|
||||||
conditions.push(parseResult.data.conditions);
|
|
||||||
parseResult.data.elements?.forEach(getElementsConditions);
|
|
||||||
} else if (data && typeof data === 'object') {
|
|
||||||
Object.keys(data).forEach((key) => getElementsConditions(data[key]));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
config.elements?.forEach(getElementsConditions);
|
|
||||||
return conditions;
|
|
||||||
};
|
|
||||||
|
|
||||||
const conditions = getAllConditions(config);
|
|
||||||
this._hasHAStateConditions = conditions.some(
|
|
||||||
(condition) => !!condition.state?.length,
|
|
||||||
);
|
|
||||||
conditions.forEach((condition) => {
|
|
||||||
if (condition.media_query) {
|
|
||||||
const mql = window.matchMedia(condition.media_query);
|
|
||||||
mql.addEventListener('change', this._mediaQueryTrigger);
|
|
||||||
this._mediaQueries.push(mql);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import { isConfigUpgradeable } from '../config-mgmt';
|
||||||
|
import {
|
||||||
|
CardWideConfig,
|
||||||
|
FrigateCardConfig,
|
||||||
|
frigateCardConfigSchema,
|
||||||
|
RawFrigateCardConfig
|
||||||
|
} from '../config/types';
|
||||||
|
import { localize } from '../localize/localize';
|
||||||
|
import { setLowPerformanceProfile } from '../performance.js';
|
||||||
|
import { getParseErrorPaths } from '../utils/zod.js';
|
||||||
|
import { getOverriddenConfig } from './conditions-manager';
|
||||||
|
import { InitializationAspect } from './initialization-manager';
|
||||||
|
import { CardConfigAPI } from './types';
|
||||||
|
|
||||||
|
export class ConfigManager {
|
||||||
|
protected _api: CardConfigAPI;
|
||||||
|
|
||||||
|
// The main base configuration object. For most usecases use getConfig() to
|
||||||
|
// get the correct configuration (which will return overrides as appropriate).
|
||||||
|
// This variable must be called `_config` or `config` to be compatible with
|
||||||
|
// card-mod.
|
||||||
|
protected _config: FrigateCardConfig | null = null;
|
||||||
|
protected _overriddenConfig: FrigateCardConfig | null = null;
|
||||||
|
protected _rawConfig: RawFrigateCardConfig | null = null;
|
||||||
|
protected _cardWideConfig: CardWideConfig | null = null;
|
||||||
|
|
||||||
|
constructor(api: CardConfigAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasConfig(): boolean {
|
||||||
|
return !!this.getConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConfig(): FrigateCardConfig | null {
|
||||||
|
return this._overriddenConfig ?? this._config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCardWideConfig(): CardWideConfig | null {
|
||||||
|
return this._cardWideConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getNonOverriddenConfig(): FrigateCardConfig | null {
|
||||||
|
return this._config;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getRawConfig(): RawFrigateCardConfig | null {
|
||||||
|
return this._rawConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setConfig(inputConfig?: RawFrigateCardConfig): void {
|
||||||
|
if (!inputConfig) {
|
||||||
|
throw new Error(localize('error.invalid_configuration'));
|
||||||
|
}
|
||||||
|
|
||||||
|
const parseResult = frigateCardConfigSchema.safeParse(inputConfig);
|
||||||
|
if (!parseResult.success) {
|
||||||
|
const configUpgradeable = isConfigUpgradeable(inputConfig);
|
||||||
|
const hint = getParseErrorPaths(parseResult.error);
|
||||||
|
let upgradeMessage = '';
|
||||||
|
if (configUpgradeable) {
|
||||||
|
upgradeMessage = `${localize('error.upgrade_available')}. `;
|
||||||
|
}
|
||||||
|
throw new Error(
|
||||||
|
upgradeMessage +
|
||||||
|
`${localize('error.invalid_configuration')}: ` +
|
||||||
|
(hint && hint.size
|
||||||
|
? JSON.stringify([...hint], null, ' ')
|
||||||
|
: localize('error.invalid_configuration_no_hint')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const config =
|
||||||
|
parseResult.data.performance.profile !== 'low'
|
||||||
|
? parseResult.data
|
||||||
|
: setLowPerformanceProfile(inputConfig, parseResult.data);
|
||||||
|
|
||||||
|
this._rawConfig = inputConfig;
|
||||||
|
if (isEqual(this._config, config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._config = config;
|
||||||
|
this._cardWideConfig = {
|
||||||
|
performance: config.performance,
|
||||||
|
debug: config.debug,
|
||||||
|
};
|
||||||
|
|
||||||
|
this._api.getConditionsManager().setConditionsFromConfig();
|
||||||
|
this._api.getConditionsManager().setState({
|
||||||
|
view: undefined,
|
||||||
|
displayMode: undefined,
|
||||||
|
camera: undefined,
|
||||||
|
});
|
||||||
|
this._api.getMediaLoadedInfoManager().clear();
|
||||||
|
this._api.getViewManager().reset();
|
||||||
|
this._api.getMessageManager().reset();
|
||||||
|
this._api.getAutomationsManager().setAutomationsFromConfig();
|
||||||
|
this._api.getStyleManager().setPerformance();
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
|
|
||||||
|
this.computeOverrideConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
public computeOverrideConfig(): void {
|
||||||
|
const conditionsManager = this._api.getConditionsManager();
|
||||||
|
if (!this._config) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const overriddenConfig = getOverriddenConfig(
|
||||||
|
conditionsManager,
|
||||||
|
this._config,
|
||||||
|
this._config.overrides,
|
||||||
|
) as FrigateCardConfig;
|
||||||
|
|
||||||
|
// Save on Lit re-rendering costs by only updating the configuration if it
|
||||||
|
// actually changes.
|
||||||
|
if (isEqual(overriddenConfig, this._overriddenConfig)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const previousConfig = this._overriddenConfig;
|
||||||
|
this._overriddenConfig = overriddenConfig;
|
||||||
|
|
||||||
|
this._api.getStyleManager().setMinMaxHeight();
|
||||||
|
|
||||||
|
if (
|
||||||
|
previousConfig &&
|
||||||
|
(!isEqual(previousConfig?.cameras, this._overriddenConfig?.cameras) ||
|
||||||
|
!isEqual(previousConfig?.cameras_global, this._overriddenConfig?.cameras_global))
|
||||||
|
) {
|
||||||
|
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
previousConfig &&
|
||||||
|
previousConfig?.live.microphone.always_connected !==
|
||||||
|
this._overriddenConfig?.live.microphone.always_connected
|
||||||
|
) {
|
||||||
|
this._api
|
||||||
|
.getInitializationManager()
|
||||||
|
.uninitialize(InitializationAspect.MICROPHONE_CONNECT);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,259 @@
|
|||||||
|
import { LovelaceCardEditor } from 'custom-card-helpers';
|
||||||
|
import { ReactiveController } from 'lit';
|
||||||
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
|
import { FrigateCardConfig } from '../config/types';
|
||||||
|
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||||
|
import { EntityCache } from '../utils/ha/entity-registry/cache';
|
||||||
|
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||||
|
import { ActionsManager } from './actions-manager';
|
||||||
|
import { AutoUpdateManager } from './auto-update-manager';
|
||||||
|
import { AutomationsManager } from './automations-manager';
|
||||||
|
import { CameraURLManager } from './camera-url-manager';
|
||||||
|
import {
|
||||||
|
CardElementManager,
|
||||||
|
CardHTMLElement,
|
||||||
|
MenuToggleCallback,
|
||||||
|
ScrollCallback,
|
||||||
|
} from './card-element-manager';
|
||||||
|
import { ConditionsManager, ConditionsManagerListener } from './conditions-manager';
|
||||||
|
import { ConfigManager } from './config-manager';
|
||||||
|
import { DownloadManager } from './download-manager';
|
||||||
|
import { ExpandManager } from './expand-manager';
|
||||||
|
import { FullscreenManager } from './fullscreen-manager';
|
||||||
|
import { HASSManager } from './hass-manager';
|
||||||
|
import { InitializationManager } from './initialization-manager';
|
||||||
|
import { InteractionManager } from './interaction-manager';
|
||||||
|
import { MediaLoadedInfoManager } from './media-info-manager';
|
||||||
|
import { MediaPlayerManager } from './media-player-manager';
|
||||||
|
import { MessageManager } from './message-manager';
|
||||||
|
import { MicrophoneManager } from './microphone-manager';
|
||||||
|
import { QueryStringManager } from './query-string-manager';
|
||||||
|
import { StyleManager } from './style-manager';
|
||||||
|
import { TriggersManager } from './triggers-manager';
|
||||||
|
import {
|
||||||
|
CardActionsManagerAPI,
|
||||||
|
CardAutoRefreshAPI,
|
||||||
|
CardAutomationsAPI,
|
||||||
|
CardCameraAPI,
|
||||||
|
CardCameraURLAPI,
|
||||||
|
CardConditionAPI,
|
||||||
|
CardConfigAPI,
|
||||||
|
CardDownloadAPI,
|
||||||
|
CardElementAPI,
|
||||||
|
CardExpandAPI,
|
||||||
|
CardFullscreenAPI,
|
||||||
|
CardHASSAPI,
|
||||||
|
CardInitializerAPI,
|
||||||
|
CardInteractionAPI,
|
||||||
|
CardMediaLoadedAPI,
|
||||||
|
CardMediaPlayerAPI,
|
||||||
|
CardMessageAPI,
|
||||||
|
CardMicrophoneAPI,
|
||||||
|
CardQueryStringAPI,
|
||||||
|
CardStyleAPI,
|
||||||
|
CardTriggersAPI,
|
||||||
|
CardViewAPI,
|
||||||
|
} from './types';
|
||||||
|
import { ViewManager } from './view-manager';
|
||||||
|
|
||||||
|
export class CardController
|
||||||
|
implements
|
||||||
|
CardActionsManagerAPI,
|
||||||
|
CardAutomationsAPI,
|
||||||
|
CardAutoRefreshAPI,
|
||||||
|
CardCameraAPI,
|
||||||
|
CardCameraURLAPI,
|
||||||
|
CardConditionAPI,
|
||||||
|
CardConfigAPI,
|
||||||
|
CardDownloadAPI,
|
||||||
|
CardElementAPI,
|
||||||
|
CardExpandAPI,
|
||||||
|
CardFullscreenAPI,
|
||||||
|
CardHASSAPI,
|
||||||
|
CardInitializerAPI,
|
||||||
|
CardInteractionAPI,
|
||||||
|
CardMediaLoadedAPI,
|
||||||
|
CardMediaPlayerAPI,
|
||||||
|
CardMessageAPI,
|
||||||
|
CardMicrophoneAPI,
|
||||||
|
CardQueryStringAPI,
|
||||||
|
CardStyleAPI,
|
||||||
|
CardTriggersAPI,
|
||||||
|
CardViewAPI,
|
||||||
|
ReactiveController
|
||||||
|
{
|
||||||
|
// These properties may be used in the construction of 'managers' (and should
|
||||||
|
// be created first).
|
||||||
|
protected _entityRegistryManager = new EntityRegistryManager(new EntityCache());
|
||||||
|
protected _resolvedMediaCache = new ResolvedMediaCache();
|
||||||
|
|
||||||
|
protected _actionsManager = new ActionsManager(this);
|
||||||
|
protected _automationsManager = new AutomationsManager(this);
|
||||||
|
protected _autoUpdateManager = new AutoUpdateManager(this);
|
||||||
|
protected _cameraManager = new CameraManager(this);
|
||||||
|
protected _cameraURLManager = new CameraURLManager(this);
|
||||||
|
protected _cardElementManager: CardElementManager;
|
||||||
|
protected _conditionsManager: ConditionsManager;
|
||||||
|
protected _configManager = new ConfigManager(this);
|
||||||
|
protected _downloadManager = new DownloadManager(this);
|
||||||
|
protected _expandManager = new ExpandManager(this);
|
||||||
|
protected _fullscreenManager = new FullscreenManager(this);
|
||||||
|
protected _hassManager = new HASSManager(this);
|
||||||
|
protected _initializationManager = new InitializationManager(this);
|
||||||
|
protected _interactionManager = new InteractionManager(this);
|
||||||
|
protected _mediaLoadedInfoManager = new MediaLoadedInfoManager(this);
|
||||||
|
protected _mediaPlayerManager = new MediaPlayerManager(this);
|
||||||
|
protected _messageManager = new MessageManager(this);
|
||||||
|
protected _microphoneManager = new MicrophoneManager(this);
|
||||||
|
protected _queryStringManager = new QueryStringManager(this);
|
||||||
|
protected _styleManager = new StyleManager(this);
|
||||||
|
protected _triggersManager = new TriggersManager(this);
|
||||||
|
protected _viewManager = new ViewManager(this);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
host: CardHTMLElement,
|
||||||
|
scrollCallback: ScrollCallback,
|
||||||
|
menuToggleCallback: MenuToggleCallback,
|
||||||
|
conditionListener: ConditionsManagerListener,
|
||||||
|
) {
|
||||||
|
host.addController(this);
|
||||||
|
|
||||||
|
this._conditionsManager = new ConditionsManager(this, conditionListener);
|
||||||
|
this._cardElementManager = new CardElementManager(
|
||||||
|
this,
|
||||||
|
host,
|
||||||
|
scrollCallback,
|
||||||
|
menuToggleCallback,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// *************************************************************************
|
||||||
|
// Accessors
|
||||||
|
// *************************************************************************
|
||||||
|
|
||||||
|
public getActionsManager(): ActionsManager {
|
||||||
|
return this._actionsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAutomationsManager(): AutomationsManager {
|
||||||
|
return this._automationsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAutoUpdateManager(): AutoUpdateManager {
|
||||||
|
return this._autoUpdateManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCameraManager(): CameraManager {
|
||||||
|
return this._cameraManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCameraURLManager(): CameraURLManager {
|
||||||
|
return this._cameraURLManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCardElementManager(): CardElementManager {
|
||||||
|
return this._cardElementManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConditionsManager(): ConditionsManager {
|
||||||
|
return this._conditionsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async getConfigElement(): Promise<LovelaceCardEditor> {
|
||||||
|
await import('../editor.js');
|
||||||
|
return document.createElement('frigate-card-editor');
|
||||||
|
}
|
||||||
|
|
||||||
|
public getConfigManager(): ConfigManager {
|
||||||
|
return this._configManager;
|
||||||
|
}
|
||||||
|
public getDownloadManager(): DownloadManager {
|
||||||
|
return this._downloadManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEntityRegistryManager(): EntityRegistryManager {
|
||||||
|
return this._entityRegistryManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getExpandManager(): ExpandManager {
|
||||||
|
return this._expandManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getFullscreenManager(): FullscreenManager {
|
||||||
|
return this._fullscreenManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHASSManager(): HASSManager {
|
||||||
|
return this._hassManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getInitializationManager(): InitializationManager {
|
||||||
|
return this._initializationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getInteractionManager(): InteractionManager {
|
||||||
|
return this._interactionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMediaLoadedInfoManager(): MediaLoadedInfoManager {
|
||||||
|
return this._mediaLoadedInfoManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMediaPlayerManager(): MediaPlayerManager {
|
||||||
|
return this._mediaPlayerManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMessageManager(): MessageManager {
|
||||||
|
return this._messageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMicrophoneManager(): MicrophoneManager {
|
||||||
|
return this._microphoneManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getQueryStringManager(): QueryStringManager {
|
||||||
|
return this._queryStringManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getResolvedMediaCache(): ResolvedMediaCache {
|
||||||
|
return this._resolvedMediaCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static getStubConfig(entities: string[]): FrigateCardConfig {
|
||||||
|
const cameraEntity = entities.find((element) => element.startsWith('camera.'));
|
||||||
|
return {
|
||||||
|
cameras: [
|
||||||
|
{
|
||||||
|
camera_entity: cameraEntity ?? 'camera.demo',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
// Need to use 'as unknown' to convince Typescript that this really isn't a
|
||||||
|
// mistake, despite the miniscule size of the configuration vs the full type
|
||||||
|
// description.
|
||||||
|
} as unknown as FrigateCardConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getStyleManager(): StyleManager {
|
||||||
|
return this._styleManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getTriggersManager(): TriggersManager {
|
||||||
|
return this._triggersManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getViewManager(): ViewManager {
|
||||||
|
return this._viewManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
// *************************************************************************
|
||||||
|
// Handlers
|
||||||
|
// *************************************************************************
|
||||||
|
|
||||||
|
public hostConnected(): void {
|
||||||
|
this.getCardElementManager().elementConnected();
|
||||||
|
}
|
||||||
|
|
||||||
|
public hostDisconnected(): void {
|
||||||
|
this.getCardElementManager().elementDisconnected();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
import { downloadMedia, downloadURL } from '../utils/download';
|
||||||
|
import { generateScreenshotTitle } from '../utils/screenshot';
|
||||||
|
import { CardDownloadAPI } from './types';
|
||||||
|
|
||||||
|
export class DownloadManager {
|
||||||
|
protected _api: CardDownloadAPI;
|
||||||
|
|
||||||
|
constructor(api: CardDownloadAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async downloadViewerMedia(): Promise<boolean> {
|
||||||
|
const media = this._api
|
||||||
|
.getViewManager()
|
||||||
|
.getView()
|
||||||
|
?.queryResults?.getSelectedResult();
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
|
if (!media || !hass) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await downloadMedia(hass, this._api.getCameraManager(), media);
|
||||||
|
} catch (error: unknown) {
|
||||||
|
this._api.getMessageManager().setErrorIfHigherPriority(error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async downloadScreenshot(): Promise<void> {
|
||||||
|
const url = await this._api
|
||||||
|
.getMediaLoadedInfoManager()
|
||||||
|
.get()
|
||||||
|
?.player?.getScreenshotURL();
|
||||||
|
if (url) {
|
||||||
|
downloadURL(url, generateScreenshotTitle(this._api.getViewManager().getView()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import { CardExpandAPI } from './types';
|
||||||
|
|
||||||
|
export class ExpandManager {
|
||||||
|
protected _expanded = false;
|
||||||
|
protected _api: CardExpandAPI;
|
||||||
|
|
||||||
|
constructor(api: CardExpandAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isExpanded(): boolean {
|
||||||
|
return this._expanded;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleExpanded(): void {
|
||||||
|
this.setExpanded(!this._expanded);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setExpanded(expanded: boolean): void {
|
||||||
|
if (expanded && this._api.getFullscreenManager().isInFullscreen()) {
|
||||||
|
// Fullscreen and expanded mode are mutually exclusive.
|
||||||
|
this._api.getFullscreenManager().stopFullscreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._expanded = expanded;
|
||||||
|
this._api.getConditionsManager()?.setState({
|
||||||
|
expand: expanded,
|
||||||
|
});
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import screenfull from 'screenfull';
|
||||||
|
import { CardFullscreenAPI } from './types';
|
||||||
|
|
||||||
|
export class FullscreenManager {
|
||||||
|
protected _api: CardFullscreenAPI;
|
||||||
|
|
||||||
|
constructor(api: CardFullscreenAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public connect(): void {
|
||||||
|
if (screenfull.isEnabled) {
|
||||||
|
screenfull.on('change', this._fullscreenHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public disconnect(): void {
|
||||||
|
if (screenfull.isEnabled) {
|
||||||
|
screenfull.off('change', this._fullscreenHandler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isInFullscreen(): boolean {
|
||||||
|
return screenfull.isEnabled && screenfull.isFullscreen;
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggleFullscreen(): void {
|
||||||
|
screenfull.toggle(this._api.getCardElementManager().getElement());
|
||||||
|
}
|
||||||
|
|
||||||
|
public stopFullscreen(): void {
|
||||||
|
screenfull.exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _fullscreenHandler = (): void => {
|
||||||
|
this._api.getExpandManager().setExpanded(false);
|
||||||
|
|
||||||
|
this._api.getConditionsManager()?.setState({
|
||||||
|
fullscreen: this.isInFullscreen(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-render after a change to fullscreen mode to take advantage of
|
||||||
|
// the expanded screen real-estate (vs staying in aspect-ratio locked
|
||||||
|
// modes).
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import { CameraConfig } from '../config/types';
|
||||||
|
import { localize } from '../localize/localize';
|
||||||
|
import { ExtendedHomeAssistant } from '../types';
|
||||||
|
import { hasHAConnectionStateChanged, isHassDifferent } from '../utils/ha';
|
||||||
|
import { CardHASSAPI } from './types';
|
||||||
|
|
||||||
|
export class HASSManager {
|
||||||
|
protected _hass: ExtendedHomeAssistant | null = null;
|
||||||
|
protected _api: CardHASSAPI;
|
||||||
|
|
||||||
|
constructor(api: CardHASSAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getHASS(): ExtendedHomeAssistant | null {
|
||||||
|
return this._hass;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setHASS(hass: ExtendedHomeAssistant): void {
|
||||||
|
const getSelectedCameraConfig = (): CameraConfig | null => {
|
||||||
|
const view = this._api.getViewManager().getView();
|
||||||
|
const cameraManager = this._api.getCameraManager();
|
||||||
|
|
||||||
|
return view && cameraManager
|
||||||
|
? cameraManager?.getStore().getCameraConfig(view.camera) ?? null
|
||||||
|
: null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const oldHass = this._hass;
|
||||||
|
this._hass = hass;
|
||||||
|
|
||||||
|
const selectedCamera = getSelectedCameraConfig();
|
||||||
|
|
||||||
|
if (hasHAConnectionStateChanged(oldHass, hass)) {
|
||||||
|
if (!this._hass?.connected) {
|
||||||
|
this._api.getMessageManager().setMessageIfHigherPriority({
|
||||||
|
message: localize('error.reconnecting'),
|
||||||
|
icon: 'mdi:lan-disconnect',
|
||||||
|
type: 'connection',
|
||||||
|
dotdotdot: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._api.getViewManager().setViewDefault();
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
// Home Assistant pumps a lot of updates through. Re-rendering the card is
|
||||||
|
// necessary at times (e.g. to update the 'clip' view as new clips
|
||||||
|
// arrive), but also is a jarring experience for the user (e.g. if they
|
||||||
|
// are browsing the mini-gallery). Do not allow re-rendering from a Home
|
||||||
|
// Assistant update if there's been recent interaction (e.g. clicks on the
|
||||||
|
// card) or if there is media active playing.
|
||||||
|
this._isAutomatedViewUpdateAllowed() &&
|
||||||
|
isHassDifferent(this._hass, oldHass, [
|
||||||
|
...(this._api.getConfigManager().getConfig()?.view.update_entities ?? []),
|
||||||
|
...(selectedCamera?.triggers.entities ?? []),
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
// If entities being monitored have changed then reset the view to the
|
||||||
|
// default.
|
||||||
|
this._api.getViewManager().setViewDefault();
|
||||||
|
} else if (
|
||||||
|
isHassDifferent(this._hass, oldHass, [
|
||||||
|
...(this._api.getConfigManager().getConfig()?.view.render_entities ?? []),
|
||||||
|
|
||||||
|
// Refresh the card if media player state changes:
|
||||||
|
// https://github.com/dermotduffy/frigate-hass-card/issues/881
|
||||||
|
...this._api.getMediaPlayerManager().getMediaPlayers(),
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._api.getTriggersManager().updateTriggeredCameras(oldHass);
|
||||||
|
|
||||||
|
if (this._api.getConditionsManager().hasHAStateConditions()) {
|
||||||
|
this._api.getConditionsManager().setState({ state: this._hass.states });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dark mode may depend on HASS.
|
||||||
|
this._api.getStyleManager().setLightOrDarkMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _isAutomatedViewUpdateAllowed(): boolean {
|
||||||
|
return (
|
||||||
|
this._api.getConfigManager().getConfig()?.view.update_force ||
|
||||||
|
!this._api.getInteractionManager().hasInteraction()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import { loadLanguages } from '../localize/localize';
|
||||||
|
import { sideLoadHomeAssistantElements } from '../utils/ha';
|
||||||
|
import { Initializer } from '../utils/initializer/initializer';
|
||||||
|
import { CardInitializerAPI } from './types';
|
||||||
|
|
||||||
|
export enum InitializationAspect {
|
||||||
|
LANGUAGES = 'languages',
|
||||||
|
SIDE_LOAD_ELEMENTS = 'side-load-elements',
|
||||||
|
MEDIA_PLAYERS = 'media-players',
|
||||||
|
CAMERAS = 'cameras',
|
||||||
|
MICROPHONE_CONNECT = 'microphone-connect',
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InitializationManager {
|
||||||
|
protected _api: CardInitializerAPI;
|
||||||
|
protected _initializer;
|
||||||
|
|
||||||
|
constructor(api: CardInitializerAPI, initializer?: Initializer) {
|
||||||
|
this._api = api;
|
||||||
|
this._initializer = initializer ?? new Initializer();
|
||||||
|
}
|
||||||
|
|
||||||
|
public isInitializedMandatory(): boolean {
|
||||||
|
return this._initializer.isInitializedMultiple([
|
||||||
|
InitializationAspect.LANGUAGES,
|
||||||
|
InitializationAspect.SIDE_LOAD_ELEMENTS,
|
||||||
|
InitializationAspect.CAMERAS,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize the hard requirements for rendering anything.
|
||||||
|
* @returns `true` if card rendering can continue.
|
||||||
|
*/
|
||||||
|
public async initializeMandatory(): Promise<boolean> {
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
if (!hass) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await this._initializer.initializeMultipleIfNecessary({
|
||||||
|
// Caution: Ensure nothing in this set of initializers requires
|
||||||
|
// config or languages since they will not yet have been initialized.
|
||||||
|
[InitializationAspect.LANGUAGES]: async () => await loadLanguages(hass),
|
||||||
|
[InitializationAspect.SIDE_LOAD_ELEMENTS]: async () =>
|
||||||
|
await sideLoadHomeAssistantElements(),
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._api.getConfigManager().hasConfig()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await this._initializer.initializeIfNecessary(
|
||||||
|
InitializationAspect.CAMERAS,
|
||||||
|
async () => await this._api.getCameraManager().initializeCamerasFromConfig(),
|
||||||
|
))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this._api.getMessageManager().hasMessage()) {
|
||||||
|
// Set a view on initial load. However, if the query string contains a
|
||||||
|
// view related action, we don't set any view here and allow that content
|
||||||
|
// to be triggered by the firstUpdated() call that runs query string
|
||||||
|
// actions. To do otherwise may cause a race condition between the default
|
||||||
|
// view and the querystring view, see:
|
||||||
|
// https://github.com/dermotduffy/frigate-hass-card/issues/1200
|
||||||
|
const hasViewRelatedActions = this._api
|
||||||
|
.getQueryStringManager()
|
||||||
|
.hasViewRelatedActions();
|
||||||
|
if (hasViewRelatedActions) {
|
||||||
|
this._api.getQueryStringManager().executeViewRelated();
|
||||||
|
} else {
|
||||||
|
this._api.getViewManager().setViewDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize aspects of the card that can load in the 'background'.
|
||||||
|
* @returns `true` if card rendering can continue.
|
||||||
|
*/
|
||||||
|
public async initializeBackgroundIfNecessary(): Promise<boolean> {
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
const config = this._api.getConfigManager().getConfig();
|
||||||
|
|
||||||
|
if (!hass || !config) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
this._initializer.isInitializedMultiple([
|
||||||
|
...(config.menu.buttons.media_player.enabled
|
||||||
|
? [InitializationAspect.MEDIA_PLAYERS]
|
||||||
|
: []),
|
||||||
|
...(config.live.microphone.always_connected
|
||||||
|
? [InitializationAspect.MICROPHONE_CONNECT]
|
||||||
|
: []),
|
||||||
|
])
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!(await this._initializer.initializeMultipleIfNecessary({
|
||||||
|
...(config.menu.buttons.media_player.enabled && {
|
||||||
|
[InitializationAspect.MEDIA_PLAYERS]: async () =>
|
||||||
|
await this._api.getMediaPlayerManager().initialize(),
|
||||||
|
}),
|
||||||
|
...(config.live.microphone.always_connected && {
|
||||||
|
[InitializationAspect.MICROPHONE_CONNECT]: async () =>
|
||||||
|
await this._api.getMicrophoneManager().connect(),
|
||||||
|
}),
|
||||||
|
}))
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uninitialize(aspect: InitializationAspect) {
|
||||||
|
return this._initializer.uninitialize(aspect);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import throttle from 'lodash-es/throttle';
|
||||||
|
import { Timer } from '../utils/timer';
|
||||||
|
import { CardInteractionAPI } from './types';
|
||||||
|
|
||||||
|
export class InteractionManager {
|
||||||
|
protected _timer = new Timer();
|
||||||
|
protected _api: CardInteractionAPI;
|
||||||
|
|
||||||
|
constructor(api: CardInteractionAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The mouse handler may be called continually, throttle it to at most once
|
||||||
|
// per second for performance reasons.
|
||||||
|
public reportInteraction = throttle(() => {
|
||||||
|
this._reportInteraction();
|
||||||
|
}, 1 * 1000);
|
||||||
|
|
||||||
|
public hasInteraction(): boolean {
|
||||||
|
return this._timer.isRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the user interaction ('screensaver') timer to reset the view to
|
||||||
|
* default `view.timeout_seconds` after user interaction.
|
||||||
|
*/
|
||||||
|
protected _reportInteraction(): void {
|
||||||
|
this._timer.stop();
|
||||||
|
|
||||||
|
// Interactions reset the trigger state.
|
||||||
|
this._api.getTriggersManager().untrigger();
|
||||||
|
|
||||||
|
const timeoutSeconds = this._api.getConfigManager().getConfig()
|
||||||
|
?.view.timeout_seconds;
|
||||||
|
|
||||||
|
if (timeoutSeconds) {
|
||||||
|
this._timer.start(timeoutSeconds, () => {
|
||||||
|
if (this._isAutomatedUpdateAllowed()) {
|
||||||
|
this._api.getViewManager().setViewDefault();
|
||||||
|
this._api.getStyleManager().setLightOrDarkMode();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this._api.getStyleManager().setLightOrDarkMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _isAutomatedUpdateAllowed(): boolean {
|
||||||
|
return !this._api.getTriggersManager().isTriggered();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
import { MediaLoadedInfo } from '../types';
|
||||||
|
import { log } from '../utils/debug';
|
||||||
|
import { isValidMediaLoadedInfo } from '../utils/media-info';
|
||||||
|
import { CardMediaLoadedAPI } from './types';
|
||||||
|
|
||||||
|
export class MediaLoadedInfoManager {
|
||||||
|
protected _api: CardMediaLoadedAPI;
|
||||||
|
protected _current: MediaLoadedInfo | null = null;
|
||||||
|
protected _lastKnown: MediaLoadedInfo | null = null;
|
||||||
|
|
||||||
|
constructor(api: CardMediaLoadedAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public set(mediaInfo: MediaLoadedInfo): void {
|
||||||
|
if (!isValidMediaLoadedInfo(mediaInfo)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log(
|
||||||
|
this._api.getConfigManager().getCardWideConfig(),
|
||||||
|
`Frigate Card media load: `,
|
||||||
|
mediaInfo,
|
||||||
|
);
|
||||||
|
|
||||||
|
this._current = mediaInfo;
|
||||||
|
this._lastKnown = mediaInfo;
|
||||||
|
|
||||||
|
this._api.getConditionsManager().setState({ media_loaded: true });
|
||||||
|
|
||||||
|
// Fresh media information may change how the card is rendered.
|
||||||
|
this._api.getStyleManager().setExpandedMode();
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get(): MediaLoadedInfo | null {
|
||||||
|
return this._current;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getLastKnown(): MediaLoadedInfo | null {
|
||||||
|
return this._lastKnown;
|
||||||
|
}
|
||||||
|
|
||||||
|
public clear(): void {
|
||||||
|
this._current = null;
|
||||||
|
this._api.getConditionsManager().setState({ media_loaded: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
public has(): boolean {
|
||||||
|
return !!this._current;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA } from '../const';
|
||||||
|
import { ViewMedia } from '../view/media';
|
||||||
|
import { ViewMediaClassifier } from '../view/media-classifier';
|
||||||
|
import { errorToConsole } from '../utils/basic';
|
||||||
|
import { Entity } from '../utils/ha/entity-registry/types';
|
||||||
|
import { supportsFeature } from '../utils/ha/update';
|
||||||
|
import { CardMediaPlayerAPI } from './types';
|
||||||
|
|
||||||
|
export class MediaPlayerManager {
|
||||||
|
protected _mediaPlayers: string[] = [];
|
||||||
|
|
||||||
|
protected _api: CardMediaPlayerAPI;
|
||||||
|
|
||||||
|
constructor(api: CardMediaPlayerAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMediaPlayers(): string[] {
|
||||||
|
return this._mediaPlayers;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasMediaPlayers(): boolean {
|
||||||
|
return this._mediaPlayers.length > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initialize(): Promise<void> {
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
if (!hass) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValidMediaPlayer = (entityID: string): boolean => {
|
||||||
|
if (entityID.startsWith('media_player.')) {
|
||||||
|
const stateObj = hass.states[entityID];
|
||||||
|
if (
|
||||||
|
stateObj &&
|
||||||
|
stateObj.state !== 'unavailable' &&
|
||||||
|
supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA)
|
||||||
|
) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mediaPlayers = Object.keys(hass.states).filter(isValidMediaPlayer);
|
||||||
|
let mediaPlayerEntities: Map<string, Entity> | null = null;
|
||||||
|
try {
|
||||||
|
mediaPlayerEntities = await this._api
|
||||||
|
.getEntityRegistryManager()
|
||||||
|
.getEntities(hass, mediaPlayers);
|
||||||
|
} catch (e) {
|
||||||
|
// Failing to fetch media player information is not considered
|
||||||
|
// sufficiently serious to block card startup -- it is just logged and we
|
||||||
|
// move on.
|
||||||
|
errorToConsole(e as Error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out entities that are marked as hidden (this information is not
|
||||||
|
// available in the HA state, only in the registry).
|
||||||
|
this._mediaPlayers = mediaPlayers.filter((entityID) => {
|
||||||
|
// Specifically allow for media players that are not found in the entity registry:
|
||||||
|
// See: https://github.com/dermotduffy/frigate-hass-card/issues/1016
|
||||||
|
const entity = mediaPlayerEntities?.get(entityID);
|
||||||
|
return !entity || !entity.hidden_by;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop(mediaPlayer: string): Promise<void> {
|
||||||
|
await this._api
|
||||||
|
.getHASSManager()
|
||||||
|
.getHASS()
|
||||||
|
?.callService('media_player', 'media_stop', {
|
||||||
|
entity_id: mediaPlayer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async playLive(mediaPlayer: string, cameraID: string): Promise<void> {
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
const cameraConfig = this._api
|
||||||
|
.getCameraManager()
|
||||||
|
.getStore()
|
||||||
|
.getCameraConfig(cameraID);
|
||||||
|
const cameraEntity = cameraConfig?.camera_entity ?? null;
|
||||||
|
|
||||||
|
if (!hass || !cameraEntity) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title =
|
||||||
|
this._api.getCameraManager().getCameraMetadata(cameraID)?.title ?? null;
|
||||||
|
const thumbnail = hass.states[cameraEntity]?.attributes?.entity_picture ?? null;
|
||||||
|
|
||||||
|
await hass.callService('media_player', 'play_media', {
|
||||||
|
entity_id: mediaPlayer,
|
||||||
|
media_content_id: `media-source://camera/${cameraEntity}`,
|
||||||
|
media_content_type: 'application/vnd.apple.mpegurl',
|
||||||
|
extra: {
|
||||||
|
...(title && { title: title }),
|
||||||
|
...(thumbnail && { thumb: thumbnail }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async playMedia(mediaPlayer: string, media?: ViewMedia | null): Promise<void> {
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
|
if (!hass || !media) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const title = media.getTitle();
|
||||||
|
const thumbnail = media.getThumbnail();
|
||||||
|
|
||||||
|
await hass.callService('media_player', 'play_media', {
|
||||||
|
entity_id: mediaPlayer,
|
||||||
|
media_content_id: media.getContentID(),
|
||||||
|
media_content_type: ViewMediaClassifier.isVideo(media) ? 'video' : 'image',
|
||||||
|
extra: {
|
||||||
|
...(title && { title: title }),
|
||||||
|
...(thumbnail && { thumb: thumbnail }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import { FrigateCardError, MESSAGE_TYPE_PRIORITIES, Message } from '../types';
|
||||||
|
import { errorToConsole } from '../utils/basic';
|
||||||
|
import { CardMessageAPI } from './types';
|
||||||
|
|
||||||
|
export class MessageManager {
|
||||||
|
protected _message: Message | null = null;
|
||||||
|
protected _api: CardMessageAPI;
|
||||||
|
|
||||||
|
constructor(api: CardMessageAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMessage(): Message | null {
|
||||||
|
return this._message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasMessage(): boolean {
|
||||||
|
return !!this._message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasErrorMessage(): boolean {
|
||||||
|
return this._message?.type === 'error';
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset(): void {
|
||||||
|
const hadMessage = this.hasMessage();
|
||||||
|
this._message = null;
|
||||||
|
|
||||||
|
if (hadMessage) {
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setErrorIfHigherPriority(error: unknown): void {
|
||||||
|
// This object should accept unknown objects to be able to seamlessly
|
||||||
|
// process arguments to catch() which can only be unknown/any.
|
||||||
|
if (!(error instanceof Error)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
errorToConsole(error);
|
||||||
|
this.setMessageIfHigherPriority({
|
||||||
|
message: error.message,
|
||||||
|
type: 'error',
|
||||||
|
...(error instanceof FrigateCardError && { context: error.context }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public setMessageIfHigherPriority(message: Message): boolean {
|
||||||
|
const currentPriority = this._message
|
||||||
|
? MESSAGE_TYPE_PRIORITIES[this._message.type]
|
||||||
|
: 0;
|
||||||
|
const newPriority = MESSAGE_TYPE_PRIORITIES[message.type];
|
||||||
|
|
||||||
|
if (this._message && newPriority < currentPriority) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this._message = message;
|
||||||
|
|
||||||
|
// When a message is displayed it effectively unloads the media.
|
||||||
|
this._api.getMediaLoadedInfoManager().clear();
|
||||||
|
this._api.getCardElementManager().scrollReset();
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
import { errorToConsole } from './basic';
|
import { errorToConsole } from '../utils/basic';
|
||||||
import { Timer } from './timer';
|
import { Timer } from '../utils/timer';
|
||||||
|
import { CardMicrophoneAPI } from './types';
|
||||||
|
|
||||||
export class MicrophoneController {
|
export class MicrophoneManager {
|
||||||
|
protected _api: CardMicrophoneAPI;
|
||||||
protected _stream?: MediaStream | null;
|
protected _stream?: MediaStream | null;
|
||||||
protected _timer = new Timer();
|
protected _timer = new Timer();
|
||||||
|
|
||||||
@@ -10,10 +12,8 @@ export class MicrophoneController {
|
|||||||
// have the right mute status.
|
// have the right mute status.
|
||||||
protected _mute = true;
|
protected _mute = true;
|
||||||
|
|
||||||
protected _disconnectSeconds: number;
|
constructor(api: CardMicrophoneAPI) {
|
||||||
|
this._api = api;
|
||||||
constructor(disconnectSeconds?: number) {
|
|
||||||
this._disconnectSeconds = disconnectSeconds ?? 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async connect(): Promise<void> {
|
public async connect(): Promise<void> {
|
||||||
@@ -32,6 +32,8 @@ export class MicrophoneController {
|
|||||||
public async disconnect(): Promise<void> {
|
public async disconnect(): Promise<void> {
|
||||||
this._stream?.getTracks().forEach((track) => track.stop());
|
this._stream?.getTracks().forEach((track) => track.stop());
|
||||||
this._stream = undefined;
|
this._stream = undefined;
|
||||||
|
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
}
|
}
|
||||||
|
|
||||||
public getStream(): MediaStream | undefined {
|
public getStream(): MediaStream | undefined {
|
||||||
@@ -43,6 +45,8 @@ export class MicrophoneController {
|
|||||||
track.enabled = !this._mute;
|
track.enabled = !this._mute;
|
||||||
});
|
});
|
||||||
this._startTimer();
|
this._startTimer();
|
||||||
|
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
}
|
}
|
||||||
|
|
||||||
public mute(): void {
|
public mute(): void {
|
||||||
@@ -50,9 +54,24 @@ export class MicrophoneController {
|
|||||||
this._setMute();
|
this._setMute();
|
||||||
}
|
}
|
||||||
|
|
||||||
public unmute(): void {
|
public async unmute(): Promise<void> {
|
||||||
this._mute = false;
|
const unmute = (): void => {
|
||||||
this._setMute();
|
this._mute = false;
|
||||||
|
this._setMute();
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!this.isConnected() && !this.isForbidden()) {
|
||||||
|
// The connect() call is async and make take an arbitrary amount of
|
||||||
|
// time for the user to grant access to their microphone. With a
|
||||||
|
// momentary microphone button the mute call (on mouse release) may
|
||||||
|
// arrive before the connection is even granted, so we unmute first
|
||||||
|
// before the connection is made, so the mute call on release will not
|
||||||
|
// be 'overwritten' incorrectly.
|
||||||
|
unmute();
|
||||||
|
await this.connect();
|
||||||
|
} else if (this.isConnected()) {
|
||||||
|
unmute();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public isConnected(): boolean {
|
public isConnected(): boolean {
|
||||||
@@ -70,8 +89,17 @@ export class MicrophoneController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _startTimer(): void {
|
protected _startTimer(): void {
|
||||||
if (this._disconnectSeconds) {
|
const microphoneConfig = this._api.getConfigManager().getConfig()
|
||||||
this._timer.start(this._disconnectSeconds, () => {
|
?.live.microphone;
|
||||||
|
|
||||||
|
if (microphoneConfig?.always_connected) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const disconnectSeconds = microphoneConfig?.disconnect_seconds ?? 0;
|
||||||
|
|
||||||
|
if (disconnectSeconds) {
|
||||||
|
this._timer.start(disconnectSeconds, () => {
|
||||||
this.disconnect();
|
this.disconnect();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
import { FrigateCardCustomAction, FrigateCardViewAction } from '../config/types';
|
||||||
|
import { createFrigateCardCustomAction } from '../utils/action.js';
|
||||||
|
import { CardQueryStringAPI } from './types';
|
||||||
|
import { ViewManagerSetViewParameters } from './view-manager';
|
||||||
|
|
||||||
|
interface QueryStringViewIntent {
|
||||||
|
view?: ViewManagerSetViewParameters & {
|
||||||
|
default?: boolean;
|
||||||
|
};
|
||||||
|
other?: FrigateCardCustomAction[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export class QueryStringManager {
|
||||||
|
protected _api: CardQueryStringAPI;
|
||||||
|
|
||||||
|
constructor(api: CardQueryStringAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasViewRelatedActions(): boolean {
|
||||||
|
return !!this._calculateIntent().view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public executeNonViewRelated(): void {
|
||||||
|
this._executeNonViewRelated(this._calculateIntent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public executeViewRelated(): void {
|
||||||
|
this._executeViewRelated(this._calculateIntent());
|
||||||
|
}
|
||||||
|
|
||||||
|
public executeAll(): void {
|
||||||
|
const intent = this._calculateIntent();
|
||||||
|
this._executeViewRelated(intent);
|
||||||
|
this._executeNonViewRelated(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _executeViewRelated(intent: QueryStringViewIntent): void {
|
||||||
|
if (intent.view) {
|
||||||
|
if (intent.view.default) {
|
||||||
|
this._api.getViewManager().setViewDefault({
|
||||||
|
...(intent.view.cameraID && { cameraID: intent.view.cameraID }),
|
||||||
|
...(intent.view.substream && { substream: intent.view.substream }),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this._api.getViewManager().setViewByParameters({
|
||||||
|
...(intent.view.viewName && { viewName: intent.view.viewName }),
|
||||||
|
...(intent.view.cameraID && { cameraID: intent.view.cameraID }),
|
||||||
|
...(intent.view.substream && { substream: intent.view.substream }),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _executeNonViewRelated(intent: QueryStringViewIntent): void {
|
||||||
|
// Only execute non-view actions when the card has rendered at least once.
|
||||||
|
if (!this._api.getCardElementManager().hasUpdated()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
intent.other?.forEach((action) =>
|
||||||
|
this._api.getActionsManager().executeAction(action),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _calculateIntent(): QueryStringViewIntent {
|
||||||
|
const result: QueryStringViewIntent = {};
|
||||||
|
for (const action of this._getActions()) {
|
||||||
|
if (this._isViewAction(action)) {
|
||||||
|
(result.view ??= {}).viewName = action.frigate_card_action;
|
||||||
|
(result.view ??= {}).default = undefined;
|
||||||
|
} else if (action.frigate_card_action === 'default') {
|
||||||
|
(result.view ??= {}).default = true;
|
||||||
|
(result.view ??= {}).viewName = undefined;
|
||||||
|
} else if (action.frigate_card_action === 'camera_select') {
|
||||||
|
(result.view ??= {}).cameraID = action.camera;
|
||||||
|
} else if (action.frigate_card_action === 'live_substream_select') {
|
||||||
|
(result.view ??= {}).substream = action.camera;
|
||||||
|
} else {
|
||||||
|
(result.other ??= []).push(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getActions(): FrigateCardCustomAction[] {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const actions: FrigateCardCustomAction[] = [];
|
||||||
|
const actionRE = new RegExp(
|
||||||
|
/^frigate-card-action([.:](?<cardID>\w+))?[.:](?<action>\w+)/,
|
||||||
|
);
|
||||||
|
for (const [key, value] of params.entries()) {
|
||||||
|
const match = key.match(actionRE);
|
||||||
|
if (!match || !match.groups) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const cardID: string | undefined = match.groups['cardID'];
|
||||||
|
const action = match.groups['action'];
|
||||||
|
|
||||||
|
let customAction: FrigateCardCustomAction | null = null;
|
||||||
|
switch (action) {
|
||||||
|
case 'camera_select':
|
||||||
|
case 'live_substream_select':
|
||||||
|
if (value) {
|
||||||
|
customAction = createFrigateCardCustomAction(action, {
|
||||||
|
camera: value,
|
||||||
|
cardID: cardID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'camera_ui':
|
||||||
|
case 'clip':
|
||||||
|
case 'clips':
|
||||||
|
case 'default':
|
||||||
|
case 'diagnostics':
|
||||||
|
case 'download':
|
||||||
|
case 'expand':
|
||||||
|
case 'image':
|
||||||
|
case 'live':
|
||||||
|
case 'menu_toggle':
|
||||||
|
case 'recording':
|
||||||
|
case 'recordings':
|
||||||
|
case 'snapshot':
|
||||||
|
case 'snapshots':
|
||||||
|
case 'timeline':
|
||||||
|
customAction = createFrigateCardCustomAction(action, {
|
||||||
|
cardID: cardID,
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
console.warn(
|
||||||
|
`Frigate card received unknown card action in query string: ${action}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (customAction) {
|
||||||
|
actions.push(customAction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return actions;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _isViewAction = (
|
||||||
|
action: FrigateCardCustomAction,
|
||||||
|
): action is FrigateCardViewAction => {
|
||||||
|
switch (action.frigate_card_action) {
|
||||||
|
case 'clip':
|
||||||
|
case 'clips':
|
||||||
|
case 'diagnostics':
|
||||||
|
case 'image':
|
||||||
|
case 'live':
|
||||||
|
case 'recording':
|
||||||
|
case 'recordings':
|
||||||
|
case 'snapshot':
|
||||||
|
case 'snapshots':
|
||||||
|
case 'timeline':
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { FrigateCardConfig } from '../config/types';
|
||||||
|
import { setPerformanceCSSStyles } from '../performance';
|
||||||
|
import { View } from '../view/view';
|
||||||
|
import { setOrRemoveAttribute } from '../utils/basic';
|
||||||
|
import { CardStyleAPI } from './types';
|
||||||
|
|
||||||
|
export class StyleManager {
|
||||||
|
protected _api: CardStyleAPI;
|
||||||
|
|
||||||
|
constructor(api: CardStyleAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setLightOrDarkMode(): void {
|
||||||
|
const config = this._api.getConfigManager().getConfig();
|
||||||
|
const isDarkMode =
|
||||||
|
config?.view.dark_mode === 'on' ||
|
||||||
|
(config?.view.dark_mode === 'auto' &&
|
||||||
|
(!this._api.getInteractionManager().hasInteraction() ||
|
||||||
|
!!this._api.getHASSManager().getHASS()?.themes.darkMode));
|
||||||
|
|
||||||
|
setOrRemoveAttribute(
|
||||||
|
this._api.getCardElementManager().getElement(),
|
||||||
|
isDarkMode,
|
||||||
|
'dark',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setExpandedMode(): void {
|
||||||
|
const card = this._api.getCardElementManager().getElement();
|
||||||
|
const view = this._api.getViewManager().getView();
|
||||||
|
|
||||||
|
// When a new media loads, set the aspect ratio for when the card is
|
||||||
|
// expanded/popped-up. This is based exclusively on last media content,
|
||||||
|
// as dimension configuration does not apply in fullscreen or expanded mode.
|
||||||
|
const lastKnown = this._api.getMediaLoadedInfoManager().getLastKnown();
|
||||||
|
card.style.setProperty(
|
||||||
|
'--frigate-card-expand-aspect-ratio',
|
||||||
|
view?.isAnyMediaView() && lastKnown
|
||||||
|
? `${lastKnown.width} / ${lastKnown.height}`
|
||||||
|
: 'unset',
|
||||||
|
);
|
||||||
|
// Non-media may have no intrinsic dimensions (or multiple media items in a
|
||||||
|
// grid) and so we need to explicit request the dialog to use all available
|
||||||
|
// space.
|
||||||
|
const isGrid = view?.isGrid();
|
||||||
|
card.style.setProperty(
|
||||||
|
'--frigate-card-expand-width',
|
||||||
|
!isGrid && view?.isAnyMediaView()
|
||||||
|
? 'none'
|
||||||
|
: 'var(--frigate-card-expand-max-width)',
|
||||||
|
);
|
||||||
|
card.style.setProperty(
|
||||||
|
'--frigate-card-expand-height',
|
||||||
|
!isGrid && view?.isAnyMediaView()
|
||||||
|
? 'none'
|
||||||
|
: 'var(--frigate-card-expand-max-height)',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setMinMaxHeight(): void {
|
||||||
|
const config = this._api.getConfigManager().getConfig();
|
||||||
|
if (config) {
|
||||||
|
const card = this._api.getCardElementManager().getElement();
|
||||||
|
card.style.setProperty('--frigate-card-min-height', config.dimensions.min_height);
|
||||||
|
card.style.setProperty('--frigate-card-max-height', config.dimensions.max_height);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setPerformance(): void {
|
||||||
|
setPerformanceCSSStyles(
|
||||||
|
this._api.getCardElementManager().getElement(),
|
||||||
|
this._api.getConfigManager().getCardWideConfig()?.performance,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _isAspectRatioEnforced(
|
||||||
|
config: FrigateCardConfig,
|
||||||
|
view?: View | null,
|
||||||
|
): boolean {
|
||||||
|
const aspectRatioMode = config.dimensions.aspect_ratio_mode;
|
||||||
|
|
||||||
|
// Do not artifically constrain aspect ratio if:
|
||||||
|
// - It's fullscreen.
|
||||||
|
// - It's in expanded mode.
|
||||||
|
// - Aspect ratio enforcement is disabled.
|
||||||
|
// - Aspect ratio enforcement is dynamic and it's a media view (i.e. not the
|
||||||
|
// gallery) or diagnostics / timeline.
|
||||||
|
return !(
|
||||||
|
this._api.getFullscreenManager().isInFullscreen() ||
|
||||||
|
this._api.getExpandManager().isExpanded() ||
|
||||||
|
aspectRatioMode === 'unconstrained' ||
|
||||||
|
(aspectRatioMode === 'dynamic' &&
|
||||||
|
(!view ||
|
||||||
|
view?.isAnyMediaView() ||
|
||||||
|
view?.is('timeline') ||
|
||||||
|
view?.is('diagnostics')))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the aspect ratio padding required to enforce the aspect ratio (if it is
|
||||||
|
* required).
|
||||||
|
* @returns A padding percentage.
|
||||||
|
*/
|
||||||
|
public getAspectRatioStyle(): string {
|
||||||
|
const config = this._api.getConfigManager().getConfig();
|
||||||
|
const view = this._api.getViewManager().getView();
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
if (!this._isAspectRatioEnforced(config, view)) {
|
||||||
|
return 'auto';
|
||||||
|
}
|
||||||
|
|
||||||
|
const aspectRatioMode = config.dimensions.aspect_ratio_mode;
|
||||||
|
|
||||||
|
const lastKnown = this._api.getMediaLoadedInfoManager().getLastKnown();
|
||||||
|
if (lastKnown && aspectRatioMode === 'dynamic') {
|
||||||
|
return `${lastKnown.width} / ${lastKnown.height}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${config.dimensions.aspect_ratio[0]} / ${config.dimensions.aspect_ratio[1]}`;
|
||||||
|
}
|
||||||
|
return '16 / 9';
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import orderBy from 'lodash-es/orderBy';
|
||||||
|
import { getHassDifferences, isTriggeredState } from '../utils/ha';
|
||||||
|
import { Timer } from '../utils/timer';
|
||||||
|
import { CardTriggersAPI } from './types';
|
||||||
|
|
||||||
|
export class TriggersManager {
|
||||||
|
protected _api: CardTriggersAPI;
|
||||||
|
|
||||||
|
protected _triggers: Map<string, Date> = new Map();
|
||||||
|
protected _untriggerTimer = new Timer();
|
||||||
|
|
||||||
|
constructor(api: CardTriggersAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public isTriggered(): boolean {
|
||||||
|
return !!this._triggers.size || this._untriggerTimer.isRunning();
|
||||||
|
}
|
||||||
|
|
||||||
|
public updateTriggeredCameras(oldHass?: HomeAssistant | null): boolean {
|
||||||
|
if (!this._shouldTrackTriggers()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
let triggerChanges = false;
|
||||||
|
|
||||||
|
const cameras = this._api.getCameraManager().getStore().getVisibleCameras();
|
||||||
|
for (const [cameraID, config] of cameras?.entries()) {
|
||||||
|
const triggerEntities = config.triggers.entities;
|
||||||
|
const diffs = getHassDifferences(hass, oldHass, triggerEntities, {
|
||||||
|
stateOnly: true,
|
||||||
|
});
|
||||||
|
const shouldTrigger = diffs.some((diff) => isTriggeredState(diff.newState));
|
||||||
|
const shouldUntrigger = triggerEntities.every(
|
||||||
|
(entity) => !isTriggeredState(hass?.states[entity]),
|
||||||
|
);
|
||||||
|
if (shouldTrigger) {
|
||||||
|
this._triggers.set(cameraID, now);
|
||||||
|
triggerChanges = true;
|
||||||
|
} else if (shouldUntrigger && this._triggers.has(cameraID)) {
|
||||||
|
this._triggers.delete(cameraID);
|
||||||
|
triggerChanges = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triggerChanges) {
|
||||||
|
const targetCameraID = this._getMostRecentTrigger();
|
||||||
|
if (targetCameraID) {
|
||||||
|
this._triggerAction(targetCameraID);
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
this._startUntriggerTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public untrigger(): void {
|
||||||
|
const wasTriggered = this.isTriggered();
|
||||||
|
this._triggers.clear();
|
||||||
|
this._untriggerTimer.stop();
|
||||||
|
|
||||||
|
if (wasTriggered) {
|
||||||
|
this._untriggerAction();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _triggerAction(cameraID: string): void {
|
||||||
|
const view = this._api.getViewManager().getView();
|
||||||
|
if (
|
||||||
|
this._isAutomatedViewUpdateAllowed() &&
|
||||||
|
(view?.camera !== cameraID || !view?.is('live'))
|
||||||
|
) {
|
||||||
|
this._api.getViewManager().setViewByParameters({
|
||||||
|
viewName: 'live',
|
||||||
|
cameraID: cameraID,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _untriggerAction(): void {
|
||||||
|
if (
|
||||||
|
!this.isTriggered() &&
|
||||||
|
this._isAutomatedViewUpdateAllowed() &&
|
||||||
|
this._api.getConfigManager().getConfig()?.view.scan.untrigger_reset
|
||||||
|
) {
|
||||||
|
this._api.getViewManager().setViewDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _isAutomatedViewUpdateAllowed(): boolean {
|
||||||
|
return (
|
||||||
|
this._api.getConfigManager().getConfig()?.view.update_force ||
|
||||||
|
!this._api.getInteractionManager().hasInteraction()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _shouldTrackTriggers(): boolean {
|
||||||
|
return !!this._api.getConfigManager().getConfig()?.view.scan.enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _startUntriggerTimer(): void {
|
||||||
|
this._untriggerTimer.start(
|
||||||
|
/* istanbul ignore next: the case of config being null here cannot be
|
||||||
|
reached, as there's no way to have the untrigger call happen without
|
||||||
|
a config. -- @preserve */
|
||||||
|
this._api.getConfigManager().getConfig()?.view.scan.untrigger_seconds ?? 0,
|
||||||
|
() => {
|
||||||
|
this._untriggerAction();
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getMostRecentTrigger(): string | null {
|
||||||
|
const sorted = orderBy(
|
||||||
|
[...this._triggers.entries()],
|
||||||
|
(entry) => entry[1].getTime(),
|
||||||
|
'desc',
|
||||||
|
);
|
||||||
|
return sorted.length ? sorted[0][0] : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,214 @@
|
|||||||
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
|
import { ConditionsManager } from './conditions-manager';
|
||||||
|
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||||
|
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||||
|
import { ActionsManager } from './actions-manager';
|
||||||
|
import { AutoUpdateManager } from './auto-update-manager';
|
||||||
|
import { AutomationsManager } from './automations-manager';
|
||||||
|
import { CameraURLManager } from './camera-url-manager';
|
||||||
|
import { CardElementManager } from './card-element-manager';
|
||||||
|
import { ConfigManager } from './config-manager';
|
||||||
|
import { DownloadManager } from './download-manager';
|
||||||
|
import { ExpandManager } from './expand-manager';
|
||||||
|
import { FullscreenManager } from './fullscreen-manager';
|
||||||
|
import { HASSManager } from './hass-manager';
|
||||||
|
import { InitializationManager } from './initialization-manager';
|
||||||
|
import { InteractionManager } from './interaction-manager';
|
||||||
|
import { MediaLoadedInfoManager } from './media-info-manager';
|
||||||
|
import { MediaPlayerManager } from './media-player-manager';
|
||||||
|
import { MessageManager } from './message-manager';
|
||||||
|
import { MicrophoneManager } from './microphone-manager';
|
||||||
|
import { StyleManager } from './style-manager';
|
||||||
|
import { TriggersManager } from './triggers-manager';
|
||||||
|
import { ViewManager } from './view-manager';
|
||||||
|
import { QueryStringManager } from './query-string-manager';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This defines a series of limited APIs that various manager helpers use to
|
||||||
|
* control the card. Explicitly specifying them helps make coupling intentional
|
||||||
|
* and avoids cyclic importing.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export interface CardActionsManagerAPI {
|
||||||
|
getCameraManager(): CameraManager;
|
||||||
|
getCameraURLManager(): CameraURLManager;
|
||||||
|
getCardElementManager(): CardElementManager;
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
|
getDownloadManager(): DownloadManager;
|
||||||
|
getExpandManager(): ExpandManager;
|
||||||
|
getFullscreenManager(): FullscreenManager;
|
||||||
|
getHASSManager(): HASSManager;
|
||||||
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
|
getMediaPlayerManager(): MediaPlayerManager;
|
||||||
|
getMessageManager(): MessageManager;
|
||||||
|
getViewManager(): ViewManager;
|
||||||
|
getMicrophoneManager(): MicrophoneManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardAutomationsAPI {
|
||||||
|
getCardElementManager(): CardElementManager;
|
||||||
|
getConditionsManager(): ConditionsManager;
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
|
getHASSManager(): HASSManager;
|
||||||
|
getMessageManager(): MessageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardAutoRefreshAPI {
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
|
getViewManager(): ViewManager;
|
||||||
|
getTriggersManager(): TriggersManager;
|
||||||
|
getInteractionManager(): InteractionManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardCameraAPI {
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
|
getEntityRegistryManager(): EntityRegistryManager;
|
||||||
|
getResolvedMediaCache(): ResolvedMediaCache;
|
||||||
|
getHASSManager(): HASSManager;
|
||||||
|
getMessageManager(): MessageManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardCameraURLAPI {
|
||||||
|
getCameraManager(): CameraManager;
|
||||||
|
getViewManager(): ViewManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardConditionAPI {
|
||||||
|
getAutomationsManager(): AutomationsManager;
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardConfigAPI {
|
||||||
|
getAutomationsManager(): AutomationsManager;
|
||||||
|
getCardElementManager(): CardElementManager;
|
||||||
|
getConditionsManager(): ConditionsManager;
|
||||||
|
getInitializationManager(): InitializationManager;
|
||||||
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
|
getMessageManager(): MessageManager;
|
||||||
|
getStyleManager(): StyleManager;
|
||||||
|
getViewManager(): ViewManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardDownloadAPI {
|
||||||
|
getCameraManager(): CameraManager;
|
||||||
|
getHASSManager(): HASSManager;
|
||||||
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
|
getMessageManager(): MessageManager;
|
||||||
|
getViewManager(): ViewManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardElementAPI {
|
||||||
|
getActionsManager(): ActionsManager;
|
||||||
|
getFullscreenManager(): FullscreenManager;
|
||||||
|
getInteractionManager(): InteractionManager;
|
||||||
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
|
getQueryStringManager(): QueryStringManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardExpandAPI {
|
||||||
|
getFullscreenManager(): FullscreenManager;
|
||||||
|
getConditionsManager(): ConditionsManager;
|
||||||
|
getCardElementManager(): CardElementManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardFullscreenAPI {
|
||||||
|
getCardElementManager(): CardElementManager;
|
||||||
|
getExpandManager(): ExpandManager;
|
||||||
|
getConditionsManager(): ConditionsManager;
|
||||||
|
getMediaPlayerManager(): MediaPlayerManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardHASSAPI {
|
||||||
|
getCameraManager(): CameraManager;
|
||||||
|
getCardElementManager(): CardElementManager;
|
||||||
|
getConditionsManager(): ConditionsManager;
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
|
getInteractionManager(): InteractionManager;
|
||||||
|
getMediaPlayerManager(): MediaPlayerManager;
|
||||||
|
getMessageManager(): MessageManager;
|
||||||
|
getStyleManager(): StyleManager;
|
||||||
|
getTriggersManager(): TriggersManager;
|
||||||
|
getViewManager(): ViewManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardInitializerAPI {
|
||||||
|
getCameraManager(): CameraManager;
|
||||||
|
getCardElementManager(): CardElementManager;
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
|
getEntityRegistryManager(): EntityRegistryManager;
|
||||||
|
getHASSManager(): HASSManager;
|
||||||
|
getMediaPlayerManager(): MediaPlayerManager;
|
||||||
|
getMessageManager(): MessageManager;
|
||||||
|
getMicrophoneManager(): MicrophoneManager;
|
||||||
|
getQueryStringManager(): QueryStringManager;
|
||||||
|
getResolvedMediaCache(): ResolvedMediaCache;
|
||||||
|
getViewManager(): ViewManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardInteractionAPI {
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
|
getStyleManager(): StyleManager;
|
||||||
|
getTriggersManager(): TriggersManager;
|
||||||
|
getViewManager(): ViewManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardMediaLoadedAPI {
|
||||||
|
getConditionsManager(): ConditionsManager;
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
|
getCardElementManager(): CardElementManager;
|
||||||
|
getStyleManager(): StyleManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardMediaPlayerAPI {
|
||||||
|
getHASSManager(): HASSManager;
|
||||||
|
getCameraManager(): CameraManager;
|
||||||
|
getEntityRegistryManager(): EntityRegistryManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardMessageAPI {
|
||||||
|
getConditionsManager(): ConditionsManager;
|
||||||
|
getCardElementManager(): CardElementManager;
|
||||||
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardMicrophoneAPI {
|
||||||
|
getCardElementManager(): CardElementManager;
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardQueryStringAPI {
|
||||||
|
getCardElementManager(): CardElementManager;
|
||||||
|
getViewManager(): ViewManager;
|
||||||
|
getActionsManager(): ActionsManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardStyleAPI {
|
||||||
|
getCardElementManager(): CardElementManager;
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
|
getExpandManager(): ExpandManager;
|
||||||
|
getFullscreenManager(): FullscreenManager;
|
||||||
|
getHASSManager(): HASSManager;
|
||||||
|
getInteractionManager(): InteractionManager;
|
||||||
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
|
getViewManager(): ViewManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardTriggersAPI {
|
||||||
|
getCameraManager(): CameraManager;
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
|
getHASSManager(): HASSManager;
|
||||||
|
getInteractionManager(): InteractionManager;
|
||||||
|
getViewManager(): ViewManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CardViewAPI {
|
||||||
|
getAutoUpdateManager(): AutoUpdateManager;
|
||||||
|
getCameraManager(): CameraManager;
|
||||||
|
getConfigManager(): ConfigManager;
|
||||||
|
getHASSManager(): HASSManager;
|
||||||
|
getMediaLoadedInfoManager(): MediaLoadedInfoManager;
|
||||||
|
getMessageManager(): MessageManager;
|
||||||
|
getStyleManager(): StyleManager;
|
||||||
|
getConditionsManager(): ConditionsManager;
|
||||||
|
getCardElementManager(): CardElementManager;
|
||||||
|
}
|
||||||
@@ -0,0 +1,286 @@
|
|||||||
|
import { ViewContext } from 'view';
|
||||||
|
import { FrigateCardConfig, FrigateCardView, ViewDisplayMode } from '../config/types';
|
||||||
|
import { View } from '../view/view';
|
||||||
|
import { getAllDependentCameras } from '../utils/camera';
|
||||||
|
import { log } from '../utils/debug';
|
||||||
|
import { executeMediaQueryForView } from '../utils/media-to-view';
|
||||||
|
import { CardViewAPI } from './types';
|
||||||
|
|
||||||
|
interface ViewManagerSetViewDefaultParameters {
|
||||||
|
cameraID?: string;
|
||||||
|
substream?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ViewManagerSetViewParameters
|
||||||
|
extends ViewManagerSetViewDefaultParameters {
|
||||||
|
viewName?: FrigateCardView;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ViewManager {
|
||||||
|
protected _view: View | null = null;
|
||||||
|
protected _api: CardViewAPI;
|
||||||
|
|
||||||
|
constructor(api: CardViewAPI) {
|
||||||
|
this._api = api;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getView(): View | null {
|
||||||
|
return this._view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public setView(view: View): void {
|
||||||
|
this._setView(view);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setViewDefault(params?: ViewManagerSetViewDefaultParameters): void {
|
||||||
|
const config = this._api.getConfigManager().getConfig();
|
||||||
|
if (config) {
|
||||||
|
let forceCameraID: string | null = params?.cameraID ?? null;
|
||||||
|
if (!forceCameraID && this._view?.camera && config.view.update_cycle_camera) {
|
||||||
|
const cameraIDs = [
|
||||||
|
...this._api.getCameraManager().getStore().getVisibleCameraIDs(),
|
||||||
|
];
|
||||||
|
const currentIndex = cameraIDs.indexOf(this._view.camera);
|
||||||
|
const targetIndex = currentIndex + 1 >= cameraIDs.length ? 0 : currentIndex + 1;
|
||||||
|
forceCameraID = cameraIDs[targetIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
this.setViewByParameters({
|
||||||
|
...params,
|
||||||
|
viewName: config.view.default,
|
||||||
|
...(forceCameraID && { cameraID: forceCameraID }),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restart the refresh timer, so the default view is refreshed at a fixed
|
||||||
|
// interval from now (if so configured).
|
||||||
|
this._api.getAutoUpdateManager().startDefaultViewTimer();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setViewByParameters(params: ViewManagerSetViewParameters): void {
|
||||||
|
const config = this._api.getConfigManager().getConfig();
|
||||||
|
|
||||||
|
if (config) {
|
||||||
|
let cameraID: string | null = null;
|
||||||
|
|
||||||
|
const cameras = this._api.getCameraManager().getStore().getVisibleCameraIDs();
|
||||||
|
if (cameras.size) {
|
||||||
|
if (params?.cameraID && cameras.has(params.cameraID)) {
|
||||||
|
cameraID = params.cameraID;
|
||||||
|
} else {
|
||||||
|
// Reset to the default camera.
|
||||||
|
cameraID = cameras.keys().next().value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const viewName = params?.viewName ?? this._view?.view ?? config.view.default;
|
||||||
|
if (cameraID && viewName && this.isViewSupportedByCamera(cameraID, viewName)) {
|
||||||
|
const displayMode =
|
||||||
|
this._view?.displayMode ??
|
||||||
|
this._getDefaultDisplayModeForView(viewName, config);
|
||||||
|
let view: View = new View({
|
||||||
|
view: viewName,
|
||||||
|
camera: cameraID,
|
||||||
|
displayMode: displayMode,
|
||||||
|
});
|
||||||
|
if (params.substream) {
|
||||||
|
view = this._createViewWithSelectedSubstream(view, params.substream);
|
||||||
|
}
|
||||||
|
this._setView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setViewWithNewContext(context: ViewContext): void {
|
||||||
|
if (this._view) {
|
||||||
|
return this._setView(this._view?.clone().mergeInContext(context));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public reset(): void {
|
||||||
|
this._view = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async setViewWithNewDisplayMode(displayMode: ViewDisplayMode): Promise<void> {
|
||||||
|
const hass = this._api.getHASSManager().getHASS();
|
||||||
|
|
||||||
|
if (this._view && hass) {
|
||||||
|
const view = this._view.evolve({
|
||||||
|
displayMode: displayMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const cameraCount = this._api
|
||||||
|
.getCameraManager()
|
||||||
|
.getStore()
|
||||||
|
.getVisibleCameraCount();
|
||||||
|
const queryCameraCount = view.query?.getQueryCameraIDs()?.size ?? 0;
|
||||||
|
const generateNewQuery =
|
||||||
|
view?.query &&
|
||||||
|
queryCameraCount &&
|
||||||
|
((view.isGrid() && queryCameraCount < cameraCount) ||
|
||||||
|
(!view.isGrid() && queryCameraCount > 1));
|
||||||
|
|
||||||
|
if (generateNewQuery && view && view.query) {
|
||||||
|
// If the user requests a grid but the current query does not have a
|
||||||
|
// query for more than one camera, reset the query results, change the
|
||||||
|
// existing query to refer to all cameras and execute it to fetch new
|
||||||
|
// results.
|
||||||
|
let viewWithNewQuery: View | null = null;
|
||||||
|
try {
|
||||||
|
viewWithNewQuery = await executeMediaQueryForView(
|
||||||
|
this._api.getCameraManager(),
|
||||||
|
view,
|
||||||
|
view.query
|
||||||
|
.clone()
|
||||||
|
.setQueryCameraIDs(
|
||||||
|
view.isGrid()
|
||||||
|
? this._api.getCameraManager().getStore().getVisibleCameraIDs()
|
||||||
|
: view.camera,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
this._api.getMessageManager().setErrorIfHigherPriority(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (viewWithNewQuery) {
|
||||||
|
return this._setView(viewWithNewQuery);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return this._setView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public setViewWithSubstream(substream?: string): void {
|
||||||
|
if (!this._view) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._setView(
|
||||||
|
substream
|
||||||
|
? this._createViewWithSelectedSubstream(this._view, substream)
|
||||||
|
: this._createViewWithNextStream(this._view),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public setViewWithoutSubstream(): void {
|
||||||
|
const view = this._createViewWithoutSubstream();
|
||||||
|
if (view) {
|
||||||
|
return this._setView(view);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isViewSupportedByCamera(cameraID: string, view: FrigateCardView): boolean {
|
||||||
|
const capabilities = this._api.getCameraManager().getCameraCapabilities(cameraID);
|
||||||
|
switch (view) {
|
||||||
|
case 'live':
|
||||||
|
case 'image':
|
||||||
|
case 'diagnostics':
|
||||||
|
return true;
|
||||||
|
case 'clip':
|
||||||
|
case 'clips':
|
||||||
|
return !!capabilities?.supportsClips;
|
||||||
|
case 'snapshot':
|
||||||
|
case 'snapshots':
|
||||||
|
return !!capabilities?.supportsSnapshots;
|
||||||
|
case 'recording':
|
||||||
|
case 'recordings':
|
||||||
|
return !!capabilities?.supportsRecordings;
|
||||||
|
case 'timeline':
|
||||||
|
return !!capabilities?.supportsTimeline;
|
||||||
|
case 'media':
|
||||||
|
return (
|
||||||
|
!!capabilities?.supportsClips ||
|
||||||
|
!!capabilities?.supportsSnapshots ||
|
||||||
|
!!capabilities?.supportsRecordings
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _getDefaultDisplayModeForView(
|
||||||
|
viewName: FrigateCardView,
|
||||||
|
config?: FrigateCardConfig,
|
||||||
|
): ViewDisplayMode {
|
||||||
|
let mode: ViewDisplayMode | null = null;
|
||||||
|
switch (viewName) {
|
||||||
|
case 'media':
|
||||||
|
case 'clip':
|
||||||
|
case 'recording':
|
||||||
|
case 'snapshot':
|
||||||
|
mode = config?.media_viewer.display?.mode ?? null;
|
||||||
|
break;
|
||||||
|
case 'live':
|
||||||
|
mode = config?.live.display?.mode ?? null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return mode ?? 'single';
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _setView(view: View): void {
|
||||||
|
const oldView = this._view;
|
||||||
|
View.adoptFromViewIfAppropriate(view, oldView);
|
||||||
|
|
||||||
|
log(
|
||||||
|
this._api.getConfigManager().getCardWideConfig(),
|
||||||
|
`Frigate Card view change: `,
|
||||||
|
view.view,
|
||||||
|
);
|
||||||
|
this._view = view;
|
||||||
|
|
||||||
|
if (View.isMajorMediaChange(oldView, view)) {
|
||||||
|
this._api.getMediaLoadedInfoManager().clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldView?.view !== view.view) {
|
||||||
|
this._api.getCardElementManager().scrollReset();
|
||||||
|
}
|
||||||
|
|
||||||
|
this._api.getMessageManager().reset();
|
||||||
|
this._api.getStyleManager().setExpandedMode();
|
||||||
|
|
||||||
|
this._api.getConditionsManager()?.setState({
|
||||||
|
view: view.view,
|
||||||
|
camera: view.camera,
|
||||||
|
displayMode: view.displayMode ?? undefined,
|
||||||
|
});
|
||||||
|
this._api.getCardElementManager().update();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _createViewWithSelectedSubstream(baseView: View, substreamID: string): View {
|
||||||
|
const overrides: Map<string, string> =
|
||||||
|
baseView?.context?.live?.overrides ?? new Map();
|
||||||
|
overrides.set(baseView.camera, substreamID);
|
||||||
|
return baseView.clone().mergeInContext({
|
||||||
|
live: { overrides: overrides },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _createViewWithNextStream(baseView: View): View {
|
||||||
|
const dependencies = [
|
||||||
|
...getAllDependentCameras(this._api.getCameraManager(), baseView.camera),
|
||||||
|
];
|
||||||
|
if (dependencies.length <= 1) {
|
||||||
|
return baseView.clone();
|
||||||
|
}
|
||||||
|
|
||||||
|
const view = baseView.clone();
|
||||||
|
const overrides: Map<string, string> = view.context?.live?.overrides ?? new Map();
|
||||||
|
const currentOverride = overrides.get(view.camera) ?? view.camera;
|
||||||
|
const currentIndex = dependencies.indexOf(currentOverride);
|
||||||
|
const newIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % dependencies.length;
|
||||||
|
overrides.set(view.camera, dependencies[newIndex]);
|
||||||
|
view.mergeInContext({ live: { overrides: overrides } });
|
||||||
|
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected _createViewWithoutSubstream(): View | null {
|
||||||
|
if (!this._view) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const view = this._view.clone();
|
||||||
|
const overrides: Map<string, string> | undefined = view.context?.live?.overrides;
|
||||||
|
if (overrides && overrides.has(view.camera)) {
|
||||||
|
view.context?.live?.overrides?.delete(view.camera);
|
||||||
|
}
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
}
|
||||||
+127
-1677
File diff suppressed because it is too large
Load Diff
@@ -9,8 +9,8 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
import { Ref, createRef, ref } from 'lit/directives/ref.js';
|
||||||
|
import { TransitionEffect } from '../config/types';
|
||||||
import carouselStyle from '../scss/carousel.scss';
|
import carouselStyle from '../scss/carousel.scss';
|
||||||
import { TransitionEffect } from '../types';
|
|
||||||
import {
|
import {
|
||||||
CarouselController,
|
CarouselController,
|
||||||
CarouselDirection,
|
CarouselDirection,
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { CSSResultGroup, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
|
import { RawFrigateCardConfig } from '../config/types';
|
||||||
|
import { localize } from '../localize/localize';
|
||||||
|
import basicBlockStyle from '../scss/basic-block.scss';
|
||||||
|
import { Diagnostics, getDiagnostics } from '../utils/diagnostics';
|
||||||
|
import { renderMessage } from './message';
|
||||||
|
|
||||||
|
@customElement('frigate-card-diagnostics')
|
||||||
|
export class FrigateCardDiagnostics extends LitElement {
|
||||||
|
@property({ attribute: false })
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false })
|
||||||
|
public rawConfig?: RawFrigateCardConfig;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
protected _diagnostics: Diagnostics | null = null;
|
||||||
|
|
||||||
|
protected async _fetchDiagnostics(): Promise<void> {
|
||||||
|
this._diagnostics = await getDiagnostics(this.hass, this.rawConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (!this._diagnostics) {
|
||||||
|
this._fetchDiagnostics().then(() => this.requestUpdate());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
return renderMessage({
|
||||||
|
message: localize('error.diagnostics'),
|
||||||
|
type: 'diagnostics',
|
||||||
|
icon: 'mdi:information',
|
||||||
|
context: this._diagnostics,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return unsafeCSS(basicBlockStyle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
'frigate-card-diagnostics': FrigateCardDiagnostics;
|
||||||
|
}
|
||||||
|
}
|
||||||
+20
-21
@@ -1,39 +1,42 @@
|
|||||||
import { HASSDomEvent, HomeAssistant } from 'custom-card-helpers';
|
import { HASSDomEvent, HomeAssistant } from 'custom-card-helpers';
|
||||||
import {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
|
html,
|
||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import elementsStyle from '../scss/elements.scss';
|
import { actionHandler } from '../action-handler-directive.js';
|
||||||
import ptzStyle from '../scss/elements-ptz.scss';
|
|
||||||
import {
|
import {
|
||||||
Actions,
|
Actions,
|
||||||
ActionsConfig,
|
ActionsConfig,
|
||||||
FrigateCardError,
|
|
||||||
FrigateCardPTZConfig,
|
FrigateCardPTZConfig,
|
||||||
FrigateConditional,
|
FrigateConditional,
|
||||||
MenuButton,
|
|
||||||
MenuIcon,
|
MenuIcon,
|
||||||
|
MenuItem,
|
||||||
MenuStateIcon,
|
MenuStateIcon,
|
||||||
MenuSubmenu,
|
MenuSubmenu,
|
||||||
MenuSubmenuSelect,
|
MenuSubmenuSelect,
|
||||||
PictureElements,
|
PictureElements,
|
||||||
} from '../types.js';
|
} from '../config/types.js';
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import { dispatchFrigateCardErrorEvent } from './message.js';
|
import ptzStyle from '../scss/elements-ptz.scss';
|
||||||
import { actionHandler } from '../action-handler-directive.js';
|
import elementsStyle from '../scss/elements.scss';
|
||||||
|
import { FrigateCardError } from '../types.js';
|
||||||
import {
|
import {
|
||||||
frigateCardHandleActionConfig,
|
frigateCardHandleActionConfig,
|
||||||
frigateCardHasAction,
|
frigateCardHasAction,
|
||||||
getActionConfigGivenAction,
|
getActionConfigGivenAction,
|
||||||
} from '../utils/action.js';
|
} from '../utils/action.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
import { ConditionControllerEpoch, evaluateConditionViaEvent } from '../conditions.js';
|
import {
|
||||||
|
ConditionsManagerEpoch,
|
||||||
|
evaluateConditionViaEvent,
|
||||||
|
} from '../card-controller/conditions-manager.js';
|
||||||
|
import { dispatchFrigateCardErrorEvent } from './message.js';
|
||||||
|
|
||||||
/* A note on picture element rendering:
|
/* A note on picture element rendering:
|
||||||
*
|
*
|
||||||
@@ -85,7 +88,7 @@ export class FrigateCardElementsCore extends LitElement {
|
|||||||
* property even though it is not currently directly used by this class.
|
* property even though it is not currently directly used by this class.
|
||||||
*/
|
*/
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public conditionControllerEpoch?: ConditionControllerEpoch;
|
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
||||||
|
|
||||||
protected _root: HuiConditionalElement | null = null;
|
protected _root: HuiConditionalElement | null = null;
|
||||||
|
|
||||||
@@ -167,7 +170,7 @@ export class FrigateCardElements extends LitElement {
|
|||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public conditionControllerEpoch?: ConditionControllerEpoch;
|
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public elements: PictureElements;
|
public elements: PictureElements;
|
||||||
@@ -181,11 +184,7 @@ export class FrigateCardElements extends LitElement {
|
|||||||
protected _menuRemoveHandler(ev: Event): void {
|
protected _menuRemoveHandler(ev: Event): void {
|
||||||
// Re-dispatch event from this element (instead of the disconnected one, as
|
// Re-dispatch event from this element (instead of the disconnected one, as
|
||||||
// there is no parent of the disconnected element).
|
// there is no parent of the disconnected element).
|
||||||
dispatchFrigateCardEvent<MenuButton>(
|
dispatchFrigateCardEvent<MenuItem>(this, 'menu-remove', (ev as CustomEvent).detail);
|
||||||
this,
|
|
||||||
'menu-remove',
|
|
||||||
(ev as CustomEvent).detail,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -193,7 +192,7 @@ export class FrigateCardElements extends LitElement {
|
|||||||
* @param ev The event.
|
* @param ev The event.
|
||||||
*/
|
*/
|
||||||
protected _menuAddHandler(ev: Event): void {
|
protected _menuAddHandler(ev: Event): void {
|
||||||
ev = ev as CustomEvent<MenuButton>;
|
ev = ev as CustomEvent<MenuItem>;
|
||||||
const path = ev.composedPath();
|
const path = ev.composedPath();
|
||||||
if (!path.length) {
|
if (!path.length) {
|
||||||
return;
|
return;
|
||||||
@@ -226,7 +225,7 @@ export class FrigateCardElements extends LitElement {
|
|||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`<frigate-card-elements-core
|
return html`<frigate-card-elements-core
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.conditionControllerEpoch=${this.conditionControllerEpoch}
|
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
||||||
.elements=${this.elements}
|
.elements=${this.elements}
|
||||||
>
|
>
|
||||||
</frigate-card-elements-core>`;
|
</frigate-card-elements-core>`;
|
||||||
|
|||||||
+17
-20
@@ -7,37 +7,37 @@ import {
|
|||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import galleryStyle from '../scss/gallery.scss';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import galleryCoreStyle from '../scss/gallery-core.scss';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
|
import throttle from 'lodash-es/throttle';
|
||||||
|
import { CameraManager, ExtendedMediaQueryResult } from '../camera-manager/manager.js';
|
||||||
|
import { EventQuery, MediaQuery, RecordingQuery } from '../camera-manager/types';
|
||||||
import {
|
import {
|
||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
ExtendedHomeAssistant,
|
|
||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
GalleryConfig,
|
GalleryConfig,
|
||||||
THUMBNAIL_WIDTH_MAX,
|
THUMBNAIL_WIDTH_MAX,
|
||||||
} from '../types.js';
|
} from '../config/types';
|
||||||
|
import { localize } from '../localize/localize';
|
||||||
|
import galleryCoreStyle from '../scss/gallery-core.scss';
|
||||||
|
import galleryStyle from '../scss/gallery.scss';
|
||||||
|
import { ExtendedHomeAssistant } from '../types.js';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||||
|
import { errorToConsole, sleep } from '../utils/basic';
|
||||||
import {
|
import {
|
||||||
changeViewToRecentEventsForCameraAndDependents,
|
changeViewToRecentEventsForCameraAndDependents,
|
||||||
changeViewToRecentRecordingForCameraAndDependents,
|
changeViewToRecentRecordingForCameraAndDependents,
|
||||||
} from '../utils/media-to-view.js';
|
} from '../utils/media-to-view.js';
|
||||||
import { CameraManager, ExtendedMediaQueryResult } from '../camera-manager/manager.js';
|
import { ViewMedia } from '../view/media';
|
||||||
|
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
|
||||||
|
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
||||||
|
import { MediaQueriesResults } from '../view/media-queries-results';
|
||||||
import { View } from '../view/view.js';
|
import { View } from '../view/view.js';
|
||||||
|
import './media-filter';
|
||||||
import { renderMessage, renderProgressIndicator } from './message.js';
|
import { renderMessage, renderProgressIndicator } from './message.js';
|
||||||
|
import './surround-basic';
|
||||||
import './thumbnail.js';
|
import './thumbnail.js';
|
||||||
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
|
||||||
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
|
||||||
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
|
|
||||||
import { EventQuery, MediaQuery, RecordingQuery } from '../camera-manager/types';
|
|
||||||
import { MediaQueriesResults } from '../view/media-queries-results';
|
|
||||||
import { errorToConsole, sleep } from '../utils/basic';
|
|
||||||
import './media-filter';
|
|
||||||
import './surround-basic';
|
|
||||||
import { ViewMedia } from '../view/media';
|
|
||||||
import { localize } from '../localize/localize';
|
|
||||||
import throttle from 'lodash-es/throttle';
|
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
|
||||||
|
|
||||||
const GALLERY_MEDIA_FILTER_MENU_ICONS = {
|
const GALLERY_MEDIA_FILTER_MENU_ICONS = {
|
||||||
closed: 'mdi:filter-cog-outline',
|
closed: 'mdi:filter-cog-outline',
|
||||||
@@ -82,7 +82,6 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
if (this.view.is('recordings')) {
|
if (this.view.is('recordings')) {
|
||||||
changeViewToRecentRecordingForCameraAndDependents(
|
changeViewToRecentRecordingForCameraAndDependents(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.cardWideConfig,
|
this.cardWideConfig,
|
||||||
this.view,
|
this.view,
|
||||||
@@ -95,7 +94,6 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
: null;
|
: null;
|
||||||
changeViewToRecentEventsForCameraAndDependents(
|
changeViewToRecentEventsForCameraAndDependents(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.cardWideConfig,
|
this.cardWideConfig,
|
||||||
this.view,
|
this.view,
|
||||||
@@ -344,7 +342,6 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
let extension: ExtendedMediaQueryResult<MediaQuery> | null;
|
let extension: ExtendedMediaQueryResult<MediaQuery> | null;
|
||||||
try {
|
try {
|
||||||
extension = await this.cameraManager.extendMediaQueries<MediaQuery>(
|
extension = await this.cameraManager.extendMediaQueries<MediaQuery>(
|
||||||
this.hass,
|
|
||||||
rawQueries,
|
rawQueries,
|
||||||
existingMedia,
|
existingMedia,
|
||||||
direction,
|
direction,
|
||||||
|
|||||||
@@ -6,29 +6,25 @@ import {
|
|||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
unsafeCSS
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { live } from 'lit/directives/live.js';
|
import { live } from 'lit/directives/live.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import { CachedValueController } from '../cached-value-controller.js';
|
import { CachedValueController } from '../cached-value-controller.js';
|
||||||
|
import { CameraConfig, ImageViewConfig } from '../config/types.js';
|
||||||
import defaultImage from '../images/frigate-bird-in-sky.jpg';
|
import defaultImage from '../images/frigate-bird-in-sky.jpg';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import imageStyle from '../scss/image.scss';
|
import imageStyle from '../scss/image.scss';
|
||||||
import {
|
import { FrigateCardMediaPlayer, MediaLoadedInfo } from '../types.js';
|
||||||
CameraConfig,
|
|
||||||
FrigateCardMediaPlayer,
|
|
||||||
ImageViewConfig,
|
|
||||||
MediaLoadedInfo
|
|
||||||
} from '../types.js';
|
|
||||||
import { contentsChanged } from '../utils/basic.js';
|
import { contentsChanged } from '../utils/basic.js';
|
||||||
import { isHassDifferent } from '../utils/ha';
|
import { isHassDifferent } from '../utils/ha';
|
||||||
import {
|
import {
|
||||||
createMediaLoadedInfo,
|
createMediaLoadedInfo,
|
||||||
dispatchExistingMediaLoadedInfoAsEvent,
|
dispatchExistingMediaLoadedInfoAsEvent,
|
||||||
dispatchMediaPauseEvent,
|
dispatchMediaPauseEvent,
|
||||||
dispatchMediaPlayEvent
|
dispatchMediaPlayEvent,
|
||||||
} from '../utils/media-info.js';
|
} from '../utils/media-info.js';
|
||||||
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
import { updateElementStyleFromMediaLayoutConfig } from '../utils/media-layout.js';
|
||||||
import { View } from '../view/view.js';
|
import { View } from '../view/view.js';
|
||||||
|
|||||||
@@ -8,14 +8,10 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||||
|
import { CameraConfig, MicrophoneConfig } from '../../config/types.js';
|
||||||
import { localize } from '../../localize/localize';
|
import { localize } from '../../localize/localize';
|
||||||
import liveMSEStyle from '../../scss/live-go2rtc.scss';
|
import liveMSEStyle from '../../scss/live-go2rtc.scss';
|
||||||
import {
|
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../types.js';
|
||||||
CameraConfig,
|
|
||||||
ExtendedHomeAssistant,
|
|
||||||
FrigateCardMediaPlayer,
|
|
||||||
MicrophoneConfig,
|
|
||||||
} from '../../types.js';
|
|
||||||
import { getEndpointAddressOrDispatchError } from '../../utils/endpoint';
|
import { getEndpointAddressOrDispatchError } from '../../utils/endpoint';
|
||||||
import { setControlsOnVideo } from '../../utils/media.js';
|
import { setControlsOnVideo } from '../../utils/media.js';
|
||||||
import { screenshotMedia } from '../../utils/screenshot.js';
|
import { screenshotMedia } from '../../utils/screenshot.js';
|
||||||
|
|||||||
@@ -2,12 +2,13 @@ import { HomeAssistant } from 'custom-card-helpers';
|
|||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import liveHAStyle from '../../scss/live-ha.scss';
|
import { CameraConfig } from '../../config/types';
|
||||||
import { CameraConfig, FrigateCardMediaPlayer } from '../../types.js';
|
|
||||||
import { getStateObjOrDispatchError } from './live.js';
|
|
||||||
import '../../patches/ha-camera-stream';
|
import '../../patches/ha-camera-stream';
|
||||||
import '../../patches/ha-hls-player.js';
|
import '../../patches/ha-hls-player.js';
|
||||||
import '../../patches/ha-web-rtc-player.ts';
|
import '../../patches/ha-web-rtc-player.ts';
|
||||||
|
import liveHAStyle from '../../scss/live-ha.scss';
|
||||||
|
import { FrigateCardMediaPlayer } from '../../types.js';
|
||||||
|
import { getStateObjOrDispatchError } from './live.js';
|
||||||
|
|
||||||
@customElement('frigate-card-live-ha')
|
@customElement('frigate-card-live-ha')
|
||||||
export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPlayer {
|
export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPlayer {
|
||||||
@@ -55,7 +56,7 @@ export class FrigateCardLiveHA extends LitElement implements FrigateCardMediaPla
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async getScreenshotURL(): Promise<string | null> {
|
public async getScreenshotURL(): Promise<string | null> {
|
||||||
return await this._playerRef.value?.getScreenshotURL() ?? null;
|
return (await this._playerRef.value?.getScreenshotURL()) ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
|
|||||||
@@ -2,8 +2,9 @@ import { HomeAssistant } from 'custom-card-helpers';
|
|||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
|
import { CameraConfig } from '../../config/types';
|
||||||
import basicBlockStyle from '../../scss/basic-block.scss';
|
import basicBlockStyle from '../../scss/basic-block.scss';
|
||||||
import { CameraConfig, FrigateCardMediaPlayer } from '../../types.js';
|
import { FrigateCardMediaPlayer } from '../../types.js';
|
||||||
import '../image.js';
|
import '../image.js';
|
||||||
import { getStateObjOrDispatchError } from './live.js';
|
import { getStateObjOrDispatchError } from './live.js';
|
||||||
|
|
||||||
|
|||||||
@@ -4,22 +4,18 @@ import { customElement, property } from 'lit/decorators.js';
|
|||||||
import { until } from 'lit/directives/until.js';
|
import { until } from 'lit/directives/until.js';
|
||||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||||
import { renderProgressIndicator } from '../../components/message.js';
|
import { renderProgressIndicator } from '../../components/message.js';
|
||||||
|
import { CameraConfig, CardWideConfig } from '../../config/types.js';
|
||||||
import { localize } from '../../localize/localize.js';
|
import { localize } from '../../localize/localize.js';
|
||||||
import liveJSMPEGStyle from '../../scss/live-jsmpeg.scss';
|
import liveJSMPEGStyle from '../../scss/live-jsmpeg.scss';
|
||||||
import {
|
import { ExtendedHomeAssistant, FrigateCardMediaPlayer } from '../../types.js';
|
||||||
CameraConfig,
|
|
||||||
CardWideConfig,
|
|
||||||
ExtendedHomeAssistant,
|
|
||||||
FrigateCardMediaPlayer,
|
|
||||||
} from '../../types.js';
|
|
||||||
import { getEndpointAddressOrDispatchError } from '../../utils/endpoint.js';
|
import { getEndpointAddressOrDispatchError } from '../../utils/endpoint.js';
|
||||||
import {
|
import {
|
||||||
dispatchMediaLoadedEvent,
|
dispatchMediaLoadedEvent,
|
||||||
dispatchMediaPauseEvent,
|
dispatchMediaPauseEvent,
|
||||||
dispatchMediaPlayEvent,
|
dispatchMediaPlayEvent,
|
||||||
} from '../../utils/media-info.js';
|
} from '../../utils/media-info.js';
|
||||||
import { dispatchErrorMessageEvent } from '../message.js';
|
|
||||||
import { Timer } from '../../utils/timer.js';
|
import { Timer } from '../../utils/timer.js';
|
||||||
|
import { dispatchErrorMessageEvent } from '../message.js';
|
||||||
|
|
||||||
// Number of seconds a signed URL is valid for.
|
// Number of seconds a signed URL is valid for.
|
||||||
const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
const JSMPEG_URL_SIGN_EXPIRY_SECONDS = 24 * 60 * 60;
|
||||||
|
|||||||
@@ -3,14 +3,10 @@ import { HomeAssistant } from 'custom-card-helpers';
|
|||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||||
|
import { CameraConfig, CardWideConfig } from '../../config/types.js';
|
||||||
import { localize } from '../../localize/localize.js';
|
import { localize } from '../../localize/localize.js';
|
||||||
import liveWebRTCCardStyle from '../../scss/live-webrtc-card.scss';
|
import liveWebRTCCardStyle from '../../scss/live-webrtc-card.scss';
|
||||||
import {
|
import { FrigateCardError, FrigateCardMediaPlayer } from '../../types.js';
|
||||||
CameraConfig,
|
|
||||||
CardWideConfig,
|
|
||||||
FrigateCardError,
|
|
||||||
FrigateCardMediaPlayer,
|
|
||||||
} from '../../types.js';
|
|
||||||
import { mayHaveAudio } from '../../utils/audio.js';
|
import { mayHaveAudio } from '../../utils/audio.js';
|
||||||
import {
|
import {
|
||||||
dispatchMediaLoadedEvent,
|
dispatchMediaLoadedEvent,
|
||||||
|
|||||||
+24
-20
@@ -16,26 +16,31 @@ import { keyed } from 'lit/directives/keyed.js';
|
|||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
import { CameraManager } from '../../camera-manager/manager.js';
|
import { CameraManager } from '../../camera-manager/manager.js';
|
||||||
import { CameraConfigs, CameraEndpoints } from '../../camera-manager/types.js';
|
import { CameraConfigs, CameraEndpoints } from '../../camera-manager/types.js';
|
||||||
import { ConditionControllerEpoch, getOverriddenConfig } from '../../conditions.js';
|
import {
|
||||||
|
CameraConfig,
|
||||||
|
CardWideConfig,
|
||||||
|
frigateCardConfigDefaults,
|
||||||
|
LiveConfig,
|
||||||
|
LiveOverrides,
|
||||||
|
LiveProvider,
|
||||||
|
TransitionEffect,
|
||||||
|
} from '../../config/types.js';
|
||||||
import { localize } from '../../localize/localize.js';
|
import { localize } from '../../localize/localize.js';
|
||||||
import basicBlockStyle from '../../scss/basic-block.scss';
|
import basicBlockStyle from '../../scss/basic-block.scss';
|
||||||
import liveCarouselStyle from '../../scss/live-carousel.scss';
|
import liveCarouselStyle from '../../scss/live-carousel.scss';
|
||||||
import liveProviderStyle from '../../scss/live-provider.scss';
|
import liveProviderStyle from '../../scss/live-provider.scss';
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
|
||||||
CardWideConfig,
|
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
frigateCardConfigDefaults,
|
|
||||||
FrigateCardMediaPlayer,
|
FrigateCardMediaPlayer,
|
||||||
LiveConfig,
|
|
||||||
LiveOverrides,
|
|
||||||
LiveProvider,
|
|
||||||
MediaLoadedInfo,
|
MediaLoadedInfo,
|
||||||
Message,
|
Message,
|
||||||
TransitionEffect,
|
|
||||||
} from '../../types.js';
|
} from '../../types.js';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../../utils/action.js';
|
||||||
import { contentsChanged } from '../../utils/basic.js';
|
import { contentsChanged } from '../../utils/basic.js';
|
||||||
|
import {
|
||||||
|
ConditionsManagerEpoch,
|
||||||
|
getOverriddenConfig,
|
||||||
|
} from '../../card-controller/conditions-manager.js';
|
||||||
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
|
import { CarouselSelected } from '../../utils/embla/carousel-controller.js';
|
||||||
import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy-load.js';
|
import { AutoLazyLoad } from '../../utils/embla/plugins/auto-lazy-load/auto-lazy-load.js';
|
||||||
import { AutoMediaActions } from '../../utils/embla/plugins/auto-media-actions/auto-media-actions.js';
|
import { AutoMediaActions } from '../../utils/embla/plugins/auto-media-actions/auto-media-actions.js';
|
||||||
@@ -121,7 +126,7 @@ export const getStateObjOrDispatchError = (
|
|||||||
@customElement('frigate-card-live')
|
@customElement('frigate-card-live')
|
||||||
export class FrigateCardLive extends LitElement {
|
export class FrigateCardLive extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public conditionControllerEpoch?: ConditionControllerEpoch;
|
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: ExtendedHomeAssistant;
|
public hass?: ExtendedHomeAssistant;
|
||||||
@@ -247,7 +252,7 @@ export class FrigateCardLive extends LitElement {
|
|||||||
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
|
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
|
||||||
.overriddenLiveConfig=${this.overriddenLiveConfig}
|
.overriddenLiveConfig=${this.overriddenLiveConfig}
|
||||||
.inBackground=${this._inBackground}
|
.inBackground=${this._inBackground}
|
||||||
.conditionControllerEpoch=${this.conditionControllerEpoch}
|
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
||||||
.liveOverrides=${this.liveOverrides}
|
.liveOverrides=${this.liveOverrides}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
@@ -305,7 +310,7 @@ export class FrigateCardLiveGrid extends LitElement {
|
|||||||
public liveOverrides?: LiveOverrides;
|
public liveOverrides?: LiveOverrides;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public conditionControllerEpoch?: ConditionControllerEpoch;
|
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cardWideConfig?: CardWideConfig;
|
public cardWideConfig?: CardWideConfig;
|
||||||
@@ -325,7 +330,7 @@ export class FrigateCardLiveGrid extends LitElement {
|
|||||||
.viewFilterCameraID=${cameraID}
|
.viewFilterCameraID=${cameraID}
|
||||||
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
|
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
|
||||||
.overriddenLiveConfig=${this.overriddenLiveConfig}
|
.overriddenLiveConfig=${this.overriddenLiveConfig}
|
||||||
.conditionControllerEpoch=${this.conditionControllerEpoch}
|
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
||||||
.liveOverrides=${this.liveOverrides}
|
.liveOverrides=${this.liveOverrides}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
@@ -360,7 +365,7 @@ export class FrigateCardLiveGrid extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.conditionControllerEpoch || !this.nonOverriddenLiveConfig) {
|
if (!this.conditionsManagerEpoch || !this.nonOverriddenLiveConfig) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
|
const cameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||||
@@ -406,7 +411,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
public liveOverrides?: LiveOverrides;
|
public liveOverrides?: LiveOverrides;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public conditionControllerEpoch?: ConditionControllerEpoch;
|
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cardWideConfig?: CardWideConfig;
|
public cardWideConfig?: CardWideConfig;
|
||||||
@@ -572,7 +577,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
!this.nonOverriddenLiveConfig ||
|
!this.nonOverriddenLiveConfig ||
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
!this.cameraManager ||
|
!this.cameraManager ||
|
||||||
!this.conditionControllerEpoch
|
!this.conditionsManagerEpoch
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -581,13 +586,13 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
// <frigate-card-live-provider> is rendering right now, so we provide a
|
// <frigate-card-live-provider> is rendering right now, so we provide a
|
||||||
// stateOverride to evaluate the condition in that context.
|
// stateOverride to evaluate the condition in that context.
|
||||||
const config = getOverriddenConfig(
|
const config = getOverriddenConfig(
|
||||||
this.conditionControllerEpoch.controller,
|
this.conditionsManagerEpoch.manager,
|
||||||
this.nonOverriddenLiveConfig,
|
this.nonOverriddenLiveConfig,
|
||||||
this.liveOverrides,
|
this.liveOverrides,
|
||||||
{ camera: cameraID },
|
{ camera: cameraID },
|
||||||
) as LiveConfig;
|
) as LiveConfig;
|
||||||
|
|
||||||
const cameraMetadata = this.cameraManager.getCameraMetadata(this.hass, cameraID);
|
const cameraMetadata = this.cameraManager.getCameraMetadata(cameraID);
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="embla__slide">
|
<div class="embla__slide">
|
||||||
@@ -650,14 +655,13 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cameraMetadataPrevious = prevID
|
const cameraMetadataPrevious = prevID
|
||||||
? this.cameraManager.getCameraMetadata(this.hass, overrideCameraID(prevID))
|
? this.cameraManager.getCameraMetadata(overrideCameraID(prevID))
|
||||||
: null;
|
: null;
|
||||||
const cameraMetadataCurrent = this.cameraManager.getCameraMetadata(
|
const cameraMetadataCurrent = this.cameraManager.getCameraMetadata(
|
||||||
this.hass,
|
|
||||||
overrideCameraID(this.viewFilterCameraID ?? this.view.camera),
|
overrideCameraID(this.viewFilterCameraID ?? this.view.camera),
|
||||||
);
|
);
|
||||||
const cameraMetadataNext = nextID
|
const cameraMetadataNext = nextID
|
||||||
? this.cameraManager.getCameraMetadata(this.hass, overrideCameraID(nextID))
|
? this.cameraManager.getCameraMetadata(overrideCameraID(nextID))
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const titleConfig = getDefaultTitleConfigForView(
|
const titleConfig = getDefaultTitleConfigForView(
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin';
|
||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import endOfDay from 'date-fns/endOfDay';
|
||||||
|
import endOfMonth from 'date-fns/endOfMonth';
|
||||||
|
import endOfYesterday from 'date-fns/endOfYesterday';
|
||||||
|
import endOfToday from 'date-fns/esm/endOfToday';
|
||||||
|
import startOfToday from 'date-fns/esm/startOfToday';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import parse from 'date-fns/parse';
|
||||||
|
import startOfDay from 'date-fns/startOfDay';
|
||||||
|
import startOfYesterday from 'date-fns/startOfYesterday';
|
||||||
|
import sub from 'date-fns/sub';
|
||||||
import {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
html,
|
||||||
@@ -10,35 +22,23 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
|
import isEqual from 'lodash-es/isEqual';
|
||||||
|
import orderBy from 'lodash-es/orderBy';
|
||||||
|
import uniqWith from 'lodash-es/uniqWith';
|
||||||
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
import { DateRange } from '../camera-manager/range';
|
import { DateRange } from '../camera-manager/range';
|
||||||
|
import { DataQuery, MediaMetadata, QueryType } from '../camera-manager/types';
|
||||||
|
import { CardWideConfig } from '../config/types';
|
||||||
import { localize } from '../localize/localize';
|
import { localize } from '../localize/localize';
|
||||||
import mediaFilterStyle from '../scss/media-filter.scss';
|
import mediaFilterStyle from '../scss/media-filter.scss';
|
||||||
import { executeMediaQueryForView } from '../utils/media-to-view.js';
|
|
||||||
import { errorToConsole, formatDate, prettifyTitle } from '../utils/basic';
|
import { errorToConsole, formatDate, prettifyTitle } from '../utils/basic';
|
||||||
import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin';
|
import { executeMediaQueryForViewWithErrorDispatching } from '../utils/media-to-view.js';
|
||||||
import './select';
|
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
|
||||||
import { FrigateCardSelect, SelectOption, SelectValues } from './select';
|
|
||||||
import uniqWith from 'lodash-es/uniqWith';
|
|
||||||
import sub from 'date-fns/sub';
|
|
||||||
import endOfDay from 'date-fns/endOfDay';
|
|
||||||
import endOfYesterday from 'date-fns/endOfYesterday';
|
|
||||||
import endOfToday from 'date-fns/esm/endOfToday';
|
|
||||||
import startOfToday from 'date-fns/esm/startOfToday';
|
|
||||||
import startOfDay from 'date-fns/startOfDay';
|
|
||||||
import startOfYesterday from 'date-fns/startOfYesterday';
|
|
||||||
import parse from 'date-fns/parse';
|
|
||||||
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import './select';
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { FrigateCardSelect, SelectOption, SelectValues } from './select';
|
||||||
import { DataQuery, MediaMetadata, QueryType } from '../camera-manager/types';
|
|
||||||
import format from 'date-fns/format';
|
|
||||||
import endOfMonth from 'date-fns/endOfMonth';
|
|
||||||
import isEqual from 'lodash-es/isEqual';
|
|
||||||
import { EventMediaQueries, RecordingMediaQueries } from '../view/media-queries';
|
|
||||||
import './select.js';
|
import './select.js';
|
||||||
import orderBy from 'lodash-es/orderBy';
|
|
||||||
import { CardWideConfig } from '../types';
|
|
||||||
|
|
||||||
interface MediaFilterCoreDefaults {
|
interface MediaFilterCoreDefaults {
|
||||||
cameraIDs?: string[];
|
cameraIDs?: string[];
|
||||||
@@ -228,9 +228,8 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
(
|
(
|
||||||
await executeMediaQueryForView(
|
await executeMediaQueryForViewWithErrorDispatching(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.view,
|
this.view,
|
||||||
queries,
|
queries,
|
||||||
@@ -252,9 +251,8 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
(
|
(
|
||||||
await executeMediaQueryForView(
|
await executeMediaQueryForViewWithErrorDispatching(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.view,
|
this.view,
|
||||||
queries,
|
queries,
|
||||||
@@ -275,7 +273,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
this._cameraOptions = Array.from(cameras.keys()).map((cameraID) => ({
|
this._cameraOptions = Array.from(cameras.keys()).map((cameraID) => ({
|
||||||
value: cameraID,
|
value: cameraID,
|
||||||
label: this.hass
|
label: this.hass
|
||||||
? this.cameraManager?.getCameraMetadata(this.hass, cameraID)?.title ?? ''
|
? this.cameraManager?.getCameraMetadata(cameraID)?.title ?? ''
|
||||||
: '',
|
: '',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@@ -284,7 +282,6 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
if (changedProps.has('cameraManager') && this.hass && this.cameraManager) {
|
if (changedProps.has('cameraManager') && this.hass && this.cameraManager) {
|
||||||
this._mediaMetadataController = new MediaMetadataController(
|
this._mediaMetadataController = new MediaMetadataController(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -407,7 +404,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
...(what && { what: what }),
|
...(what && { what: what }),
|
||||||
...(where && { where: where }),
|
...(where && { where: where }),
|
||||||
...(favorite !== undefined && { favorite: favorite }),
|
...(favorite !== undefined && { favorite: favorite }),
|
||||||
...(tags && { tags: tags })
|
...(tags && { tags: tags }),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,18 +472,18 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
</frigate-card-select>`
|
</frigate-card-select>`
|
||||||
: ''}
|
: ''}
|
||||||
${areEvents && this._mediaMetadataController.tagsOptions.length
|
${areEvents && this._mediaMetadataController.tagsOptions.length
|
||||||
? html` <frigate-card-select
|
? html` <frigate-card-select
|
||||||
${ref(this._refTags)}
|
${ref(this._refTags)}
|
||||||
label=${localize('media_filter.tag')}
|
label=${localize('media_filter.tag')}
|
||||||
placeholder=${localize('media_filter.select_tag')}
|
placeholder=${localize('media_filter.select_tag')}
|
||||||
clearable
|
clearable
|
||||||
multiple
|
multiple
|
||||||
.options=${this._mediaMetadataController.tagsOptions}
|
.options=${this._mediaMetadataController.tagsOptions}
|
||||||
.value=${this._defaults?.tags}
|
.value=${this._defaults?.tags}
|
||||||
@frigate-card:select:change=${this._valueChangedHandler.bind(this)}
|
@frigate-card:select:change=${this._valueChangedHandler.bind(this)}
|
||||||
>
|
>
|
||||||
</frigate-card-select>`
|
</frigate-card-select>`
|
||||||
: ''}
|
: ''}
|
||||||
${areEvents && this._mediaMetadataController.whereOptions.length
|
${areEvents && this._mediaMetadataController.whereOptions.length
|
||||||
? html` <frigate-card-select
|
? html` <frigate-card-select
|
||||||
${ref(this._refWhere)}
|
${ref(this._refWhere)}
|
||||||
@@ -523,7 +520,6 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
|
|
||||||
export class MediaMetadataController implements ReactiveController {
|
export class MediaMetadataController implements ReactiveController {
|
||||||
protected _host: ReactiveControllerHost;
|
protected _host: ReactiveControllerHost;
|
||||||
protected _hass: HomeAssistant;
|
|
||||||
protected _cameraManager: CameraManager;
|
protected _cameraManager: CameraManager;
|
||||||
|
|
||||||
public tagsOptions: SelectOption[] = [];
|
public tagsOptions: SelectOption[] = [];
|
||||||
@@ -531,13 +527,8 @@ export class MediaMetadataController implements ReactiveController {
|
|||||||
public whatOptions: SelectOption[] = [];
|
public whatOptions: SelectOption[] = [];
|
||||||
public whereOptions: SelectOption[] = [];
|
public whereOptions: SelectOption[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(host: ReactiveControllerHost, cameraManager: CameraManager) {
|
||||||
host: ReactiveControllerHost,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
cameraManager: CameraManager,
|
|
||||||
) {
|
|
||||||
this._host = host;
|
this._host = host;
|
||||||
this._hass = hass;
|
|
||||||
this._cameraManager = cameraManager;
|
this._cameraManager = cameraManager;
|
||||||
host.addController(this);
|
host.addController(this);
|
||||||
}
|
}
|
||||||
@@ -549,7 +540,7 @@ export class MediaMetadataController implements ReactiveController {
|
|||||||
async hostConnected() {
|
async hostConnected() {
|
||||||
let metadata: MediaMetadata | null;
|
let metadata: MediaMetadata | null;
|
||||||
try {
|
try {
|
||||||
metadata = await this._cameraManager.getMediaMetadata(this._hass);
|
metadata = await this._cameraManager.getMediaMetadata();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorToConsole(e as Error);
|
errorToConsole(e as Error);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
|
import { ViewDisplayConfig } from '../config/types';
|
||||||
import mediaGridStyle from '../scss/media-grid.scss';
|
import mediaGridStyle from '../scss/media-grid.scss';
|
||||||
import { ViewDisplayConfig } from '../types.js';
|
|
||||||
import { MediaGridController } from '../utils/media-grid-controller.js';
|
import { MediaGridController } from '../utils/media-grid-controller.js';
|
||||||
|
|
||||||
@customElement('frigate-card-media-grid')
|
@customElement('frigate-card-media-grid')
|
||||||
|
|||||||
+10
-11
@@ -1,10 +1,10 @@
|
|||||||
import { HASSDomEvent, HomeAssistant } from 'custom-card-helpers';
|
import { HASSDomEvent, HomeAssistant } from 'custom-card-helpers';
|
||||||
import {
|
import {
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
|
html,
|
||||||
unsafeCSS,
|
unsafeCSS,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
@@ -12,26 +12,25 @@ import { classMap } from 'lit/directives/class-map.js';
|
|||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { styleMap } from 'lit/directives/style-map.js';
|
import { styleMap } from 'lit/directives/style-map.js';
|
||||||
import { actionHandler } from '../action-handler-directive.js';
|
import { actionHandler } from '../action-handler-directive.js';
|
||||||
import menuStyle from '../scss/menu.scss';
|
import { FRIGATE_ICON_SVG_PATH } from '../camera-manager/frigate/icon.js';
|
||||||
import type {
|
import type {
|
||||||
ActionsConfig,
|
|
||||||
ActionType,
|
ActionType,
|
||||||
MenuButton,
|
ActionsConfig,
|
||||||
MenuConfig,
|
MenuConfig,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
StateParameters,
|
} from '../config/types.js';
|
||||||
} from '../types.js';
|
import { FRIGATE_BUTTON_MENU_ICON } from '../const.js';
|
||||||
|
import menuStyle from '../scss/menu.scss';
|
||||||
|
import type { StateParameters } from '../types.js';
|
||||||
import {
|
import {
|
||||||
convertActionToFrigateCardCustomAction,
|
convertActionToFrigateCardCustomAction,
|
||||||
frigateCardHandleActionConfig,
|
frigateCardHandleActionConfig,
|
||||||
frigateCardHasAction,
|
frigateCardHasAction,
|
||||||
getActionConfigGivenAction,
|
getActionConfigGivenAction,
|
||||||
} from '../utils/action.js';
|
} from '../utils/action.js';
|
||||||
import { FRIGATE_ICON_SVG_PATH } from '../camera-manager/frigate/icon.js';
|
|
||||||
import { refreshDynamicStateParameters } from '../utils/ha';
|
import { refreshDynamicStateParameters } from '../utils/ha';
|
||||||
import './submenu.js';
|
|
||||||
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
||||||
import { FRIGATE_BUTTON_MENU_ICON } from '../const.js';
|
import './submenu.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A menu for the FrigateCard.
|
* A menu for the FrigateCard.
|
||||||
@@ -62,7 +61,7 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
protected _menuConfig?: MenuConfig;
|
protected _menuConfig?: MenuConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public buttons: MenuButton[] = [];
|
public buttons: MenuItem[] = [];
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public entityRegistryManager?: EntityRegistryManager;
|
public entityRegistryManager?: EntityRegistryManager;
|
||||||
@@ -228,7 +227,7 @@ export class FrigateCardMenu extends LitElement {
|
|||||||
* @param button The button configuration to render.
|
* @param button The button configuration to render.
|
||||||
* @returns A rendered template or void.
|
* @returns A rendered template or void.
|
||||||
*/
|
*/
|
||||||
protected _renderButton(button: MenuButton): TemplateResult | void {
|
protected _renderButton(button: MenuItem): TemplateResult | void {
|
||||||
if (button.type === 'custom:frigate-card-menu-submenu') {
|
if (button.type === 'custom:frigate-card-menu-submenu') {
|
||||||
return html` <frigate-card-submenu
|
return html` <frigate-card-submenu
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
|||||||
@@ -2,10 +2,11 @@ import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit
|
|||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
|
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
|
||||||
import { ref, Ref } from 'lit/directives/ref.js';
|
import { ref, Ref } from 'lit/directives/ref.js';
|
||||||
|
import { CardWideConfig } from '../config/types.js';
|
||||||
import { TROUBLESHOOTING_URL } from '../const.js';
|
import { TROUBLESHOOTING_URL } from '../const.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import messageStyle from '../scss/message.scss';
|
import messageStyle from '../scss/message.scss';
|
||||||
import { CardWideConfig, FrigateCardError, Message, MessageType } from '../types.js';
|
import { FrigateCardError, Message, MessageType } from '../types.js';
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
|
|
||||||
@customElement('frigate-card-message')
|
@customElement('frigate-card-message')
|
||||||
@@ -106,12 +107,12 @@ export class FrigateCardProgressIndicator extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderMessage(message: Message): TemplateResult {
|
export function renderMessage(message: Message | null): TemplateResult {
|
||||||
if (message.type === 'error') {
|
if (message?.type === 'error') {
|
||||||
return html` <frigate-card-error-message
|
return html` <frigate-card-error-message
|
||||||
.message=${message}
|
.message=${message}
|
||||||
></frigate-card-error-message>`;
|
></frigate-card-error-message>`;
|
||||||
} else {
|
} else if (message) {
|
||||||
return html` <frigate-card-message
|
return html` <frigate-card-message
|
||||||
.message=${message.message}
|
.message=${message.message}
|
||||||
.icon=${message.icon}
|
.icon=${message.icon}
|
||||||
@@ -124,7 +125,7 @@ export function renderMessage(message: Message): TemplateResult {
|
|||||||
|
|
||||||
export function renderProgressIndicator(options?: {
|
export function renderProgressIndicator(options?: {
|
||||||
message?: string;
|
message?: string;
|
||||||
cardWideConfig?: CardWideConfig;
|
cardWideConfig?: CardWideConfig | null;
|
||||||
componentRef?: Ref<HTMLElement>;
|
componentRef?: Ref<HTMLElement>;
|
||||||
classes?: ClassInfo;
|
classes?: ClassInfo;
|
||||||
size?: FrigateCardProgressIndicatorSize;
|
size?: FrigateCardProgressIndicatorSize;
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
import { CSSResultGroup, LitElement, TemplateResult, html, unsafeCSS } from 'lit';
|
||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
|
import { NextPreviousControlConfig } from '../config/types.js';
|
||||||
import { NextPreviousControlConfig } from '../types.js';
|
|
||||||
|
|
||||||
import controlStyle from '../scss/next-previous-control.scss';
|
import controlStyle from '../scss/next-previous-control.scss';
|
||||||
import { createFetchThumbnailTask } from '../utils/thumbnail.js';
|
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
|
||||||
import { renderTask } from '../utils/task.js';
|
import { renderTask } from '../utils/task.js';
|
||||||
|
import { createFetchThumbnailTask } from '../utils/thumbnail.js';
|
||||||
|
|
||||||
@customElement('frigate-card-next-previous-control')
|
@customElement('frigate-card-next-previous-control')
|
||||||
export class FrigateCardNextPreviousControl extends LitElement {
|
export class FrigateCardNextPreviousControl extends LitElement {
|
||||||
|
|||||||
@@ -11,13 +11,9 @@ import { customElement, property, state } from 'lit/decorators.js';
|
|||||||
import { ifDefined } from 'lit/directives/if-defined.js';
|
import { ifDefined } from 'lit/directives/if-defined.js';
|
||||||
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||||
import { actionHandler } from '../action-handler-directive.js';
|
import { actionHandler } from '../action-handler-directive.js';
|
||||||
|
import { MenuSubmenu, MenuSubmenuItem, MenuSubmenuSelect } from '../config/types.js';
|
||||||
import submenuStyle from '../scss/submenu.scss';
|
import submenuStyle from '../scss/submenu.scss';
|
||||||
import {
|
import { StateParameters } from '../types.js';
|
||||||
MenuSubmenu,
|
|
||||||
MenuSubmenuItem,
|
|
||||||
MenuSubmenuSelect,
|
|
||||||
StateParameters,
|
|
||||||
} from '../types.js';
|
|
||||||
import {
|
import {
|
||||||
frigateCardHasAction,
|
frigateCardHasAction,
|
||||||
stopEventFromActivatingCardWideActions,
|
stopEventFromActivatingCardWideActions,
|
||||||
@@ -39,7 +35,9 @@ export class FrigateCardSubmenu extends LitElement {
|
|||||||
if (!this.hass) {
|
if (!this.hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const stateParameters = refreshDynamicStateParameters(this.hass, { ...item } as StateParameters);
|
const stateParameters = refreshDynamicStateParameters(this.hass, {
|
||||||
|
...item,
|
||||||
|
} as StateParameters);
|
||||||
const getIcon = (stateParameters: StateParameters): TemplateResult => {
|
const getIcon = (stateParameters: StateParameters): TemplateResult => {
|
||||||
if (stateParameters.icon) {
|
if (stateParameters.icon) {
|
||||||
return html` <ha-icon
|
return html` <ha-icon
|
||||||
@@ -95,7 +93,7 @@ export class FrigateCardSubmenu extends LitElement {
|
|||||||
@click=${(ev) => stopEventFromActivatingCardWideActions(ev)}
|
@click=${(ev) => stopEventFromActivatingCardWideActions(ev)}
|
||||||
>
|
>
|
||||||
<ha-icon-button
|
<ha-icon-button
|
||||||
style="${styleMap(this.submenu.style as StyleInfo || {})}"
|
style="${styleMap((this.submenu.style as StyleInfo) || {})}"
|
||||||
class="button"
|
class="button"
|
||||||
slot="trigger"
|
slot="trigger"
|
||||||
.label=${this.submenu.title || ''}
|
.label=${this.submenu.title || ''}
|
||||||
|
|||||||
@@ -8,14 +8,13 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { CameraManager } from '../camera-manager/manager.js';
|
import { CameraManager } from '../camera-manager/manager.js';
|
||||||
import basicBlockStyle from '../scss/basic-block.scss';
|
|
||||||
import {
|
import {
|
||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
ClipsOrSnapshotsOrAll,
|
|
||||||
ExtendedHomeAssistant,
|
|
||||||
MiniTimelineControlConfig,
|
MiniTimelineControlConfig,
|
||||||
ThumbnailsControlConfig,
|
ThumbnailsControlConfig,
|
||||||
} from '../types.js';
|
} from '../config/types.js';
|
||||||
|
import basicBlockStyle from '../scss/basic-block.scss';
|
||||||
|
import { ClipsOrSnapshotsOrAll, ExtendedHomeAssistant } from '../types.js';
|
||||||
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { contentsChanged, dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
import { getAllDependentCameras } from '../utils/camera.js';
|
import { getAllDependentCameras } from '../utils/camera.js';
|
||||||
import { changeViewToRecentEventsForCameraAndDependents } from '../utils/media-to-view';
|
import { changeViewToRecentEventsForCameraAndDependents } from '../utils/media-to-view';
|
||||||
@@ -82,7 +81,6 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
}
|
}
|
||||||
await changeViewToRecentEventsForCameraAndDependents(
|
await changeViewToRecentEventsForCameraAndDependents(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.cardWideConfig,
|
this.cardWideConfig,
|
||||||
this.view,
|
this.view,
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import {
|
|||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { CameraManager } from '../camera-manager/manager.js';
|
import { CameraManager } from '../camera-manager/manager.js';
|
||||||
|
import { ThumbnailsControlConfig } from '../config/types.js';
|
||||||
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
|
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
|
||||||
import { ExtendedHomeAssistant, ThumbnailsControlConfig } from '../types.js';
|
import { ExtendedHomeAssistant } from '../types.js';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
import { CarouselDirection } from '../utils/embla/carousel-controller.js';
|
import { CarouselDirection } from '../utils/embla/carousel-controller.js';
|
||||||
|
|||||||
@@ -271,7 +271,9 @@ export class FrigateCardThumbnailDetailsRecording extends LitElement {
|
|||||||
const rawEndTime = this.media.getEndTime();
|
const rawEndTime = this.media.getEndTime();
|
||||||
const duration =
|
const duration =
|
||||||
rawStartTime && rawEndTime ? getDurationString(rawStartTime, rawEndTime) : null;
|
rawStartTime && rawEndTime ? getDurationString(rawStartTime, rawEndTime) : null;
|
||||||
const inProgress = this.media.inProgress() ? localize('recording.in_progress') : null;
|
const inProgress = this.media.inProgress()
|
||||||
|
? localize('recording.in_progress')
|
||||||
|
: null;
|
||||||
|
|
||||||
const seek = this.seek ? format(this.seek, 'HH:mm:ss') : null;
|
const seek = this.seek ? format(this.seek, 'HH:mm:ss') : null;
|
||||||
|
|
||||||
@@ -404,7 +406,6 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
mediaCapabilities?.canDownload;
|
mediaCapabilities?.canDownload;
|
||||||
|
|
||||||
const cameraTitle = this.cameraManager.getCameraMetadata(
|
const cameraTitle = this.cameraManager.getCameraMetadata(
|
||||||
this.hass,
|
|
||||||
this.media.getCameraID(),
|
this.media.getCameraID(),
|
||||||
)?.title;
|
)?.title;
|
||||||
|
|
||||||
@@ -434,7 +435,6 @@ export class FrigateCardThumbnail extends LitElement {
|
|||||||
if (this.hass && this.media) {
|
if (this.hass && this.media) {
|
||||||
try {
|
try {
|
||||||
await this.cameraManager?.favoriteMedia(
|
await this.cameraManager?.favoriteMedia(
|
||||||
this.hass,
|
|
||||||
this.media,
|
this.media,
|
||||||
!this.media?.isFavorite(),
|
!this.media?.isFavorite(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -30,17 +30,17 @@ import { CameraManager } from '../camera-manager/manager';
|
|||||||
import { rangesOverlap } from '../camera-manager/range';
|
import { rangesOverlap } from '../camera-manager/range';
|
||||||
import { MediaQuery } from '../camera-manager/types';
|
import { MediaQuery } from '../camera-manager/types';
|
||||||
import { convertRangeToCacheFriendlyTimes } from '../camera-manager/util';
|
import { convertRangeToCacheFriendlyTimes } from '../camera-manager/util';
|
||||||
import { localize } from '../localize/localize';
|
|
||||||
import timelineCoreStyle from '../scss/timeline-core.scss';
|
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
ExtendedHomeAssistant,
|
|
||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
FrigateCardView,
|
FrigateCardView,
|
||||||
ThumbnailsControlBaseConfig,
|
ThumbnailsControlBaseConfig,
|
||||||
TimelineCoreConfig,
|
TimelineCoreConfig,
|
||||||
} from '../types';
|
} from '../config/types';
|
||||||
|
import { localize } from '../localize/localize';
|
||||||
|
import timelineCoreStyle from '../scss/timeline-core.scss';
|
||||||
|
import { ExtendedHomeAssistant } from '../types';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../utils/action';
|
import { stopEventFromActivatingCardWideActions } from '../utils/action';
|
||||||
import {
|
import {
|
||||||
contentsChanged,
|
contentsChanged,
|
||||||
@@ -50,7 +50,10 @@ import {
|
|||||||
isTruthy,
|
isTruthy,
|
||||||
setOrRemoveAttribute,
|
setOrRemoveAttribute,
|
||||||
} from '../utils/basic';
|
} from '../utils/basic';
|
||||||
import { executeMediaQueryForView, findBestMediaIndex } from '../utils/media-to-view';
|
import {
|
||||||
|
executeMediaQueryForViewWithErrorDispatching,
|
||||||
|
findBestMediaIndex,
|
||||||
|
} from '../utils/media-to-view';
|
||||||
import { FrigateCardTimelineItem, TimelineDataSource } from '../utils/timeline-source';
|
import { FrigateCardTimelineItem, TimelineDataSource } from '../utils/timeline-source';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
import { ViewMediaClassifier } from '../view/media-classifier';
|
import { ViewMediaClassifier } from '../view/media-classifier';
|
||||||
@@ -580,9 +583,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
) {
|
) {
|
||||||
const query = this._createMediaQueries('recording');
|
const query = this._createMediaQueries('recording');
|
||||||
if (query) {
|
if (query) {
|
||||||
view = await executeMediaQueryForView(
|
view = await executeMediaQueryForViewWithErrorDispatching(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.view,
|
this.view,
|
||||||
query,
|
query,
|
||||||
@@ -675,11 +677,11 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
}
|
}
|
||||||
this._removeTargetBar();
|
this._removeTargetBar();
|
||||||
|
|
||||||
if (!this.hass || !this._timeline || !this.view) {
|
if (!this._timeline || !this.view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await this._timelineSource?.refresh(this.hass, this._getPrefetchWindow(properties));
|
await this._timelineSource?.refresh(this._getPrefetchWindow(properties));
|
||||||
|
|
||||||
const queryType = MediaQueriesClassifier.getQueriesType(this.view.query);
|
const queryType = MediaQueriesClassifier.getQueriesType(this.view.query);
|
||||||
if (!queryType) {
|
if (!queryType) {
|
||||||
@@ -735,9 +737,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
if (!this.hass || !this.cameraManager || !this.view || !query) {
|
if (!this.hass || !this.cameraManager || !this.view || !query) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const view = await executeMediaQueryForView(
|
const view = await executeMediaQueryForViewWithErrorDispatching(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.view,
|
this.view,
|
||||||
query,
|
query,
|
||||||
@@ -776,7 +777,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
if (!this.hass || !this.cameraManager) {
|
if (!this.hass || !this.cameraManager) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cameraMetadata = this.cameraManager.getCameraMetadata(this.hass, cameraID);
|
const cameraMetadata = this.cameraManager.getCameraMetadata(cameraID);
|
||||||
const cameraCapabilities = this.cameraManager.getCameraCapabilities(cameraID);
|
const cameraCapabilities = this.cameraManager.getCameraCapabilities(cameraID);
|
||||||
|
|
||||||
if (cameraMetadata && cameraCapabilities?.supportsTimeline) {
|
if (cameraMetadata && cameraCapabilities?.supportsTimeline) {
|
||||||
@@ -973,13 +974,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
* Update the timeline from the view object.
|
* Update the timeline from the view object.
|
||||||
*/
|
*/
|
||||||
protected async _updateTimelineFromView(): Promise<void> {
|
protected async _updateTimelineFromView(): Promise<void> {
|
||||||
if (
|
if (!this.view || !this.timelineConfig || !this._timelineSource || !this._timeline) {
|
||||||
!this.hass ||
|
|
||||||
!this.view ||
|
|
||||||
!this.timelineConfig ||
|
|
||||||
!this._timelineSource ||
|
|
||||||
!this._timeline
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1029,7 +1024,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
// (via fetchIfNecessary) may update the timeline contents which causes
|
// (via fetchIfNecessary) may update the timeline contents which causes
|
||||||
// the visjs timeline to stop dragging/panning operations which is very
|
// the visjs timeline to stop dragging/panning operations which is very
|
||||||
// disruptive to the user.
|
// disruptive to the user.
|
||||||
await this._timelineSource?.refresh(this.hass, prefetchedWindow);
|
await this._timelineSource?.refresh(prefetchedWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentSelection = this._timeline.getSelection();
|
const currentSelection = this._timeline.getSelection();
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
|
import { CardWideConfig, TimelineConfig } from '../config/types';
|
||||||
import basicBlockStyle from '../scss/basic-block.scss';
|
import basicBlockStyle from '../scss/basic-block.scss';
|
||||||
import { CardWideConfig, ExtendedHomeAssistant, TimelineConfig } from '../types';
|
import { ExtendedHomeAssistant } from '../types';
|
||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
import './surround.js';
|
import './surround.js';
|
||||||
import './timeline-core.js';
|
import './timeline-core.js';
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
import { createRef, Ref, ref } from 'lit/directives/ref.js';
|
||||||
|
import { TitleControlConfig } from '../config/types';
|
||||||
import titleStyle from '../scss/title-control.scss';
|
import titleStyle from '../scss/title-control.scss';
|
||||||
import { TitleControlConfig } from '../types.js';
|
|
||||||
import { Timer } from '../utils/timer';
|
import { Timer } from '../utils/timer';
|
||||||
import { View } from '../view/view.js';
|
import { View } from '../view/view.js';
|
||||||
|
|
||||||
@@ -64,7 +64,14 @@ export class FrigateCardTitleControl extends LitElement {
|
|||||||
protected _toastRef: Ref<PaperToast> = createRef();
|
protected _toastRef: Ref<PaperToast> = createRef();
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
if (!this.text || !this.config || this.config.mode == 'none' || !this.fitInto) {
|
if (
|
||||||
|
!this.text ||
|
||||||
|
!this.config ||
|
||||||
|
!this.config.mode ||
|
||||||
|
this.config.duration_seconds === undefined ||
|
||||||
|
this.config.mode === 'none' ||
|
||||||
|
!this.fitInto
|
||||||
|
) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ import {
|
|||||||
renderMessage,
|
renderMessage,
|
||||||
renderProgressIndicator,
|
renderProgressIndicator,
|
||||||
} from '../components/message.js';
|
} from '../components/message.js';
|
||||||
|
import {
|
||||||
|
CardWideConfig,
|
||||||
|
frigateCardConfigDefaults,
|
||||||
|
TransitionEffect,
|
||||||
|
ViewerConfig,
|
||||||
|
} from '../config/types.js';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import '../patches/ha-hls-player';
|
import '../patches/ha-hls-player';
|
||||||
import basicBlockStyle from '../scss/basic-block.scss';
|
import basicBlockStyle from '../scss/basic-block.scss';
|
||||||
@@ -23,13 +29,9 @@ import viewerCarouselStyle from '../scss/viewer-carousel.scss';
|
|||||||
import viewerProviderStyle from '../scss/viewer-provider.scss';
|
import viewerProviderStyle from '../scss/viewer-provider.scss';
|
||||||
import viewerStyle from '../scss/viewer.scss';
|
import viewerStyle from '../scss/viewer.scss';
|
||||||
import {
|
import {
|
||||||
CardWideConfig,
|
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
frigateCardConfigDefaults,
|
|
||||||
FrigateCardMediaPlayer,
|
FrigateCardMediaPlayer,
|
||||||
MediaLoadedInfo,
|
MediaLoadedInfo,
|
||||||
TransitionEffect,
|
|
||||||
ViewerConfig,
|
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
import { stopEventFromActivatingCardWideActions } from '../utils/action.js';
|
||||||
import { mayHaveAudio } from '../utils/audio.js';
|
import { mayHaveAudio } from '../utils/audio.js';
|
||||||
@@ -151,7 +153,6 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
if (mediaType === 'recordings') {
|
if (mediaType === 'recordings') {
|
||||||
changeViewToRecentRecordingForCameraAndDependents(
|
changeViewToRecentRecordingForCameraAndDependents(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.cardWideConfig,
|
this.cardWideConfig,
|
||||||
this.view,
|
this.view,
|
||||||
@@ -163,7 +164,6 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
} else {
|
} else {
|
||||||
changeViewToRecentEventsForCameraAndDependents(
|
changeViewToRecentEventsForCameraAndDependents(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.cardWideConfig,
|
this.cardWideConfig,
|
||||||
this.view,
|
this.view,
|
||||||
@@ -447,7 +447,6 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cameraMetadata = this.cameraManager.getCameraMetadata(
|
const cameraMetadata = this.cameraManager.getCameraMetadata(
|
||||||
this.hass,
|
|
||||||
selectedMedia.getCameraID(),
|
selectedMedia.getCameraID(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -526,12 +525,7 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
*/
|
*/
|
||||||
protected async _seekHandler(): Promise<void> {
|
protected async _seekHandler(): Promise<void> {
|
||||||
const seek = this.view?.context?.mediaViewer?.seek;
|
const seek = this.view?.context?.mediaViewer?.seek;
|
||||||
if (
|
if (!this.hass || !seek || !this._media || !this._player) {
|
||||||
!this.hass ||
|
|
||||||
!seek ||
|
|
||||||
!this._media ||
|
|
||||||
!this._player
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const selectedMedia = this._media[this._selected];
|
const selectedMedia = this._media[this._selected];
|
||||||
@@ -548,8 +542,7 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const seekTime =
|
const seekTime =
|
||||||
(await this.cameraManager?.getMediaSeekTime(this.hass, selectedMedia, seek)) ??
|
(await this.cameraManager?.getMediaSeekTime(selectedMedia, seek)) ?? null;
|
||||||
null;
|
|
||||||
|
|
||||||
if (seekTime !== null) {
|
if (seekTime !== null) {
|
||||||
this._player.seek(seekTime);
|
this._player.seek(seekTime);
|
||||||
@@ -813,7 +806,7 @@ export class FrigateCardViewerProvider
|
|||||||
|
|
||||||
let mediaArray: ViewMedia[] | null;
|
let mediaArray: ViewMedia[] | null;
|
||||||
try {
|
try {
|
||||||
mediaArray = await this.cameraManager.executeMediaQueries(this.hass, queries);
|
mediaArray = await this.cameraManager.executeMediaQueries(queries);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorToConsole(e as Error);
|
errorToConsole(e as Error);
|
||||||
return;
|
return;
|
||||||
|
|||||||
+46
-32
@@ -9,13 +9,17 @@ import {
|
|||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { CameraManager } from '../camera-manager/manager.js';
|
import { CameraManager } from '../camera-manager/manager.js';
|
||||||
import { ConditionControllerEpoch, getOverridesByKey } from '../conditions';
|
import { ConditionsManagerEpoch, getOverridesByKey } from '../card-controller/conditions-manager.js';
|
||||||
import viewsStyle from '../scss/views.scss';
|
import viewsStyle from '../scss/views.scss';
|
||||||
import { CardWideConfig, ExtendedHomeAssistant, FrigateCardConfig } from '../types.js';
|
import { ExtendedHomeAssistant } from '../types.js';
|
||||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
import { ConfigManager } from '../card-controller/config-manager.js';
|
||||||
|
import { ResolvedMediaCache } from '../utils/ha/resolved-media.js';
|
||||||
import { View } from '../view/view.js';
|
import { View } from '../view/view.js';
|
||||||
import './surround.js';
|
import './surround.js';
|
||||||
|
|
||||||
|
// As a special case: Diagnostics is not dynamically loaded in case something goes wrong.
|
||||||
|
import './diagnostics.js';
|
||||||
|
|
||||||
@customElement('frigate-card-views')
|
@customElement('frigate-card-views')
|
||||||
export class FrigateCardViews extends LitElement {
|
export class FrigateCardViews extends LitElement {
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
@@ -28,19 +32,13 @@ export class FrigateCardViews extends LitElement {
|
|||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public config?: FrigateCardConfig;
|
public configManager?: ConfigManager;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public nonOverriddenConfig?: FrigateCardConfig;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cardWideConfig?: CardWideConfig;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public resolvedMediaCache?: ResolvedMediaCache;
|
public resolvedMediaCache?: ResolvedMediaCache;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public conditionControllerEpoch?: ConditionControllerEpoch;
|
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hide?: boolean;
|
public hide?: boolean;
|
||||||
@@ -93,15 +91,24 @@ export class FrigateCardViews extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _shouldLivePreload(): boolean {
|
protected _shouldLivePreload(): boolean {
|
||||||
return !!this.config?.live.preload;
|
return (
|
||||||
|
// Special case: Never preload for diagnostics -- we want that to be as
|
||||||
|
// minimal as possible.
|
||||||
|
!!this.configManager?.getConfig()?.live.preload && !this.view?.is('diagnostics')
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
|
const config = this.configManager?.getConfig();
|
||||||
|
const nonOverriddenConfig = this.configManager?.getNonOverriddenConfig();
|
||||||
|
const cardWideConfig = this.configManager?.getCardWideConfig();
|
||||||
|
const rawConfig = this.configManager?.getRawConfig();
|
||||||
|
|
||||||
// Only essential items should be added to the below list, since we want the
|
// Only essential items should be added to the below list, since we want the
|
||||||
// overall views pane to render in ~almost all cases (e.g. for a camera
|
// overall views pane to render in ~almost all cases (e.g. for a camera
|
||||||
// initialization error to display, `view` and `cameraConfig` may both be
|
// initialization error to display, `view` and `cameraConfig` may both be
|
||||||
// undefined, but we still want to render).
|
// undefined, but we still want to render).
|
||||||
if (!this.hass || !this.config || !this.nonOverriddenConfig) {
|
if (!this.hass || !config || !nonOverriddenConfig || !cardWideConfig) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,17 +122,17 @@ export class FrigateCardViews extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const thumbnailConfig = this.view?.is('live')
|
const thumbnailConfig = this.view?.is('live')
|
||||||
? this.config.live.controls.thumbnails
|
? config.live.controls.thumbnails
|
||||||
: this.view?.isViewerView()
|
: this.view?.isViewerView()
|
||||||
? this.config.media_viewer.controls.thumbnails
|
? config.media_viewer.controls.thumbnails
|
||||||
: this.view?.is('timeline')
|
: this.view?.is('timeline')
|
||||||
? this.config.timeline.controls.thumbnails
|
? config.timeline.controls.thumbnails
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const miniTimelineConfig = this.view?.is('live')
|
const miniTimelineConfig = this.view?.is('live')
|
||||||
? this.config.live.controls.timeline
|
? config.live.controls.timeline
|
||||||
: this.view?.isViewerView()
|
: this.view?.isViewerView()
|
||||||
? this.config.media_viewer.controls.timeline
|
? config.media_viewer.controls.timeline
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const cameraConfig = this.view
|
const cameraConfig = this.view
|
||||||
@@ -137,16 +144,16 @@ export class FrigateCardViews extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.fetchMedia=${this.view?.is('live')
|
.fetchMedia=${this.view?.is('live')
|
||||||
? this.config.live.controls.thumbnails.media
|
? config.live.controls.thumbnails.media
|
||||||
: undefined}
|
: undefined}
|
||||||
.thumbnailConfig=${!this.hide ? thumbnailConfig : undefined}
|
.thumbnailConfig=${!this.hide ? thumbnailConfig : undefined}
|
||||||
.timelineConfig=${!this.hide ? miniTimelineConfig : undefined}
|
.timelineConfig=${!this.hide ? miniTimelineConfig : undefined}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${cardWideConfig}
|
||||||
>
|
>
|
||||||
${!this.hide && this.view?.is('image') && cameraConfig
|
${!this.hide && this.view?.is('image') && cameraConfig
|
||||||
? html` <frigate-card-image
|
? html` <frigate-card-image
|
||||||
.imageConfig=${this.config.image}
|
.imageConfig=${config.image}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameraConfig=${cameraConfig}
|
.cameraConfig=${cameraConfig}
|
||||||
@@ -158,9 +165,9 @@ export class FrigateCardViews extends LitElement {
|
|||||||
? html` <frigate-card-gallery
|
? html` <frigate-card-gallery
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.galleryConfig=${this.config.media_gallery}
|
.galleryConfig=${config.media_gallery}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${cardWideConfig}
|
||||||
>
|
>
|
||||||
</frigate-card-gallery>`
|
</frigate-card-gallery>`
|
||||||
: ``}
|
: ``}
|
||||||
@@ -169,10 +176,10 @@ export class FrigateCardViews extends LitElement {
|
|||||||
<frigate-card-viewer
|
<frigate-card-viewer
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.viewerConfig=${this.config.media_viewer}
|
.viewerConfig=${config.media_viewer}
|
||||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${cardWideConfig}
|
||||||
>
|
>
|
||||||
</frigate-card-viewer>
|
</frigate-card-viewer>
|
||||||
`
|
`
|
||||||
@@ -181,12 +188,19 @@ export class FrigateCardViews extends LitElement {
|
|||||||
? html` <frigate-card-timeline
|
? html` <frigate-card-timeline
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.timelineConfig=${this.config.timeline}
|
.timelineConfig=${config.timeline}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${cardWideConfig}
|
||||||
>
|
>
|
||||||
</frigate-card-timeline>`
|
</frigate-card-timeline>`
|
||||||
: ``}
|
: ``}
|
||||||
|
${!this.hide && this.view?.is('diagnostics')
|
||||||
|
? html` <frigate-card-diagnostics
|
||||||
|
.hass=${this.hass}
|
||||||
|
.rawConfig=${rawConfig}
|
||||||
|
>
|
||||||
|
</frigate-card-diagnostics>`
|
||||||
|
: ``}
|
||||||
${
|
${
|
||||||
// Note: Subtle difference in condition below vs the other views in order
|
// Note: Subtle difference in condition below vs the other views in order
|
||||||
// to always render the live view for live.preload mode.
|
// to always render the live view for live.preload mode.
|
||||||
@@ -199,12 +213,12 @@ export class FrigateCardViews extends LitElement {
|
|||||||
<frigate-card-live
|
<frigate-card-live
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.nonOverriddenLiveConfig=${this.nonOverriddenConfig.live}
|
.nonOverriddenLiveConfig=${nonOverriddenConfig.live}
|
||||||
.overriddenLiveConfig=${this.config.live}
|
.overriddenLiveConfig=${config.live}
|
||||||
.conditionControllerEpoch=${this.conditionControllerEpoch}
|
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
||||||
.liveOverrides=${getOverridesByKey('live', this.config.overrides)}
|
.liveOverrides=${getOverridesByKey('live', config.overrides)}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${cardWideConfig}
|
||||||
.microphoneStream=${this.microphoneStream}
|
.microphoneStream=${this.microphoneStream}
|
||||||
class="${classMap(liveClasses)}"
|
class="${classMap(liveClasses)}"
|
||||||
>
|
>
|
||||||
|
|||||||
+6
-6
@@ -2,6 +2,12 @@ import cloneDeep from 'lodash-es/cloneDeep';
|
|||||||
import get from 'lodash-es/get';
|
import get from 'lodash-es/get';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import set from 'lodash-es/set';
|
import set from 'lodash-es/set';
|
||||||
|
import {
|
||||||
|
BUTTON_SIZE_MIN,
|
||||||
|
RawFrigateCardConfig,
|
||||||
|
THUMBNAIL_WIDTH_MAX,
|
||||||
|
THUMBNAIL_WIDTH_MIN,
|
||||||
|
} from './config/types';
|
||||||
import {
|
import {
|
||||||
CONF_CAMERAS,
|
CONF_CAMERAS,
|
||||||
CONF_CAMERAS_GLOBAL_IMAGE,
|
CONF_CAMERAS_GLOBAL_IMAGE,
|
||||||
@@ -28,12 +34,6 @@ import {
|
|||||||
CONF_MENU_STYLE,
|
CONF_MENU_STYLE,
|
||||||
CONF_OVERRIDES,
|
CONF_OVERRIDES,
|
||||||
} from './const';
|
} from './const';
|
||||||
import {
|
|
||||||
BUTTON_SIZE_MIN,
|
|
||||||
RawFrigateCardConfig,
|
|
||||||
THUMBNAIL_WIDTH_MAX,
|
|
||||||
THUMBNAIL_WIDTH_MIN,
|
|
||||||
} from './types';
|
|
||||||
import { arrayify } from './utils/basic';
|
import { arrayify } from './utils/basic';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1512
File diff suppressed because it is too large
Load Diff
+11
-11
@@ -16,6 +16,16 @@ import {
|
|||||||
setConfigValue,
|
setConfigValue,
|
||||||
upgradeConfig,
|
upgradeConfig,
|
||||||
} from './config-mgmt.js';
|
} from './config-mgmt.js';
|
||||||
|
import {
|
||||||
|
BUTTON_SIZE_MIN,
|
||||||
|
FRIGATE_MENU_PRIORITY_MAX,
|
||||||
|
FrigateCardConfig,
|
||||||
|
frigateCardConfigDefaults,
|
||||||
|
RawFrigateCardConfig,
|
||||||
|
RawFrigateCardConfigArray,
|
||||||
|
THUMBNAIL_WIDTH_MAX,
|
||||||
|
THUMBNAIL_WIDTH_MIN,
|
||||||
|
} from './config/types.js';
|
||||||
import {
|
import {
|
||||||
CONF_CAMERAS,
|
CONF_CAMERAS,
|
||||||
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
|
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
|
||||||
@@ -134,8 +144,8 @@ import {
|
|||||||
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
|
CONF_MEDIA_VIEWER_TRANSITION_EFFECT,
|
||||||
CONF_MEDIA_VIEWER_ZOOMABLE,
|
CONF_MEDIA_VIEWER_ZOOMABLE,
|
||||||
CONF_MENU_ALIGNMENT,
|
CONF_MENU_ALIGNMENT,
|
||||||
CONF_MENU_BUTTONS,
|
|
||||||
CONF_MENU_BUTTON_SIZE,
|
CONF_MENU_BUTTON_SIZE,
|
||||||
|
CONF_MENU_BUTTONS,
|
||||||
CONF_MENU_POSITION,
|
CONF_MENU_POSITION,
|
||||||
CONF_MENU_STYLE,
|
CONF_MENU_STYLE,
|
||||||
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
|
CONF_PERFORMANCE_FEATURES_ANIMATED_PROGRESS_INDICATOR,
|
||||||
@@ -171,16 +181,6 @@ import {
|
|||||||
import { localize } from './localize/localize.js';
|
import { localize } from './localize/localize.js';
|
||||||
import { setLowPerformanceProfile } from './performance.js';
|
import { setLowPerformanceProfile } from './performance.js';
|
||||||
import frigate_card_editor_style from './scss/editor.scss';
|
import frigate_card_editor_style from './scss/editor.scss';
|
||||||
import {
|
|
||||||
BUTTON_SIZE_MIN,
|
|
||||||
FrigateCardConfig,
|
|
||||||
frigateCardConfigDefaults,
|
|
||||||
FRIGATE_MENU_PRIORITY_MAX,
|
|
||||||
RawFrigateCardConfig,
|
|
||||||
RawFrigateCardConfigArray,
|
|
||||||
THUMBNAIL_WIDTH_MAX,
|
|
||||||
THUMBNAIL_WIDTH_MIN,
|
|
||||||
} from './types.js';
|
|
||||||
import { arrayMove, prettifyTitle } from './utils/basic.js';
|
import { arrayMove, prettifyTitle } from './utils/basic.js';
|
||||||
import { getCameraID } from './utils/camera.js';
|
import { getCameraID } from './utils/camera.js';
|
||||||
import {
|
import {
|
||||||
|
|||||||
+6
-6
@@ -1,10 +1,9 @@
|
|||||||
import { deepRemoveDefaults } from './utils/zod.js';
|
|
||||||
import {
|
|
||||||
frigateCardConfigSchema,
|
|
||||||
RawFrigateCardConfig,
|
|
||||||
PerformanceConfig,
|
|
||||||
} from './types';
|
|
||||||
import { getConfigValue, setConfigValue } from './config-mgmt.js';
|
import { getConfigValue, setConfigValue } from './config-mgmt.js';
|
||||||
|
import {
|
||||||
|
PerformanceConfig,
|
||||||
|
RawFrigateCardConfig,
|
||||||
|
frigateCardConfigSchema,
|
||||||
|
} from './config/types.js';
|
||||||
import {
|
import {
|
||||||
CONF_CAMERAS_GLOBAL_IMAGE_REFRESH_SECONDS,
|
CONF_CAMERAS_GLOBAL_IMAGE_REFRESH_SECONDS,
|
||||||
CONF_CAMERAS_GLOBAL_TRIGGERS_OCCUPANCY,
|
CONF_CAMERAS_GLOBAL_TRIGGERS_OCCUPANCY,
|
||||||
@@ -53,6 +52,7 @@ import {
|
|||||||
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
|
CONF_TIMELINE_CONTROLS_THUMBNAILS_SHOW_TIMELINE_CONTROL,
|
||||||
CONF_TIMELINE_SHOW_RECORDINGS,
|
CONF_TIMELINE_SHOW_RECORDINGS,
|
||||||
} from './const.js';
|
} from './const.js';
|
||||||
|
import { deepRemoveDefaults } from './utils/zod.js';
|
||||||
|
|
||||||
// Caution: These values are applied after parsing (since we cannot know the
|
// Caution: These values are applied after parsing (since we cannot know the
|
||||||
// performance profile until afterwards), so there is no validation on these
|
// performance profile until afterwards), so there is no validation on these
|
||||||
|
|||||||
+11
-1477
File diff suppressed because it is too large
Load Diff
+1
-20
@@ -10,9 +10,8 @@ import {
|
|||||||
FrigateCardAction,
|
FrigateCardAction,
|
||||||
FrigateCardCustomAction,
|
FrigateCardCustomAction,
|
||||||
frigateCardCustomActionSchema,
|
frigateCardCustomActionSchema,
|
||||||
FrigateCardViewAction,
|
|
||||||
ViewDisplayMode,
|
ViewDisplayMode,
|
||||||
} from '../types.js';
|
} from '../config/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert a generic Action to a FrigateCardCustomAction if it parses correctly.
|
* Convert a generic Action to a FrigateCardCustomAction if it parses correctly.
|
||||||
@@ -187,21 +186,3 @@ export const frigateCardHasAction = (config?: ActionType | ActionType[]): boolea
|
|||||||
export const stopEventFromActivatingCardWideActions = (ev: Event): void => {
|
export const stopEventFromActivatingCardWideActions = (ev: Event): void => {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
};
|
};
|
||||||
|
|
||||||
export const isViewAction = (
|
|
||||||
action: FrigateCardCustomAction,
|
|
||||||
): action is FrigateCardViewAction => {
|
|
||||||
switch (action.frigate_card_action) {
|
|
||||||
case 'clip':
|
|
||||||
case 'clips':
|
|
||||||
case 'image':
|
|
||||||
case 'live':
|
|
||||||
case 'recording':
|
|
||||||
case 'recordings':
|
|
||||||
case 'snapshot':
|
|
||||||
case 'snapshots':
|
|
||||||
case 'timeline':
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
import { CameraManager } from '../camera-manager/manager.js';
|
import { CameraManager } from '../camera-manager/manager.js';
|
||||||
import { CameraConfig, RawFrigateCardConfig } from '../types.js';
|
import { CameraConfig, RawFrigateCardConfig } from '../config/types.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a camera id.
|
* Get a camera id.
|
||||||
|
|||||||
+2
-2
@@ -1,6 +1,6 @@
|
|||||||
import { CardWideConfig } from '../types';
|
import { CardWideConfig } from '../config/types';
|
||||||
|
|
||||||
export const log = (cardWideConfig?: CardWideConfig, ...args: unknown[]) => {
|
export const log = (cardWideConfig?: CardWideConfig | null, ...args: unknown[]) => {
|
||||||
if (cardWideConfig?.debug?.logging) {
|
if (cardWideConfig?.debug?.logging) {
|
||||||
console.debug(...args);
|
console.debug(...args);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import pkg from '../../package.json';
|
||||||
|
import { RawFrigateCardConfig } from '../config/types';
|
||||||
|
import { getLanguage } from '../localize/localize';
|
||||||
|
import { DeviceList, getAllDevices } from './ha/device-registry';
|
||||||
|
|
||||||
|
type FrigateVersions = Record<string, string>;
|
||||||
|
|
||||||
|
interface GitDiagnostics {
|
||||||
|
build_version?: string;
|
||||||
|
build_date?: string;
|
||||||
|
commit_date?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Diagnostics {
|
||||||
|
card_version: string;
|
||||||
|
browser: string;
|
||||||
|
date: Date;
|
||||||
|
lang: string;
|
||||||
|
timezone: string;
|
||||||
|
git: GitDiagnostics;
|
||||||
|
|
||||||
|
frigate_versions?: FrigateVersions;
|
||||||
|
ha_version?: string;
|
||||||
|
config?: RawFrigateCardConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getDiagnostics = async (
|
||||||
|
hass?: HomeAssistant,
|
||||||
|
rawConfig?: RawFrigateCardConfig,
|
||||||
|
): Promise<Diagnostics> => {
|
||||||
|
let devices: DeviceList | undefined = [];
|
||||||
|
if (hass) {
|
||||||
|
try {
|
||||||
|
devices = await getAllDevices(hass);
|
||||||
|
} catch (e) {
|
||||||
|
// Pass. This is optional.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the Frigate devices in order to extract the Frigate integration and
|
||||||
|
// server version numbers.
|
||||||
|
const frigateDevices = devices?.filter((device) => device.manufacturer === 'Frigate');
|
||||||
|
const frigateVersionMap: Map<string, string> = new Map();
|
||||||
|
frigateDevices?.forEach((device) => {
|
||||||
|
device.config_entries.forEach((configEntry) => {
|
||||||
|
if (device.model) {
|
||||||
|
frigateVersionMap.set(configEntry, device.model);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
card_version: pkg.version,
|
||||||
|
browser: navigator.userAgent,
|
||||||
|
date: new Date(),
|
||||||
|
...(frigateVersionMap.size && {
|
||||||
|
frigate_versions: Object.fromEntries(frigateVersionMap),
|
||||||
|
}),
|
||||||
|
lang: getLanguage(),
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
git: {
|
||||||
|
...(pkg['gitVersion'] && { build_version: pkg['gitVersion'] }),
|
||||||
|
...(pkg['buildDate'] && { build_date: pkg['buildDate'] }),
|
||||||
|
...(pkg['gitDate'] && { commit_date: pkg['gitDate'] }),
|
||||||
|
},
|
||||||
|
...(hass && { ha_version: hass.config.version }),
|
||||||
|
...(rawConfig && { config: rawConfig }),
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -39,7 +39,7 @@ export const downloadMedia = async (
|
|||||||
cameraManager: CameraManager,
|
cameraManager: CameraManager,
|
||||||
media: ViewMedia,
|
media: ViewMedia,
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const download = await cameraManager.getMediaDownloadPath(hass, media);
|
const download = await cameraManager.getMediaDownloadPath(media);
|
||||||
if (!download) {
|
if (!download) {
|
||||||
throw new FrigateCardError(localize('error.download_no_media'));
|
throw new FrigateCardError(localize('error.download_no_media'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import EmblaCarousel, { EmblaCarouselType } from 'embla-carousel';
|
|||||||
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
import { WheelGesturesPlugin } from 'embla-carousel-wheel-gestures';
|
||||||
import { CreatePluginType, LoosePluginType } from 'embla-carousel/components/Plugins';
|
import { CreatePluginType, LoosePluginType } from 'embla-carousel/components/Plugins';
|
||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import { TransitionEffect } from '../../types';
|
import { TransitionEffect } from '../../config/types';
|
||||||
import { dispatchFrigateCardEvent, getChildrenFromElement } from '../basic.js';
|
import { dispatchFrigateCardEvent, getChildrenFromElement } from '../basic.js';
|
||||||
|
|
||||||
export interface CarouselSelected {
|
export interface CarouselSelected {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { EmblaCarouselType, EmblaEventType } from 'embla-carousel';
|
|||||||
import { CreateOptionsType } from 'embla-carousel/components/Options';
|
import { CreateOptionsType } from 'embla-carousel/components/Options';
|
||||||
import { OptionsHandlerType } from 'embla-carousel/components/OptionsHandler';
|
import { OptionsHandlerType } from 'embla-carousel/components/OptionsHandler';
|
||||||
import { CreatePluginType, LoosePluginType } from 'embla-carousel/components/Plugins';
|
import { CreatePluginType, LoosePluginType } from 'embla-carousel/components/Plugins';
|
||||||
import { LazyUnloadCondition } from '../../../../types';
|
import { LazyUnloadCondition } from '../../../../config/types';
|
||||||
|
|
||||||
declare module 'embla-carousel/components/Plugins' {
|
declare module 'embla-carousel/components/Plugins' {
|
||||||
interface EmblaPluginsType {
|
interface EmblaPluginsType {
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import {
|
|||||||
AutoPauseCondition,
|
AutoPauseCondition,
|
||||||
AutoPlayCondition,
|
AutoPlayCondition,
|
||||||
AutoUnmuteCondition,
|
AutoUnmuteCondition,
|
||||||
FrigateCardMediaPlayer,
|
} from '../../../../config/types.js';
|
||||||
} from '../../../../types.js';
|
import { FrigateCardMediaPlayer } from '../../../../types.js';
|
||||||
|
|
||||||
declare module 'embla-carousel/components/Plugins' {
|
declare module 'embla-carousel/components/Plugins' {
|
||||||
interface EmblaPluginsType {
|
interface EmblaPluginsType {
|
||||||
|
|||||||
@@ -364,3 +364,19 @@ export function canonicalizeHAURL(
|
|||||||
}
|
}
|
||||||
return url ?? null;
|
return url ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if HA connection state has changed.
|
||||||
|
* @param newHass The new HA object.
|
||||||
|
* @param oldHass The old HA object.
|
||||||
|
* @returns `true` if the connection state has changed.
|
||||||
|
*/
|
||||||
|
export const hasHAConnectionStateChanged = (
|
||||||
|
oldHass: HomeAssistant | undefined | null,
|
||||||
|
newHass: HomeAssistant | undefined | null,
|
||||||
|
): boolean => {
|
||||||
|
return (
|
||||||
|
(!oldHass && !newHass?.connected) ||
|
||||||
|
(!!oldHass && oldHass.connected !== !!newHass?.connected)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { allPromises } from './basic';
|
import { allPromises } from '../basic';
|
||||||
|
|
||||||
enum InitializationState {
|
enum InitializationState {
|
||||||
INITIALIZING = 'initializing',
|
INITIALIZING = 'initializing',
|
||||||
INITIALIZED = 'initialized',
|
INITIALIZED = 'initialized',
|
||||||
}
|
}
|
||||||
|
|
||||||
type Initializer = () => Promise<unknown>;
|
type InitializationCallback = () => Promise<unknown>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Manages initialization state & calling initializers.
|
* Manages initialization state & calling initializers.
|
||||||
*/
|
*/
|
||||||
export class FrigateCardInitializer {
|
export class Initializer {
|
||||||
protected _state: Map<string, InitializationState>;
|
protected _state: Map<string, InitializationState>;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -18,7 +18,7 @@ export class FrigateCardInitializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async initializeMultipleIfNecessary(
|
public async initializeMultipleIfNecessary(
|
||||||
aspects: Record<string, Initializer>,
|
aspects: Record<string, InitializationCallback>,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const results = await allPromises(
|
const results = await allPromises(
|
||||||
Object.entries(aspects),
|
Object.entries(aspects),
|
||||||
@@ -36,7 +36,7 @@ export class FrigateCardInitializer {
|
|||||||
*/
|
*/
|
||||||
public async initializeIfNecessary(
|
public async initializeIfNecessary(
|
||||||
aspect: string,
|
aspect: string,
|
||||||
initializer?: Initializer,
|
initializer?: InitializationCallback,
|
||||||
): Promise<boolean> {
|
): Promise<boolean> {
|
||||||
const state = this._state.get(aspect);
|
const state = this._state.get(aspect);
|
||||||
if (state !== InitializationState.INITIALIZED) {
|
if (state !== InitializationState.INITIALIZED) {
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import isEqual from 'lodash-es/isEqual';
|
import isEqual from 'lodash-es/isEqual';
|
||||||
import throttle from 'lodash-es/throttle';
|
import throttle from 'lodash-es/throttle';
|
||||||
import Masonry from 'masonry-layout';
|
import Masonry from 'masonry-layout';
|
||||||
import { MediaLoadedInfo, ViewDisplayConfig } from '../types';
|
import { ViewDisplayConfig } from '../config/types';
|
||||||
|
import { MediaLoadedInfo } from '../types';
|
||||||
import {
|
import {
|
||||||
dispatchFrigateCardEvent,
|
dispatchFrigateCardEvent,
|
||||||
getChildrenFromElement,
|
getChildrenFromElement,
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
import { MediaLoadedInfo } from '../types';
|
|
||||||
|
|
||||||
export class MediaLoadedInfoController {
|
|
||||||
protected _current: MediaLoadedInfo | null = null;
|
|
||||||
protected _lastKnown: MediaLoadedInfo | null = null;
|
|
||||||
|
|
||||||
public set(current: MediaLoadedInfo): void {
|
|
||||||
this._current = current;
|
|
||||||
this._lastKnown = current;
|
|
||||||
}
|
|
||||||
|
|
||||||
public get(): MediaLoadedInfo | null {
|
|
||||||
return this._current;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getLastKnown(): MediaLoadedInfo | null {
|
|
||||||
return this._lastKnown;
|
|
||||||
}
|
|
||||||
|
|
||||||
public clear(): void {
|
|
||||||
this._current = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public has(): boolean {
|
|
||||||
return !!this._current;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { MediaLayoutConfig } from '../types';
|
import { MediaLayoutConfig } from '../config/types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update element style from a media configuration.
|
* Update element style from a media configuration.
|
||||||
|
|||||||
+49
-24
@@ -1,10 +1,10 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
|
||||||
import { ViewContext } from 'view';
|
import { ViewContext } from 'view';
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
import { MediaQuery } from '../camera-manager/types';
|
import { MediaQuery } from '../camera-manager/types';
|
||||||
import { dispatchFrigateCardErrorEvent } from '../components/message';
|
import { dispatchFrigateCardErrorEvent } from '../components/message';
|
||||||
|
import { CardWideConfig, FrigateCardView } from '../config/types';
|
||||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const';
|
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const';
|
||||||
import { CardWideConfig, ClipsOrSnapshotsOrAll, FrigateCardView } from '../types';
|
import { ClipsOrSnapshotsOrAll } from '../types';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
import {
|
import {
|
||||||
EventMediaQueries,
|
EventMediaQueries,
|
||||||
@@ -20,7 +20,6 @@ type ResultSelectType = 'latest' | 'time' | 'none';
|
|||||||
|
|
||||||
export const changeViewToRecentEventsForCameraAndDependents = async (
|
export const changeViewToRecentEventsForCameraAndDependents = async (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
hass: HomeAssistant,
|
|
||||||
cameraManager: CameraManager,
|
cameraManager: CameraManager,
|
||||||
cardWideConfig: CardWideConfig,
|
cardWideConfig: CardWideConfig,
|
||||||
view: View,
|
view: View,
|
||||||
@@ -46,10 +45,16 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
(
|
(
|
||||||
await executeMediaQueryForView(element, hass, cameraManager, view, queries, {
|
await executeMediaQueryForViewWithErrorDispatching(
|
||||||
targetView: options?.targetView,
|
element,
|
||||||
select: options?.select,
|
cameraManager,
|
||||||
})
|
view,
|
||||||
|
queries,
|
||||||
|
{
|
||||||
|
targetView: options?.targetView,
|
||||||
|
select: options?.select,
|
||||||
|
},
|
||||||
|
)
|
||||||
)?.dispatchChangeEvent(element);
|
)?.dispatchChangeEvent(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -82,7 +87,6 @@ const createQueriesForEventsView = (
|
|||||||
*/
|
*/
|
||||||
export const changeViewToRecentRecordingForCameraAndDependents = async (
|
export const changeViewToRecentRecordingForCameraAndDependents = async (
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
hass: HomeAssistant,
|
|
||||||
cameraManager: CameraManager,
|
cameraManager: CameraManager,
|
||||||
cardWideConfig: CardWideConfig,
|
cardWideConfig: CardWideConfig,
|
||||||
view: View,
|
view: View,
|
||||||
@@ -109,10 +113,16 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
|
|||||||
}
|
}
|
||||||
|
|
||||||
(
|
(
|
||||||
await executeMediaQueryForView(element, hass, cameraManager, view, queries, {
|
await executeMediaQueryForViewWithErrorDispatching(
|
||||||
targetView: options?.targetView,
|
element,
|
||||||
select: options?.select,
|
cameraManager,
|
||||||
})
|
view,
|
||||||
|
queries,
|
||||||
|
{
|
||||||
|
targetView: options?.targetView,
|
||||||
|
select: options?.select,
|
||||||
|
},
|
||||||
|
)
|
||||||
)?.dispatchChangeEvent(element);
|
)?.dispatchChangeEvent(element);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -130,8 +140,6 @@ const createQueriesForRecordingsView = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const executeMediaQueryForView = async (
|
export const executeMediaQueryForView = async (
|
||||||
element: HTMLElement,
|
|
||||||
hass: HomeAssistant,
|
|
||||||
cameraManager: CameraManager,
|
cameraManager: CameraManager,
|
||||||
view: View,
|
view: View,
|
||||||
query: MediaQueries,
|
query: MediaQueries,
|
||||||
@@ -142,21 +150,12 @@ export const executeMediaQueryForView = async (
|
|||||||
select?: ResultSelectType;
|
select?: ResultSelectType;
|
||||||
},
|
},
|
||||||
): Promise<View | null> => {
|
): Promise<View | null> => {
|
||||||
let mediaArray: ViewMedia[] | null;
|
|
||||||
|
|
||||||
const queries = query.getQueries();
|
const queries = query.getQueries();
|
||||||
if (!queries) {
|
if (!queries) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
const mediaArray = await cameraManager.executeMediaQueries<MediaQuery>(queries);
|
||||||
mediaArray = await cameraManager.executeMediaQueries<MediaQuery>(hass, queries);
|
|
||||||
} catch (e) {
|
|
||||||
errorToConsole(e as Error);
|
|
||||||
dispatchFrigateCardErrorEvent(element, e as Error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!mediaArray) {
|
if (!mediaArray) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -186,6 +185,32 @@ export const executeMediaQueryForView = async (
|
|||||||
.mergeInContext(viewerContext);
|
.mergeInContext(viewerContext);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const executeMediaQueryForViewWithErrorDispatching = async (
|
||||||
|
element: HTMLElement,
|
||||||
|
cameraManager: CameraManager,
|
||||||
|
view: View,
|
||||||
|
query: MediaQueries,
|
||||||
|
options?: {
|
||||||
|
targetCameraID?: string;
|
||||||
|
targetView?: FrigateCardView;
|
||||||
|
targetTime?: Date;
|
||||||
|
select?: ResultSelectType;
|
||||||
|
},
|
||||||
|
): Promise<View | null> => {
|
||||||
|
try {
|
||||||
|
return await executeMediaQueryForView(cameraManager, view, query, {
|
||||||
|
targetCameraID: options?.targetCameraID,
|
||||||
|
targetView: options?.targetView,
|
||||||
|
targetTime: options?.targetTime,
|
||||||
|
select: options?.select,
|
||||||
|
});
|
||||||
|
} catch (e: unknown) {
|
||||||
|
errorToConsole(e as Error);
|
||||||
|
dispatchFrigateCardErrorEvent(element, e as Error);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find the longest matching media object that contains a given targetTime.
|
* Find the longest matching media object that contains a given targetTime.
|
||||||
* Longest is chosen to give the most stability to the media viewer.
|
* Longest is chosen to give the most stability to the media viewer.
|
||||||
|
|||||||
@@ -1,34 +1,44 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { StyleInfo } from 'lit/directives/style-map';
|
import { StyleInfo } from 'lit/directives/style-map';
|
||||||
import screenfull from 'screenfull';
|
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
|
import {
|
||||||
|
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
||||||
|
FrigateCardConfig,
|
||||||
|
FrigateCardCustomAction,
|
||||||
|
MenuItem
|
||||||
|
} from '../config/types';
|
||||||
import { FRIGATE_BUTTON_MENU_ICON } from '../const';
|
import { FRIGATE_BUTTON_MENU_ICON } from '../const';
|
||||||
import { localize } from '../localize/localize.js';
|
import { localize } from '../localize/localize.js';
|
||||||
import {
|
import {
|
||||||
FrigateCardConfig,
|
|
||||||
FrigateCardCustomAction,
|
|
||||||
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
|
||||||
MediaLoadedInfo,
|
MediaLoadedInfo,
|
||||||
MenuButton,
|
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
import { createFrigateCardCustomAction } from './action';
|
import { createFrigateCardCustomAction } from './action';
|
||||||
import { getAllDependentCameras } from './camera';
|
import { getAllDependentCameras } from './camera';
|
||||||
|
import { MediaPlayerManager } from '../card-controller/media-player-manager';
|
||||||
|
import { MicrophoneManager } from '../card-controller/microphone-manager';
|
||||||
import { getEntityIcon, getEntityTitle } from './ha';
|
import { getEntityIcon, getEntityTitle } from './ha';
|
||||||
import { MicrophoneController } from './microphone';
|
|
||||||
import { hasSubstream } from './substream';
|
import { hasSubstream } from './substream';
|
||||||
|
export interface MenuButtonControllerOptions {
|
||||||
|
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
||||||
|
showCameraUIButton?: boolean;
|
||||||
|
inFullscreenMode?: boolean;
|
||||||
|
inExpandedMode?: boolean;
|
||||||
|
microphoneManager?: MicrophoneManager | null;
|
||||||
|
mediaPlayerController?: MediaPlayerManager | null;
|
||||||
|
}
|
||||||
|
|
||||||
export class MenuButtonController {
|
export class MenuButtonController {
|
||||||
// Array of dynamic menu buttons to be added to menu.
|
// Array of dynamic menu buttons to be added to menu.
|
||||||
protected _dynamicMenuButtons: MenuButton[] = [];
|
protected _dynamicMenuButtons: MenuItem[] = [];
|
||||||
|
|
||||||
public addDynamicMenuButton(button: MenuButton): void {
|
public addDynamicMenuButton(button: MenuItem): void {
|
||||||
if (!this._dynamicMenuButtons.includes(button)) {
|
if (!this._dynamicMenuButtons.includes(button)) {
|
||||||
this._dynamicMenuButtons.push(button);
|
this._dynamicMenuButtons.push(button);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public removeDynamicMenuButton(button: MenuButton): void {
|
public removeDynamicMenuButton(button: MenuItem): void {
|
||||||
this._dynamicMenuButtons = this._dynamicMenuButtons.filter(
|
this._dynamicMenuButtons = this._dynamicMenuButtons.filter(
|
||||||
(existingButton) => existingButton != button,
|
(existingButton) => existingButton != button,
|
||||||
);
|
);
|
||||||
@@ -43,14 +53,8 @@ export class MenuButtonController {
|
|||||||
config: FrigateCardConfig,
|
config: FrigateCardConfig,
|
||||||
cameraManager: CameraManager,
|
cameraManager: CameraManager,
|
||||||
view: View,
|
view: View,
|
||||||
expanded: boolean,
|
options?: MenuButtonControllerOptions,
|
||||||
options?: {
|
): MenuItem[] {
|
||||||
currentMediaLoadedInfo?: MediaLoadedInfo | null;
|
|
||||||
mediaPlayers?: string[];
|
|
||||||
cameraURL?: string | null;
|
|
||||||
microphoneController?: MicrophoneController;
|
|
||||||
},
|
|
||||||
): MenuButton[] {
|
|
||||||
const visibleCameras = cameraManager.getStore().getVisibleCameras();
|
const visibleCameras = cameraManager.getStore().getVisibleCameras();
|
||||||
const selectedCameraID = view.camera;
|
const selectedCameraID = view.camera;
|
||||||
const selectedCameraConfig = cameraManager
|
const selectedCameraConfig = cameraManager
|
||||||
@@ -65,7 +69,7 @@ export class MenuButtonController {
|
|||||||
? cameraManager?.getMediaCapabilities(selectedMedia)
|
? cameraManager?.getMediaCapabilities(selectedMedia)
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const buttons: MenuButton[] = [];
|
const buttons: MenuItem[] = [];
|
||||||
buttons.push({
|
buttons.push({
|
||||||
// Use a magic icon value that the menu will use to render the custom
|
// Use a magic icon value that the menu will use to render the custom
|
||||||
// Frigate icon.
|
// Frigate icon.
|
||||||
@@ -87,7 +91,7 @@ export class MenuButtonController {
|
|||||||
const action = createFrigateCardCustomAction('camera_select', {
|
const action = createFrigateCardCustomAction('camera_select', {
|
||||||
camera: cameraID,
|
camera: cameraID,
|
||||||
});
|
});
|
||||||
const metadata = cameraManager.getCameraMetadata(hass, cameraID) ?? undefined;
|
const metadata = cameraManager.getCameraMetadata(cameraID) ?? undefined;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -132,7 +136,7 @@ export class MenuButtonController {
|
|||||||
const action = createFrigateCardCustomAction('live_substream_select', {
|
const action = createFrigateCardCustomAction('live_substream_select', {
|
||||||
camera: cameraID,
|
camera: cameraID,
|
||||||
});
|
});
|
||||||
const metadata = cameraManager.getCameraMetadata(hass, cameraID) ?? undefined;
|
const metadata = cameraManager.getCameraMetadata(cameraID) ?? undefined;
|
||||||
const cameraConfig = cameraManager.getStore().getCameraConfig(cameraID);
|
const cameraConfig = cameraManager.getStore().getCameraConfig(cameraID);
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
@@ -244,7 +248,7 @@ export class MenuButtonController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options?.cameraURL) {
|
if (options?.showCameraUIButton) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'mdi:web',
|
icon: 'mdi:web',
|
||||||
...config.menu.buttons.camera_ui,
|
...config.menu.buttons.camera_ui,
|
||||||
@@ -257,11 +261,11 @@ export class MenuButtonController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
options?.microphoneController &&
|
options?.microphoneManager &&
|
||||||
options?.currentMediaLoadedInfo?.capabilities?.supports2WayAudio
|
options?.currentMediaLoadedInfo?.capabilities?.supports2WayAudio
|
||||||
) {
|
) {
|
||||||
const forbidden = options.microphoneController.isForbidden();
|
const forbidden = options.microphoneManager.isForbidden();
|
||||||
const muted = options.microphoneController.isMuted();
|
const muted = options.microphoneManager.isMuted();
|
||||||
const buttonType = config.menu.buttons.microphone.type;
|
const buttonType = config.menu.buttons.microphone.type;
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: forbidden
|
icon: forbidden
|
||||||
@@ -285,7 +289,7 @@ export class MenuButtonController {
|
|||||||
...(!forbidden &&
|
...(!forbidden &&
|
||||||
buttonType === 'toggle' && {
|
buttonType === 'toggle' && {
|
||||||
tap_action: createFrigateCardCustomAction(
|
tap_action: createFrigateCardCustomAction(
|
||||||
options.microphoneController.isMuted()
|
options.microphoneManager.isMuted()
|
||||||
? 'microphone_unmute'
|
? 'microphone_unmute'
|
||||||
: 'microphone_mute',
|
: 'microphone_mute',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
@@ -293,57 +297,59 @@ export class MenuButtonController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (screenfull.isEnabled && !this._isBeingCasted()) {
|
if (!this._isBeingCasted()) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: screenfull.isFullscreen ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
|
icon: options?.inFullscreenMode ? 'mdi:fullscreen-exit' : 'mdi:fullscreen',
|
||||||
...config.menu.buttons.fullscreen,
|
...config.menu.buttons.fullscreen,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.fullscreen'),
|
title: localize('config.menu.buttons.fullscreen'),
|
||||||
tap_action: createFrigateCardCustomAction(
|
tap_action: createFrigateCardCustomAction(
|
||||||
'fullscreen',
|
'fullscreen',
|
||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
style: screenfull.isFullscreen ? this._getEmphasizedStyle() : {},
|
style: options?.inFullscreenMode ? this._getEmphasizedStyle() : {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: expanded ? 'mdi:arrow-collapse-all' : 'mdi:arrow-expand-all',
|
icon: options?.inExpandedMode ? 'mdi:arrow-collapse-all' : 'mdi:arrow-expand-all',
|
||||||
...config.menu.buttons.expand,
|
...config.menu.buttons.expand,
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
title: localize('config.menu.buttons.expand'),
|
title: localize('config.menu.buttons.expand'),
|
||||||
tap_action: createFrigateCardCustomAction('expand') as FrigateCardCustomAction,
|
tap_action: createFrigateCardCustomAction('expand') as FrigateCardCustomAction,
|
||||||
style: expanded ? this._getEmphasizedStyle() : {},
|
style: options?.inExpandedMode ? this._getEmphasizedStyle() : {},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
if (
|
||||||
options?.mediaPlayers?.length &&
|
options?.mediaPlayerController?.hasMediaPlayers() &&
|
||||||
(view?.isViewerView() || (view.is('live') && selectedCameraConfig?.camera_entity))
|
(view?.isViewerView() || (view.is('live') && selectedCameraConfig?.camera_entity))
|
||||||
) {
|
) {
|
||||||
const mediaPlayerItems = options.mediaPlayers.map((playerEntityID) => {
|
const mediaPlayerItems = options.mediaPlayerController
|
||||||
const title = getEntityTitle(hass, playerEntityID) || playerEntityID;
|
.getMediaPlayers()
|
||||||
const state = hass.states[playerEntityID];
|
.map((playerEntityID) => {
|
||||||
const playAction = createFrigateCardCustomAction('media_player', {
|
const title = getEntityTitle(hass, playerEntityID) || playerEntityID;
|
||||||
media_player: playerEntityID,
|
const state = hass.states[playerEntityID];
|
||||||
media_player_action: 'play',
|
const playAction = createFrigateCardCustomAction('media_player', {
|
||||||
});
|
media_player: playerEntityID,
|
||||||
const stopAction = createFrigateCardCustomAction('media_player', {
|
media_player_action: 'play',
|
||||||
media_player: playerEntityID,
|
});
|
||||||
media_player_action: 'stop',
|
const stopAction = createFrigateCardCustomAction('media_player', {
|
||||||
});
|
media_player: playerEntityID,
|
||||||
const disabled = !state || state.state === 'unavailable';
|
media_player_action: 'stop',
|
||||||
|
});
|
||||||
|
const disabled = !state || state.state === 'unavailable';
|
||||||
|
|
||||||
return {
|
return {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
selected: false,
|
selected: false,
|
||||||
icon: getEntityIcon(hass, playerEntityID),
|
icon: getEntityIcon(hass, playerEntityID),
|
||||||
entity: playerEntityID,
|
entity: playerEntityID,
|
||||||
state_color: false,
|
state_color: false,
|
||||||
title: title,
|
title: title,
|
||||||
disabled: disabled,
|
disabled: disabled,
|
||||||
...(!disabled && playAction && { tap_action: playAction }),
|
...(!disabled && playAction && { tap_action: playAction }),
|
||||||
...(!disabled && stopAction && { hold_action: stopAction }),
|
...(!disabled && stopAction && { hold_action: stopAction }),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'mdi:cast',
|
icon: 'mdi:cast',
|
||||||
@@ -414,7 +420,7 @@ export class MenuButtonController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const styledDynamicButtons = this._dynamicMenuButtons.map((button) => ({
|
const styledDynamicButtons = this._dynamicMenuButtons.map((button) => ({
|
||||||
style: this._getStyleFromActions(config, view, button),
|
style: this._getStyleFromActions(config, view, button, options),
|
||||||
...button,
|
...button,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -446,7 +452,8 @@ export class MenuButtonController {
|
|||||||
protected _getStyleFromActions(
|
protected _getStyleFromActions(
|
||||||
config: FrigateCardConfig,
|
config: FrigateCardConfig,
|
||||||
view: View,
|
view: View,
|
||||||
button: MenuButton,
|
button: MenuItem,
|
||||||
|
options?: MenuButtonControllerOptions,
|
||||||
): StyleInfo {
|
): StyleInfo {
|
||||||
for (const actionSet of [
|
for (const actionSet of [
|
||||||
button.tap_action,
|
button.tap_action,
|
||||||
@@ -476,8 +483,7 @@ export class MenuButtonController {
|
|||||||
(frigateCardAction.frigate_card_action === 'default' &&
|
(frigateCardAction.frigate_card_action === 'default' &&
|
||||||
view.is(config.view.default)) ||
|
view.is(config.view.default)) ||
|
||||||
(frigateCardAction.frigate_card_action === 'fullscreen' &&
|
(frigateCardAction.frigate_card_action === 'fullscreen' &&
|
||||||
screenfull.isEnabled &&
|
!!options?.inFullscreenMode) ||
|
||||||
screenfull.isFullscreen) ||
|
|
||||||
(frigateCardAction.frigate_card_action === 'camera_select' &&
|
(frigateCardAction.frigate_card_action === 'camera_select' &&
|
||||||
view.camera === frigateCardAction.camera)
|
view.camera === frigateCardAction.camera)
|
||||||
) {
|
) {
|
||||||
|
|||||||
@@ -1,60 +0,0 @@
|
|||||||
import { FrigateCardCustomAction } from '../types';
|
|
||||||
import { createFrigateCardCustomAction } from './action.js';
|
|
||||||
|
|
||||||
export const getActionsFromQueryString = (
|
|
||||||
queryString: string,
|
|
||||||
): FrigateCardCustomAction[] => {
|
|
||||||
const params = new URLSearchParams(queryString);
|
|
||||||
const actions: FrigateCardCustomAction[] = [];
|
|
||||||
const actionRE = new RegExp(
|
|
||||||
/^frigate-card-action([.:](?<cardID>\w+))?[.:](?<action>\w+)/,
|
|
||||||
);
|
|
||||||
for (const [key, value] of params.entries()) {
|
|
||||||
const match = key.match(actionRE);
|
|
||||||
if (!match || !match.groups) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const cardID: string | undefined = match.groups['cardID'];
|
|
||||||
const action = match.groups['action'];
|
|
||||||
|
|
||||||
let customAction: FrigateCardCustomAction | null = null;
|
|
||||||
switch (action) {
|
|
||||||
case 'camera_select':
|
|
||||||
case 'live_substream_select':
|
|
||||||
if (value) {
|
|
||||||
customAction = createFrigateCardCustomAction(action, {
|
|
||||||
camera: value,
|
|
||||||
cardID: cardID,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'camera_ui':
|
|
||||||
case 'clip':
|
|
||||||
case 'clips':
|
|
||||||
case 'default':
|
|
||||||
case 'diagnostics':
|
|
||||||
case 'download':
|
|
||||||
case 'expand':
|
|
||||||
case 'image':
|
|
||||||
case 'live':
|
|
||||||
case 'menu_toggle':
|
|
||||||
case 'recording':
|
|
||||||
case 'recordings':
|
|
||||||
case 'snapshot':
|
|
||||||
case 'snapshots':
|
|
||||||
case 'timeline':
|
|
||||||
customAction = createFrigateCardCustomAction(action, {
|
|
||||||
cardID: cardID,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn(
|
|
||||||
`Frigate card received unknown card action in query string: ${action}`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (customAction) {
|
|
||||||
actions.push(customAction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return actions;
|
|
||||||
};
|
|
||||||
@@ -14,7 +14,7 @@ export const screenshotMedia = (video: HTMLVideoElement): string | null => {
|
|||||||
return canvas.toDataURL('image/jpeg');
|
return canvas.toDataURL('image/jpeg');
|
||||||
};
|
};
|
||||||
|
|
||||||
export const generateScreenshotTitle = (view?: View): string => {
|
export const generateScreenshotTitle = (view?: View | null): string => {
|
||||||
if (view?.is('live') || view?.is('image')) {
|
if (view?.is('live') || view?.is('image')) {
|
||||||
return `${view.view}-${view.camera}-${format(
|
return `${view.view}-${view.camera}-${format(
|
||||||
new Date(),
|
new Date(),
|
||||||
|
|||||||
+3
-42
@@ -1,48 +1,9 @@
|
|||||||
import { CameraManager } from '../camera-manager/manager';
|
|
||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
import { getAllDependentCameras } from './camera';
|
|
||||||
|
|
||||||
export const createViewWithSelectedSubstream = (
|
export const getStreamCameraID = (view: View): string => {
|
||||||
view: View,
|
return view?.context?.live?.overrides?.get(view.camera) ?? view.camera;
|
||||||
substreamID: string,
|
|
||||||
): View | null => {
|
|
||||||
const overrides: Map<string, string> = view.context?.live?.overrides ?? new Map();
|
|
||||||
overrides.set(view.camera, substreamID);
|
|
||||||
return view.clone().mergeInContext({
|
|
||||||
live: { overrides: overrides },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createViewWithoutSubstream = (view: View): View => {
|
|
||||||
const newView = view.clone();
|
|
||||||
const overrides: Map<string, string> | undefined = newView.context?.live?.overrides;
|
|
||||||
if (overrides && overrides.has(view.camera)) {
|
|
||||||
newView.context?.live?.overrides?.delete(view.camera);
|
|
||||||
}
|
|
||||||
return newView;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const hasSubstream = (view: View): boolean => {
|
export const hasSubstream = (view: View): boolean => {
|
||||||
const override = view?.context?.live?.overrides?.get(view.camera);
|
return getStreamCameraID(view) !== view.camera;
|
||||||
return !!override && override !== view.camera;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const createViewWithNextStream = (
|
|
||||||
cameraManager: CameraManager,
|
|
||||||
view: View,
|
|
||||||
): View => {
|
|
||||||
const dependencies = [...getAllDependentCameras(cameraManager, view.camera)];
|
|
||||||
if (dependencies.length <= 1) {
|
|
||||||
return view.clone();
|
|
||||||
}
|
|
||||||
|
|
||||||
const newView = view.clone();
|
|
||||||
const overrides: Map<string, string> = newView.context?.live?.overrides ?? new Map();
|
|
||||||
const currentOverride = overrides.get(newView.camera) ?? newView.camera;
|
|
||||||
const currentIndex = dependencies.indexOf(currentOverride);
|
|
||||||
const newIndex = currentIndex < 0 ? 0 : (currentIndex + 1) % dependencies.length;
|
|
||||||
overrides.set(view.camera, dependencies[newIndex]);
|
|
||||||
newView.mergeInContext({ live: { overrides: overrides } });
|
|
||||||
|
|
||||||
return newView;
|
|
||||||
};
|
};
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ import {
|
|||||||
dispatchFrigateCardErrorEvent,
|
dispatchFrigateCardErrorEvent,
|
||||||
renderProgressIndicator,
|
renderProgressIndicator,
|
||||||
} from '../components/message';
|
} from '../components/message';
|
||||||
import { CardWideConfig } from '../types';
|
import { CardWideConfig } from '../config/types';
|
||||||
import { errorToConsole } from './basic';
|
import { errorToConsole } from './basic';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,19 +1,18 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
|
||||||
import add from 'date-fns/add';
|
import add from 'date-fns/add';
|
||||||
import sub from 'date-fns/sub';
|
import sub from 'date-fns/sub';
|
||||||
import { DataSet } from 'vis-data';
|
import { DataSet } from 'vis-data';
|
||||||
import { IdType, TimelineItem, TimelineWindow } from 'vis-timeline/esnext';
|
import { IdType, TimelineItem, TimelineWindow } from 'vis-timeline/esnext';
|
||||||
import { ClipsOrSnapshotsOrAll } from '../types';
|
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
import { EventQuery, RecordingQuery, RecordingSegment } from '../camera-manager/types';
|
|
||||||
import { capEndDate, convertRangeToCacheFriendlyTimes } from '../camera-manager/util';
|
|
||||||
import { ViewMedia } from '../view/media';
|
|
||||||
import {
|
import {
|
||||||
compressRanges,
|
|
||||||
ExpiringMemoryRangeSet,
|
ExpiringMemoryRangeSet,
|
||||||
MemoryRangeSet,
|
MemoryRangeSet,
|
||||||
|
compressRanges,
|
||||||
} from '../camera-manager/range';
|
} from '../camera-manager/range';
|
||||||
import { errorToConsole, ModifyInterface } from './basic.js';
|
import { EventQuery, RecordingQuery, RecordingSegment } from '../camera-manager/types';
|
||||||
|
import { capEndDate, convertRangeToCacheFriendlyTimes } from '../camera-manager/util';
|
||||||
|
import { ClipsOrSnapshotsOrAll } from '../types';
|
||||||
|
import { ViewMedia } from '../view/media';
|
||||||
|
import { ModifyInterface, errorToConsole } from './basic.js';
|
||||||
|
|
||||||
// Allow timeline freshness to be at least this number of seconds out of date
|
// Allow timeline freshness to be at least this number of seconds out of date
|
||||||
// (caching times in the data-engine may increase the effective delay).
|
// (caching times in the data-engine may increase the effective delay).
|
||||||
@@ -82,11 +81,11 @@ export class TimelineDataSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async refresh(hass: HomeAssistant, window: TimelineWindow): Promise<void> {
|
public async refresh(window: TimelineWindow): Promise<void> {
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
this._refreshEvents(hass, window),
|
this._refreshEvents(window),
|
||||||
...(this._showRecordings ? [this._refreshRecordings(hass, window)] : []),
|
...(this._showRecordings ? [this._refreshRecordings(window)] : []),
|
||||||
]);
|
]);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
errorToConsole(e as Error);
|
errorToConsole(e as Error);
|
||||||
@@ -114,10 +113,7 @@ export class TimelineDataSource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _refreshEvents(
|
protected async _refreshEvents(window: TimelineWindow): Promise<void> {
|
||||||
hass: HomeAssistant,
|
|
||||||
window: TimelineWindow,
|
|
||||||
): Promise<void> {
|
|
||||||
if (
|
if (
|
||||||
this._eventRanges.hasCoverage({
|
this._eventRanges.hasCoverage({
|
||||||
start: window.start,
|
start: window.start,
|
||||||
@@ -134,7 +130,7 @@ export class TimelineDataSource {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mediaArray = await this._cameraManager.executeMediaQueries(hass, eventQueries);
|
const mediaArray = await this._cameraManager.executeMediaQueries(eventQueries);
|
||||||
const data: FrigateCardTimelineItem[] = [];
|
const data: FrigateCardTimelineItem[] = [];
|
||||||
for (const media of mediaArray ?? []) {
|
for (const media of mediaArray ?? []) {
|
||||||
const startTime = media.getStartTime();
|
const startTime = media.getStartTime();
|
||||||
@@ -159,10 +155,7 @@ export class TimelineDataSource {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _refreshRecordings(
|
protected async _refreshRecordings(window: TimelineWindow): Promise<void> {
|
||||||
hass: HomeAssistant,
|
|
||||||
window: TimelineWindow,
|
|
||||||
): Promise<void> {
|
|
||||||
type FrigateCardTimelineItemWithEnd = ModifyInterface<
|
type FrigateCardTimelineItemWithEnd = ModifyInterface<
|
||||||
FrigateCardTimelineItem,
|
FrigateCardTimelineItem,
|
||||||
{ end: number }
|
{ end: number }
|
||||||
@@ -228,10 +221,7 @@ export class TimelineDataSource {
|
|||||||
if (!recordingQueries) {
|
if (!recordingQueries) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const results = await this._cameraManager.getRecordingSegments(
|
const results = await this._cameraManager.getRecordingSegments(recordingQueries);
|
||||||
hass,
|
|
||||||
recordingQueries,
|
|
||||||
);
|
|
||||||
|
|
||||||
const newSegments: Map<string, RecordingSegment[]> = new Map();
|
const newSegments: Map<string, RecordingSegment[]> = new Map();
|
||||||
for (const [query, result] of results) {
|
for (const [query, result] of results) {
|
||||||
|
|||||||
+8
-1
@@ -1,9 +1,14 @@
|
|||||||
export class Timer {
|
export class Timer {
|
||||||
protected _timer: number | null = null;
|
protected _timer: number | null = null;
|
||||||
|
protected _repeated = false;
|
||||||
|
|
||||||
public stop(): void {
|
public stop(): void {
|
||||||
if (this._timer) {
|
if (this._timer) {
|
||||||
window.clearTimeout(this._timer);
|
if (this._repeated) {
|
||||||
|
window.clearInterval(this._timer);
|
||||||
|
} else {
|
||||||
|
window.clearTimeout(this._timer);
|
||||||
|
}
|
||||||
this._timer = null;
|
this._timer = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -18,6 +23,7 @@ export class Timer {
|
|||||||
this._timer = null;
|
this._timer = null;
|
||||||
func();
|
func();
|
||||||
}, seconds * 1000);
|
}, seconds * 1000);
|
||||||
|
this._repeated = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public startRepeated(seconds: number, func: () => void): void {
|
public startRepeated(seconds: number, func: () => void): void {
|
||||||
@@ -25,5 +31,6 @@ export class Timer {
|
|||||||
this._timer = window.setInterval(() => {
|
this._timer = window.setInterval(() => {
|
||||||
func();
|
func();
|
||||||
}, seconds * 1000);
|
}, seconds * 1000);
|
||||||
|
this._repeated = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-10
@@ -62,7 +62,7 @@ export function getParseErrorKeys<T>(error: z.ZodError<T>): string[] {
|
|||||||
* @param error The ZodError object from parsing.
|
* @param error The ZodError object from parsing.
|
||||||
* @returns An array of string error paths.
|
* @returns An array of string error paths.
|
||||||
*/
|
*/
|
||||||
export const getParseErrorPaths = <T>(error: z.ZodError<T>): Set<string> | null => {
|
export const getParseErrorPaths = <T>(error: z.ZodError<T>): Set<string> => {
|
||||||
/* Zod errors involving unions are complex, as Zod may not be able to tell
|
/* Zod errors involving unions are complex, as Zod may not be able to tell
|
||||||
* where the 'real' error is vs simply a union option not matching. This
|
* where the 'real' error is vs simply a union option not matching. This
|
||||||
* function finds all ZodError "issues" that don't have an error with 'type'
|
* function finds all ZodError "issues" that don't have an error with 'type'
|
||||||
@@ -79,16 +79,8 @@ export const getParseErrorPaths = <T>(error: z.ZodError<T>): Set<string> | null
|
|||||||
if (issue.code === 'invalid_union') {
|
if (issue.code === 'invalid_union') {
|
||||||
const unionErrors = (issue as z.ZodInvalidUnionIssue).unionErrors;
|
const unionErrors = (issue as z.ZodInvalidUnionIssue).unionErrors;
|
||||||
for (const unionError of unionErrors) {
|
for (const unionError of unionErrors) {
|
||||||
const nestedErrors = getParseErrorPaths(unionError);
|
getParseErrorPaths(unionError).forEach(contenders.add, contenders);
|
||||||
if (nestedErrors && nestedErrors.size) {
|
|
||||||
nestedErrors.forEach(contenders.add, contenders);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if (issue.code === 'invalid_type') {
|
|
||||||
if (issue.path[issue.path.length - 1] === 'type') {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
contenders.add(getParseErrorPathString(issue.path));
|
|
||||||
} else {
|
} else {
|
||||||
contenders.add(getParseErrorPathString(issue.path));
|
contenders.add(getParseErrorPathString(issue.path));
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-4
@@ -1,5 +1,6 @@
|
|||||||
import { ViewContext } from 'view';
|
import { ViewContext } from 'view';
|
||||||
import { ClipsOrSnapshots, FrigateCardView, ViewDisplayMode } from '../types.js';
|
import { FrigateCardView, ViewDisplayMode } from '../config/types.js';
|
||||||
|
import { ClipsOrSnapshots } from '../types.js';
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
import { MediaQueries } from './media-queries';
|
import { MediaQueries } from './media-queries';
|
||||||
import { MediaQueriesClassifier } from './media-queries-classifier.js';
|
import { MediaQueriesClassifier } from './media-queries-classifier.js';
|
||||||
@@ -43,7 +44,7 @@ export class View {
|
|||||||
* @param curr The current view.
|
* @param curr The current view.
|
||||||
* @returns True if the view change is a real media change.
|
* @returns True if the view change is a real media change.
|
||||||
*/
|
*/
|
||||||
public static isMajorMediaChange(prev?: View, curr?: View): boolean {
|
public static isMajorMediaChange(prev?: View | null, curr?: View): boolean {
|
||||||
return (
|
return (
|
||||||
!prev ||
|
!prev ||
|
||||||
!curr ||
|
!curr ||
|
||||||
@@ -61,7 +62,7 @@ export class View {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static adoptFromViewIfAppropriate(next: View, curr?: View): void {
|
public static adoptFromViewIfAppropriate(next: View, curr?: View | null): void {
|
||||||
if (!curr) {
|
if (!curr) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -229,7 +230,7 @@ export class View {
|
|||||||
* Determine if a view is for the media viewer.
|
* Determine if a view is for the media viewer.
|
||||||
*/
|
*/
|
||||||
public isViewerView(): boolean {
|
public isViewerView(): boolean {
|
||||||
return ['clip', 'snapshot', 'media', 'recording'].includes(this.view);
|
return ['media', 'clip', 'snapshot', 'recording'].includes(this.view);
|
||||||
}
|
}
|
||||||
|
|
||||||
public supportsMultipleDisplayModes(): boolean {
|
public supportsMultipleDisplayModes(): boolean {
|
||||||
|
|||||||
@@ -1,123 +0,0 @@
|
|||||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
||||||
import { mock } from 'vitest-mock-extended';
|
|
||||||
import { AutomationsController, AutomationsControllerError } from '../src/automations';
|
|
||||||
import { ConditionController } from '../src/conditions';
|
|
||||||
import { automationsSchema, FrigateCardError } from '../src/types';
|
|
||||||
import { frigateCardHandleAction } from '../src/utils/action.js';
|
|
||||||
import { createHASS } from './test-utils';
|
|
||||||
|
|
||||||
vi.mock('../src/utils/action.js');
|
|
||||||
|
|
||||||
describe('AutomationsController', () => {
|
|
||||||
const actions = [
|
|
||||||
{
|
|
||||||
action: 'custom:frigate-card-action',
|
|
||||||
frigate_card_action: 'clips',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
const conditions = { fullscreen: true };
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should do nothing without automations', () => {
|
|
||||||
const automationController = new AutomationsController(undefined);
|
|
||||||
automationController.execute(
|
|
||||||
mock<HTMLElement>(),
|
|
||||||
createHASS(),
|
|
||||||
new ConditionController(),
|
|
||||||
);
|
|
||||||
expect(frigateCardHandleAction).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should execute actions', () => {
|
|
||||||
const automations = automationsSchema.parse([
|
|
||||||
{
|
|
||||||
conditions: conditions,
|
|
||||||
actions: actions,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const automationController = new AutomationsController(automations);
|
|
||||||
const conditionController = new ConditionController();
|
|
||||||
const element = mock<HTMLElement>();
|
|
||||||
const hass = createHASS();
|
|
||||||
|
|
||||||
automationController.execute(element, hass, conditionController);
|
|
||||||
expect(frigateCardHandleAction).not.toBeCalled();
|
|
||||||
|
|
||||||
conditionController.setState({ fullscreen: true });
|
|
||||||
automationController.execute(element, hass, conditionController);
|
|
||||||
expect(frigateCardHandleAction).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
// Automation will not re-fire when condition continues to evaluate the
|
|
||||||
// same.
|
|
||||||
automationController.execute(element, hass, conditionController);
|
|
||||||
expect(frigateCardHandleAction).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
conditionController.setState({ fullscreen: false });
|
|
||||||
automationController.execute(element, hass, conditionController);
|
|
||||||
expect(frigateCardHandleAction).toBeCalledTimes(1);
|
|
||||||
|
|
||||||
conditionController.setState({ fullscreen: true });
|
|
||||||
automationController.execute(element, hass, conditionController);
|
|
||||||
expect(frigateCardHandleAction).toBeCalledTimes(2);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should execute actions_not', () => {
|
|
||||||
const automations = automationsSchema.parse([
|
|
||||||
{
|
|
||||||
conditions: conditions,
|
|
||||||
actions_not: actions,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const automationController = new AutomationsController(automations);
|
|
||||||
automationController.execute(
|
|
||||||
mock<HTMLElement>(),
|
|
||||||
createHASS(),
|
|
||||||
new ConditionController(),
|
|
||||||
);
|
|
||||||
expect(frigateCardHandleAction).toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should prevent automation loops', () => {
|
|
||||||
const automations = automationsSchema.parse([
|
|
||||||
{
|
|
||||||
conditions: { fullscreen: true },
|
|
||||||
actions: actions,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
conditions: { fullscreen: false },
|
|
||||||
actions: actions,
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
const automationController = new AutomationsController(automations);
|
|
||||||
const conditionController = new ConditionController();
|
|
||||||
const element = mock<HTMLElement>();
|
|
||||||
const hass = createHASS();
|
|
||||||
|
|
||||||
// Create a setup where one automation action causes another...
|
|
||||||
let fullscreen = true;
|
|
||||||
vi.mocked(frigateCardHandleAction).mockImplementation(() => {
|
|
||||||
fullscreen = !fullscreen;
|
|
||||||
conditionController.setState({ fullscreen: fullscreen });
|
|
||||||
automationController.execute(element, hass, conditionController);
|
|
||||||
});
|
|
||||||
|
|
||||||
conditionController.setState({ fullscreen: fullscreen });
|
|
||||||
|
|
||||||
expect(() =>
|
|
||||||
automationController.execute(element, hass, conditionController),
|
|
||||||
).toThrowError(/Too many nested automation calls/);
|
|
||||||
expect(frigateCardHandleAction).toBeCalledTimes(10);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should be able to construct error', () => {
|
|
||||||
const error = new AutomationsControllerError('message');
|
|
||||||
expect(error).toBeTruthy();
|
|
||||||
expect(error instanceof FrigateCardError).toBeTruthy();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -4,11 +4,16 @@ import { FrigateCameraManagerEngine } from '../../src/camera-manager/frigate/eng
|
|||||||
import { GenericCameraManagerEngine } from '../../src/camera-manager/generic/engine-generic';
|
import { GenericCameraManagerEngine } from '../../src/camera-manager/generic/engine-generic';
|
||||||
import { MotionEyeCameraManagerEngine } from '../../src/camera-manager/motioneye/engine-motioneye';
|
import { MotionEyeCameraManagerEngine } from '../../src/camera-manager/motioneye/engine-motioneye';
|
||||||
import { Engine } from '../../src/camera-manager/types.js';
|
import { Engine } from '../../src/camera-manager/types.js';
|
||||||
import { CardWideConfig } from '../../src/types.js';
|
import { CardWideConfig } from '../../src/config/types.js';
|
||||||
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry';
|
import { EntityRegistryManager } from '../../src/utils/ha/entity-registry';
|
||||||
import { EntityCache } from '../../src/utils/ha/entity-registry/cache';
|
import { EntityCache } from '../../src/utils/ha/entity-registry/cache';
|
||||||
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media';
|
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media';
|
||||||
import { createCameraConfig, createHASS, createRegistryEntity } from '../test-utils';
|
import {
|
||||||
|
createCameraConfig,
|
||||||
|
createHASS,
|
||||||
|
createRegistryEntity,
|
||||||
|
createStateEntity,
|
||||||
|
} from '../test-utils';
|
||||||
|
|
||||||
vi.mock('../../src/utils/ha/entity-registry');
|
vi.mock('../../src/utils/ha/entity-registry');
|
||||||
vi.mock('../../src/utils/ha/entity-registry/cache');
|
vi.mock('../../src/utils/ha/entity-registry/cache');
|
||||||
@@ -21,7 +26,6 @@ const createFactory = (options?: {
|
|||||||
return new CameraManagerEngineFactory(
|
return new CameraManagerEngineFactory(
|
||||||
options?.entityRegistryManager ?? new EntityRegistryManager(new EntityCache()),
|
options?.entityRegistryManager ?? new EntityRegistryManager(new EntityCache()),
|
||||||
options?.resolvedMediaCache ?? new ResolvedMediaCache(),
|
options?.resolvedMediaCache ?? new ResolvedMediaCache(),
|
||||||
options?.cardWideConfig ?? {},
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,18 +135,7 @@ describe('CameraManagerEngineFactory.getEngineForCamera()', () => {
|
|||||||
entityRegistryManager: entityRegistryManager,
|
entityRegistryManager: entityRegistryManager,
|
||||||
}).getEngineForCamera(
|
}).getEngineForCamera(
|
||||||
createHASS({
|
createHASS({
|
||||||
'camera.foo': {
|
'camera.foo': createStateEntity(),
|
||||||
entity_id: 'camera.foo',
|
|
||||||
state: 'streaming',
|
|
||||||
last_changed: 'bar',
|
|
||||||
last_updated: 'baz',
|
|
||||||
attributes: {},
|
|
||||||
context: {
|
|
||||||
id: 'context',
|
|
||||||
user_id: null,
|
|
||||||
parent_id: null,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
config,
|
config,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ import {
|
|||||||
FrigateRecordingViewMedia,
|
FrigateRecordingViewMedia,
|
||||||
} from '../../../src/camera-manager/frigate/media';
|
} from '../../../src/camera-manager/frigate/media';
|
||||||
import { FrigateEvent, eventSchema } from '../../../src/camera-manager/frigate/types.js';
|
import { FrigateEvent, eventSchema } from '../../../src/camera-manager/frigate/types.js';
|
||||||
import { CameraConfig, RawFrigateCardConfig } from '../../../src/types';
|
import { CameraConfig, RawFrigateCardConfig } from '../../../src/config/types';
|
||||||
import { ViewMedia } from '../../../src/view/media';
|
import { ViewMedia } from '../../../src/view/media';
|
||||||
import { createCameraConfig, createHASS } from '../../test-utils';
|
import { createCameraConfig, createHASS } from '../../test-utils';
|
||||||
|
|
||||||
const createEngine = (): FrigateCameraManagerEngine => {
|
const createEngine = (): FrigateCameraManagerEngine => {
|
||||||
return new FrigateCameraManagerEngine(
|
return new FrigateCameraManagerEngine(
|
||||||
{},
|
|
||||||
new RecordingSegmentsCache(),
|
new RecordingSegmentsCache(),
|
||||||
new RequestCache(),
|
new RequestCache(),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
getRecordingMediaContentID,
|
getRecordingMediaContentID,
|
||||||
getRecordingTitle,
|
getRecordingTitle,
|
||||||
} from '../../../src/camera-manager/frigate/util';
|
} from '../../../src/camera-manager/frigate/util';
|
||||||
import { CameraConfig } from '../../../src/types';
|
import { CameraConfig } from '../../../src/config/types';
|
||||||
import {
|
import {
|
||||||
createCameraConfig,
|
createCameraConfig,
|
||||||
createFrigateEvent,
|
createFrigateEvent,
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
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 { EntityRegistryManager } from '../../src/utils/ha/entity-registry/index.js';
|
||||||
|
import { ResolvedMediaCache } from '../../src/utils/ha/resolved-media.js';
|
||||||
|
import { TestViewMedia, createCameraConfig } from '../test-utils.js';
|
||||||
|
|
||||||
|
describe('CameraManagerStore', async () => {
|
||||||
|
const config_visible = createCameraConfig();
|
||||||
|
const config_hidden = createCameraConfig({
|
||||||
|
hide: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const engineFactory = new CameraManagerEngineFactory(
|
||||||
|
mock<EntityRegistryManager>(),
|
||||||
|
mock<ResolvedMediaCache>(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const engineGeneric = await engineFactory.createEngine(Engine.Generic);
|
||||||
|
const engineFrigate = await engineFactory.createEngine(Engine.Frigate);
|
||||||
|
|
||||||
|
const setupStore = async (): Promise<CameraManagerStore> => {
|
||||||
|
const store = new CameraManagerStore();
|
||||||
|
store.addCamera('camera-visible', config_visible, engineGeneric);
|
||||||
|
store.addCamera('camera-hidden', config_hidden, engineFrigate);
|
||||||
|
return store;
|
||||||
|
};
|
||||||
|
|
||||||
|
it('getCameraConfig', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
expect(store.getCameraConfig('camera-visible')).toBe(config_visible);
|
||||||
|
expect(store.getCameraConfig('camera-hidden')).toBe(config_hidden);
|
||||||
|
expect(store.getCameraConfig('camera-not-exist')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hasCameraID', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
expect(store.hasCameraID('camera-visible')).toBeTruthy();
|
||||||
|
expect(store.hasCameraID('camera-hidden')).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('hasVisibleCameraID', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
expect(store.hasVisibleCameraID('camera-visible')).toBeTruthy();
|
||||||
|
expect(store.hasVisibleCameraID('camera-hidden')).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getCameraCount', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
expect(store.getCameraCount()).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getVisibleCameraCount', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
expect(store.getVisibleCameraCount()).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getCameras', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
expect(store.getCameras()).toEqual(
|
||||||
|
new Map([
|
||||||
|
['camera-visible', config_visible],
|
||||||
|
['camera-hidden', config_hidden],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getVisibleCameras', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
expect(store.getVisibleCameras()).toEqual(
|
||||||
|
new Map([['camera-visible', config_visible]]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getCameraIDs', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
expect(store.getCameraIDs()).toEqual(new Set(['camera-visible', 'camera-hidden']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getVisibleCameraIDs', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
expect(store.getVisibleCameraIDs()).toEqual(new Set(['camera-visible']));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('reset', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
|
||||||
|
store.reset();
|
||||||
|
|
||||||
|
expect(store.getCameraCount()).toBe(0);
|
||||||
|
expect(store.getVisibleCameraCount()).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getCameraConfigForMedia', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
|
||||||
|
const media_1 = new TestViewMedia({ cameraID: 'camera-visible' });
|
||||||
|
expect(store.getCameraConfigForMedia(media_1)).toBe(config_visible);
|
||||||
|
|
||||||
|
const media_2 = new TestViewMedia({ cameraID: 'camera-not-exist' });
|
||||||
|
expect(store.getCameraConfigForMedia(media_2)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getEngineOfType', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
expect(store.getEngineOfType(Engine.Generic)).toBe(engineGeneric);
|
||||||
|
expect(store.getEngineOfType(Engine.Frigate)).toBe(engineFrigate);
|
||||||
|
expect(store.getEngineOfType(Engine.MotionEye)).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getEngineForCameraID', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
expect(store.getEngineForCameraID('camera-visible')).toBe(engineGeneric);
|
||||||
|
expect(store.getEngineForCameraID('camera-hidden')).toBe(engineFrigate);
|
||||||
|
expect(store.getEngineForCameraID('camera-not-exist')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getEnginesForCameraIDs', async () => {
|
||||||
|
it('empty input', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
expect(store.getEnginesForCameraIDs(new Set())).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('multiple cameras', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
store.addCamera('camera-visible2', config_visible, engineGeneric);
|
||||||
|
expect(
|
||||||
|
store.getEnginesForCameraIDs(
|
||||||
|
new Set([
|
||||||
|
'camera-visible',
|
||||||
|
'camera-visible2',
|
||||||
|
'camera-hidden',
|
||||||
|
'camera-not-exist',
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
).toEqual(
|
||||||
|
new Map([
|
||||||
|
[engineGeneric, new Set(['camera-visible', 'camera-visible2'])],
|
||||||
|
[engineFrigate, new Set(['camera-hidden'])],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getEngineForMedia', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
const media = new TestViewMedia({ cameraID: 'camera-visible' });
|
||||||
|
expect(store.getEngineForMedia(media)).toBe(engineGeneric);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('getAllEngines', async () => {
|
||||||
|
const store = await setupStore();
|
||||||
|
expect(store.getAllEngines()).toEqual([engineGeneric, engineFrigate]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -5,8 +5,7 @@ import {
|
|||||||
getCameraEntityFromConfig,
|
getCameraEntityFromConfig,
|
||||||
sortMedia,
|
sortMedia,
|
||||||
} from '../../src/camera-manager/util.js';
|
} from '../../src/camera-manager/util.js';
|
||||||
import { CameraConfig, cameraConfigSchema } from '../../src/types.js';
|
import { CameraConfig, cameraConfigSchema } from '../../src/config/types.js';
|
||||||
import { ViewMedia, ViewMediaType } from '../../src/view/media.js';
|
|
||||||
import { TestViewMedia } from '../test-utils.js';
|
import { TestViewMedia } from '../test-utils.js';
|
||||||
|
|
||||||
describe('convertRangeToCacheFriendlyTimes', () => {
|
describe('convertRangeToCacheFriendlyTimes', () => {
|
||||||
|
|||||||
@@ -0,0 +1,728 @@
|
|||||||
|
import { afterAll, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { mock } from 'vitest-mock-extended';
|
||||||
|
import {
|
||||||
|
ActionType,
|
||||||
|
FrigateCardCustomAction,
|
||||||
|
FrigateCardView,
|
||||||
|
frigateCardCustomActionSchema,
|
||||||
|
} from '../../src/config/types';
|
||||||
|
import { FrigateCardMediaPlayer } from '../../src/types';
|
||||||
|
import {
|
||||||
|
convertActionToFrigateCardCustomAction,
|
||||||
|
frigateCardHandleActionConfig,
|
||||||
|
getActionConfigGivenAction,
|
||||||
|
} from '../../src/utils/action.js';
|
||||||
|
import { ActionsManager } from '../../src/card-controller/actions-manager';
|
||||||
|
import {
|
||||||
|
createCardAPI,
|
||||||
|
createConfig,
|
||||||
|
createHASS,
|
||||||
|
createMediaLoadedInfo,
|
||||||
|
createView,
|
||||||
|
createViewWithMedia,
|
||||||
|
} from '../test-utils';
|
||||||
|
|
||||||
|
vi.mock('../../src/utils/action.js');
|
||||||
|
vi.mock('../../src/camera-manager/manager.js');
|
||||||
|
|
||||||
|
const createAction = (
|
||||||
|
action: Record<string, unknown>,
|
||||||
|
): FrigateCardCustomAction | null => {
|
||||||
|
const result = frigateCardCustomActionSchema.safeParse({
|
||||||
|
action: 'custom:frigate-card-action',
|
||||||
|
...action,
|
||||||
|
});
|
||||||
|
return result.success ? result.data : null;
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('ActionsManager.getMergedActions', () => {
|
||||||
|
const config = {
|
||||||
|
view: {
|
||||||
|
actions: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'navigate',
|
||||||
|
navigation_path: '1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
live: {
|
||||||
|
actions: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'navigate',
|
||||||
|
navigation_path: '2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
media_gallery: {
|
||||||
|
actions: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'navigate',
|
||||||
|
navigation_path: '3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
media_viewer: {
|
||||||
|
actions: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'navigate',
|
||||||
|
navigation_path: '4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
image: {
|
||||||
|
actions: {
|
||||||
|
tap_action: {
|
||||||
|
action: 'navigate',
|
||||||
|
navigation_path: '5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should get no merged actions with a message', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({ view: 'live' }),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true);
|
||||||
|
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
expect(manager.getMergedActions()).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should get merged actions with live view', () => {
|
||||||
|
it.each([
|
||||||
|
[
|
||||||
|
'live' as const,
|
||||||
|
{
|
||||||
|
tap_action: {
|
||||||
|
action: 'navigate',
|
||||||
|
navigation_path: '2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'clips' as const,
|
||||||
|
{
|
||||||
|
tap_action: {
|
||||||
|
action: 'navigate',
|
||||||
|
navigation_path: '3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'clip' as const,
|
||||||
|
{
|
||||||
|
tap_action: {
|
||||||
|
action: 'navigate',
|
||||||
|
navigation_path: '4',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'image' as const,
|
||||||
|
{
|
||||||
|
tap_action: {
|
||||||
|
action: 'navigate',
|
||||||
|
navigation_path: '5',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
['timeline' as const, {}],
|
||||||
|
])('%s', (viewName: FrigateCardView, result: Record<string, unknown>) => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({ view: viewName }),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig(config));
|
||||||
|
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
expect(manager.getMergedActions()).toEqual(result);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('ActionsManager.handleInteraction', () => {
|
||||||
|
it('should handle interaction', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const element = document.createElement('div');
|
||||||
|
vi.mocked(api.getCardElementManager().getElement).mockReturnValue(element);
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
const hass = createHASS();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(hass);
|
||||||
|
|
||||||
|
const actionForThisInteraction: ActionType = {
|
||||||
|
action: 'none',
|
||||||
|
};
|
||||||
|
vi.mocked(getActionConfigGivenAction).mockReturnValue(actionForThisInteraction);
|
||||||
|
|
||||||
|
manager.handleInteraction('tap');
|
||||||
|
|
||||||
|
expect(frigateCardHandleActionConfig).toBeCalledWith(
|
||||||
|
element,
|
||||||
|
hass,
|
||||||
|
manager.getMergedActions(),
|
||||||
|
'tap',
|
||||||
|
actionForThisInteraction,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not handle interaction', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(null);
|
||||||
|
|
||||||
|
// No values of hass.
|
||||||
|
manager.handleInteraction('tap');
|
||||||
|
expect(frigateCardHandleActionConfig).not.toBeCalledWith();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ActionsManager.handleActionEvent', () => {
|
||||||
|
it('should handle event', () => {
|
||||||
|
const action = createAction({ frigate_card_action: 'default' })!;
|
||||||
|
const event: CustomEvent<FrigateCardCustomAction> = new CustomEvent('ll-custom', {
|
||||||
|
detail: action,
|
||||||
|
});
|
||||||
|
|
||||||
|
// The file containing convertActionToFrigateCardCustomAction (action.ts) is
|
||||||
|
// mocked, so need to provide a value here.
|
||||||
|
vi.mocked(convertActionToFrigateCardCustomAction).mockReturnValue(action);
|
||||||
|
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
manager.handleActionEvent(event);
|
||||||
|
expect(api.getViewManager().setViewDefault).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not handle event without detail', () => {
|
||||||
|
const action = createAction({ frigate_card_action: 'default' })!;
|
||||||
|
const event = new Event('ll-custom');
|
||||||
|
|
||||||
|
// Mock this out just so that if the sentinel in handleActionEvent failed,
|
||||||
|
// it would still trigger a test failure below.
|
||||||
|
vi.mocked(convertActionToFrigateCardCustomAction).mockReturnValue(action);
|
||||||
|
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
manager.handleActionEvent(event);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not handle malformed action', () => {
|
||||||
|
const action = createAction({ frigate_card_action: 'default' })!;
|
||||||
|
const event: CustomEvent<FrigateCardCustomAction> = new CustomEvent('ll-custom', {
|
||||||
|
detail: action,
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(convertActionToFrigateCardCustomAction).mockReturnValue(null);
|
||||||
|
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
manager.handleActionEvent(event);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ActionsManager.executeAction', () => {
|
||||||
|
it('should not handle actions with different card_id', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
card_id: 'foo',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
card_id: 'NOT_foo',
|
||||||
|
frigate_card_action: 'default',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle default action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'default',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewDefault).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should handle view action', async () => {
|
||||||
|
it.each([
|
||||||
|
['clip' as const],
|
||||||
|
['clips' as const],
|
||||||
|
['image' as const],
|
||||||
|
['live' as const],
|
||||||
|
['recording' as const],
|
||||||
|
['recordings' as const],
|
||||||
|
['snapshot' as const],
|
||||||
|
['snapshots' as const],
|
||||||
|
['timeline' as const],
|
||||||
|
])('%s', async (viewName: FrigateCardView) => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: viewName,
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
viewName: viewName,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle download action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'download',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getDownloadManager().downloadViewerMedia).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle camera ui action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'camera_ui',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getCameraURLManager().openURL).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle expand action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'expand',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getExpandManager().toggleExpanded).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle fullscreen action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'fullscreen',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getFullscreenManager().toggleFullscreen).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle menu toggle action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'menu_toggle',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getCardElementManager().toggleMenu).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should handle camera_select action', () => {
|
||||||
|
it('with valid camera and view', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(createView());
|
||||||
|
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'camera_select',
|
||||||
|
camera: 'camera',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
viewName: 'live',
|
||||||
|
cameraID: 'camera',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('without config', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(null);
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({
|
||||||
|
view: 'timeline',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'camera_select',
|
||||||
|
camera: 'camera',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
viewName: 'timeline',
|
||||||
|
cameraID: 'camera',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with target view', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
view: {
|
||||||
|
// Change to clips view when the camera changes.
|
||||||
|
camera_select: 'clips',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({
|
||||||
|
view: 'live',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getViewManager().isViewSupportedByCamera).mockReturnValue(true);
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'camera_select',
|
||||||
|
camera: 'camera',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
viewName: 'clips',
|
||||||
|
cameraID: 'camera',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('without a current view', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'camera_select',
|
||||||
|
camera: 'camera',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewByParameters).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('with an unsupported view', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({
|
||||||
|
view: 'timeline',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'camera_select',
|
||||||
|
camera: 'camera',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
// Should have fallen back to the default view.
|
||||||
|
viewName: 'live',
|
||||||
|
cameraID: 'camera',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle live_substream_select action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'live_substream_select',
|
||||||
|
camera: 'substream',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewWithSubstream).toBeCalledWith('substream');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle live_substream_off action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'live_substream_off',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewWithoutSubstream).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle live_substream_on action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'live_substream_on',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewWithSubstream).toBeCalledWith();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should handle media_player action', () => {
|
||||||
|
it('to stop', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'media_player',
|
||||||
|
media_player_action: 'stop',
|
||||||
|
media_player: 'this_is_a_media_player',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getMediaPlayerManager().stop).toBeCalledWith('this_is_a_media_player');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('to play live', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({
|
||||||
|
camera: 'camera',
|
||||||
|
view: 'live',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'media_player',
|
||||||
|
media_player_action: 'play',
|
||||||
|
media_player: 'this_is_a_media_player',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getMediaPlayerManager().playLive).toBeCalledWith(
|
||||||
|
'this_is_a_media_player',
|
||||||
|
'camera',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('to play media', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const view = createViewWithMedia({
|
||||||
|
camera: 'camera',
|
||||||
|
view: 'media',
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(view);
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'media_player',
|
||||||
|
media_player_action: 'play',
|
||||||
|
media_player: 'this_is_a_media_player',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getMediaPlayerManager().playMedia).toBeCalledWith(
|
||||||
|
'this_is_a_media_player',
|
||||||
|
view.queryResults?.getSelectedResult(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('to play media without selected media', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getViewManager().getView).mockReturnValue(
|
||||||
|
createView({
|
||||||
|
view: 'media',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'media_player',
|
||||||
|
media_player_action: 'play',
|
||||||
|
media_player: 'this_is_a_media_player',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getMediaPlayerManager().playMedia).not.toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle diagnostics action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'diagnostics',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewByParameters).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
viewName: 'diagnostics',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle microphone_mute action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'microphone_mute',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getMicrophoneManager().mute).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle microphone_unmute action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'microphone_unmute',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getMicrophoneManager().unmute).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('should handle media player action', () => {
|
||||||
|
it.each([
|
||||||
|
['mute' as const],
|
||||||
|
['unmute' as const],
|
||||||
|
['play' as const],
|
||||||
|
['pause' as const],
|
||||||
|
])('%s', async (action: 'mute' | 'unmute' | 'play' | 'pause') => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const player = mock<FrigateCardMediaPlayer>();
|
||||||
|
vi.mocked(api.getMediaLoadedInfoManager().get).mockReturnValue(
|
||||||
|
createMediaLoadedInfo({
|
||||||
|
player: player,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: action,
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(player[action]).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle screenshot action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'screenshot',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getDownloadManager().downloadScreenshot).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle display_mode_select action', async () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new ActionsManager(api);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
createAction({
|
||||||
|
frigate_card_action: 'display_mode_select',
|
||||||
|
display_mode: 'grid',
|
||||||
|
})!,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewWithNewDisplayMode).toBeCalledWith('grid');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle unknown action', async () => {
|
||||||
|
const manager = new ActionsManager(createCardAPI());
|
||||||
|
|
||||||
|
const spy = vi.spyOn(global.console, 'warn').mockImplementation(() => true);
|
||||||
|
|
||||||
|
await manager.executeAction(
|
||||||
|
// Have to manually create the action (vs using `createAction()`) since
|
||||||
|
// it's malformed.
|
||||||
|
{
|
||||||
|
frigate_card_action: 'not_a_real_action',
|
||||||
|
} as unknown as FrigateCardCustomAction,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(spy).toBeCalledWith(
|
||||||
|
'Frigate card received unknown card action: not_a_real_action',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
import add from 'date-fns/add';
|
||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { AutoUpdateManager } from '../../src/card-controller/auto-update-manager';
|
||||||
|
import { createCardAPI, createConfig } from '../test-utils';
|
||||||
|
|
||||||
|
// @vitest-environment jsdom
|
||||||
|
describe('AutoUpdateManager', () => {
|
||||||
|
const start = new Date('2023-09-23T19:12:00');
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set default view when allowed', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
view: {
|
||||||
|
update_seconds: 10,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
// Card is triggered.
|
||||||
|
vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(true);
|
||||||
|
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
|
||||||
|
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(start);
|
||||||
|
|
||||||
|
const manager = new AutoUpdateManager(api);
|
||||||
|
manager.startDefaultViewTimer();
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||||
|
|
||||||
|
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||||
|
|
||||||
|
vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(false);
|
||||||
|
|
||||||
|
vi.setSystemTime(add(start, { seconds: 20 }));
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewDefault).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set default view when not configured', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
|
createConfig({
|
||||||
|
view: {
|
||||||
|
update_seconds: 0,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
vi.mocked(api.getTriggersManager().isTriggered).mockReturnValue(false);
|
||||||
|
vi.mocked(api.getInteractionManager().hasInteraction).mockReturnValue(false);
|
||||||
|
|
||||||
|
vi.useFakeTimers();
|
||||||
|
vi.setSystemTime(start);
|
||||||
|
|
||||||
|
const manager = new AutoUpdateManager(api);
|
||||||
|
manager.startDefaultViewTimer();
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||||
|
|
||||||
|
vi.setSystemTime(add(start, { seconds: 10 }));
|
||||||
|
vi.runOnlyPendingTimers();
|
||||||
|
|
||||||
|
expect(api.getViewManager().setViewDefault).not.toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { frigateCardHandleAction } from '../../src/utils/action.js';
|
||||||
|
import {
|
||||||
|
AutomationsManager,
|
||||||
|
} from '../../src/card-controller/automations-manager.js';
|
||||||
|
import { createCardAPI, createConfig, createHASS } from '../test-utils.js';
|
||||||
|
|
||||||
|
vi.mock('../../src/utils/action.js');
|
||||||
|
|
||||||
|
describe('AutomationsManager', () => {
|
||||||
|
const actions = [
|
||||||
|
{
|
||||||
|
action: 'custom:frigate-card-action',
|
||||||
|
frigate_card_action: 'clips',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const conditions = { fullscreen: true };
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing without hass', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
|
||||||
|
const automationsManager = new AutomationsManager(api);
|
||||||
|
automationsManager.execute();
|
||||||
|
expect(frigateCardHandleAction).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing without automations', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
|
||||||
|
const automationsManager = new AutomationsManager(api);
|
||||||
|
automationsManager.setAutomationsFromConfig();
|
||||||
|
automationsManager.execute();
|
||||||
|
expect(frigateCardHandleAction).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute actions', () => {
|
||||||
|
const config = createConfig({
|
||||||
|
automations: [
|
||||||
|
{
|
||||||
|
conditions: conditions,
|
||||||
|
actions: actions,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(config);
|
||||||
|
|
||||||
|
const automationsManager = new AutomationsManager(api);
|
||||||
|
automationsManager.setAutomationsFromConfig();
|
||||||
|
|
||||||
|
automationsManager.execute();
|
||||||
|
expect(frigateCardHandleAction).not.toBeCalled();
|
||||||
|
|
||||||
|
vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(true);
|
||||||
|
|
||||||
|
automationsManager.execute();
|
||||||
|
expect(frigateCardHandleAction).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
// Automation will not re-fire when condition continues to evaluate the
|
||||||
|
// same.
|
||||||
|
automationsManager.execute();
|
||||||
|
expect(frigateCardHandleAction).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(false);
|
||||||
|
|
||||||
|
automationsManager.execute();
|
||||||
|
expect(frigateCardHandleAction).toBeCalledTimes(1);
|
||||||
|
|
||||||
|
vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(true);
|
||||||
|
|
||||||
|
automationsManager.execute();
|
||||||
|
expect(frigateCardHandleAction).toBeCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should execute actions_not', () => {
|
||||||
|
const config = createConfig({
|
||||||
|
automations: [
|
||||||
|
{
|
||||||
|
conditions: conditions,
|
||||||
|
actions_not: actions,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(config);
|
||||||
|
vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(false);
|
||||||
|
|
||||||
|
const automationsManager = new AutomationsManager(api);
|
||||||
|
automationsManager.setAutomationsFromConfig();
|
||||||
|
|
||||||
|
automationsManager.execute();
|
||||||
|
|
||||||
|
expect(frigateCardHandleAction).toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should prevent automation loops', () => {
|
||||||
|
const config = createConfig({
|
||||||
|
automations: [
|
||||||
|
{
|
||||||
|
conditions: { fullscreen: true },
|
||||||
|
actions: actions,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
conditions: { fullscreen: true },
|
||||||
|
actions_not: actions,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const api = createCardAPI();
|
||||||
|
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||||
|
vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(config);
|
||||||
|
|
||||||
|
const automationsManager = new AutomationsManager(api);
|
||||||
|
automationsManager.setAutomationsFromConfig();
|
||||||
|
|
||||||
|
// Create a setup where one automation action causes another...
|
||||||
|
let evaluation = true;
|
||||||
|
vi.mocked(frigateCardHandleAction).mockImplementation(() => {
|
||||||
|
evaluation = !evaluation;
|
||||||
|
vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(
|
||||||
|
evaluation,
|
||||||
|
);
|
||||||
|
automationsManager.execute();
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mocked(api.getConditionsManager().evaluateCondition).mockReturnValue(evaluation);
|
||||||
|
|
||||||
|
automationsManager.execute();
|
||||||
|
|
||||||
|
expect(api.getMessageManager().setMessageIfHigherPriority).toBeCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
type: 'error',
|
||||||
|
message:
|
||||||
|
'Too many nested automation calls, please check your configuration for loops',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(frigateCardHandleAction).toBeCalledTimes(10);
|
||||||
|
});
|
||||||
|
});
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user