@@ -1,6 +1,6 @@
|
||||
import { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||
import { Camera, CameraInitializationOptions } from '../camera';
|
||||
import { CameraInitializationError } from '../error';
|
||||
|
||||
|
||||
@@ -1,21 +1,21 @@
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { BROWSE_MEDIA_CACHE_SECONDS } from '../../ha/browse-media/types';
|
||||
import { BrowseMediaWalker } from '../../ha/browse-media/walker';
|
||||
import { getMediaDownloadPath } from '../../ha/download';
|
||||
import { EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||
import { ResolvedMediaCache } from '../../ha/resolved-media';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { canonicalizeHAURL } from '../../utils/ha';
|
||||
import { BrowseMediaManager } from '../../utils/ha/browse-media/browse-media-manager';
|
||||
import { BROWSE_MEDIA_CACHE_SECONDS } from '../../utils/ha/browse-media/types';
|
||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||
import { ResolvedMediaCache, resolveMedia } from '../../utils/ha/resolved-media';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import { RequestCache } from '../cache';
|
||||
import { Endpoint } from '../../types';
|
||||
import { ViewMedia } from '../../view/item';
|
||||
import { ViewItemCapabilities } from '../../view/types';
|
||||
import { CameraManagerEngine } from '../engine';
|
||||
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
||||
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||
import {
|
||||
CameraEndpoint,
|
||||
CameraEventCallback,
|
||||
CameraManagerMediaCapabilities,
|
||||
DataQuery,
|
||||
CameraManagerRequestCache,
|
||||
CameraQuery,
|
||||
EventQuery,
|
||||
PartialEventQuery,
|
||||
QueryType,
|
||||
@@ -28,22 +28,22 @@ export class BrowseMediaCameraManagerEngine
|
||||
extends GenericCameraManagerEngine
|
||||
implements CameraManagerEngine
|
||||
{
|
||||
protected _browseMediaManager: BrowseMediaManager;
|
||||
protected _browseMediaWalker: BrowseMediaWalker;
|
||||
protected _entityRegistryManager: EntityRegistryManager;
|
||||
protected _resolvedMediaCache: ResolvedMediaCache;
|
||||
protected _requestCache: RequestCache;
|
||||
protected _requestCache: CameraManagerRequestCache;
|
||||
|
||||
public constructor(
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
browseMediaManager: BrowseMediaManager,
|
||||
browseMediaManager: BrowseMediaWalker,
|
||||
resolvedMediaCache: ResolvedMediaCache,
|
||||
requestCache: RequestCache,
|
||||
requestCache: CameraManagerRequestCache,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
super(stateWatcher, eventCallback);
|
||||
this._entityRegistryManager = entityRegistryManager;
|
||||
this._browseMediaManager = browseMediaManager;
|
||||
this._browseMediaWalker = browseMediaManager;
|
||||
this._resolvedMediaCache = resolvedMediaCache;
|
||||
this._requestCache = requestCache;
|
||||
}
|
||||
@@ -66,18 +66,11 @@ export class BrowseMediaCameraManagerEngine
|
||||
hass: HomeAssistant,
|
||||
_cameraConfig: CameraConfig,
|
||||
media: ViewMedia,
|
||||
): Promise<CameraEndpoint | null> {
|
||||
const contentID = media.getContentID();
|
||||
if (!contentID) {
|
||||
return null;
|
||||
}
|
||||
const resolvedMedia = await resolveMedia(hass, contentID, this._resolvedMediaCache);
|
||||
return resolvedMedia
|
||||
? { endpoint: canonicalizeHAURL(hass, resolvedMedia.url) }
|
||||
: null;
|
||||
): Promise<Endpoint | null> {
|
||||
return getMediaDownloadPath(hass, media.getContentID(), this._resolvedMediaCache);
|
||||
}
|
||||
|
||||
public getQueryResultMaxAge(query: DataQuery): number | null {
|
||||
public getQueryResultMaxAge(query: CameraQuery): number | null {
|
||||
if (query.type === QueryType.Event) {
|
||||
return BROWSE_MEDIA_CACHE_SECONDS;
|
||||
}
|
||||
@@ -85,7 +78,7 @@ export class BrowseMediaCameraManagerEngine
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public getMediaCapabilities(_media: ViewMedia): CameraManagerMediaCapabilities {
|
||||
public getMediaCapabilities(_media: ViewMedia): ViewItemCapabilities {
|
||||
return {
|
||||
canFavorite: false,
|
||||
canDownload: true,
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
import { format } from 'date-fns';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { formatDateAndTime } from '../../utils/basic';
|
||||
import { RichBrowseMedia } from '../../utils/ha/browse-media/types';
|
||||
import {
|
||||
ViewMedia,
|
||||
EventViewMedia,
|
||||
ViewMediaType,
|
||||
VideoContentType,
|
||||
} from '../../view/media';
|
||||
import { BrowseMediaMetadata } from '../browse-media/types';
|
||||
|
||||
class BrowseMediaEventViewMedia extends ViewMedia implements EventViewMedia {
|
||||
protected _browseMedia: RichBrowseMedia<BrowseMediaMetadata>;
|
||||
protected _id: string;
|
||||
|
||||
constructor(
|
||||
mediaType: ViewMediaType,
|
||||
cameraID: string,
|
||||
browseMedia: RichBrowseMedia<BrowseMediaMetadata>,
|
||||
) {
|
||||
super(mediaType, cameraID);
|
||||
this._browseMedia = browseMedia;
|
||||
|
||||
// Generate a custom ID that uses the start date (to allow multiple
|
||||
// BrowseMedia objects (e.g. images and movies) to be de-duplicated).
|
||||
if (browseMedia._metadata?.startDate) {
|
||||
this._id = `${cameraID}/${format(
|
||||
browseMedia._metadata.startDate,
|
||||
'yyyy-MM-dd HH:mm:ss',
|
||||
)}`;
|
||||
} else {
|
||||
this._id = browseMedia.media_content_id;
|
||||
}
|
||||
}
|
||||
|
||||
public getStartTime(): Date | null {
|
||||
return this._browseMedia._metadata?.startDate ?? null;
|
||||
}
|
||||
public getEndTime(): Date | null {
|
||||
return this._browseMedia._metadata?.endDate ?? null;
|
||||
}
|
||||
public getVideoContentType(): VideoContentType | null {
|
||||
return VideoContentType.MP4;
|
||||
}
|
||||
public getID(): string {
|
||||
return this._id;
|
||||
}
|
||||
public getContentID(): string {
|
||||
return this._browseMedia.media_content_id;
|
||||
}
|
||||
public getTitle(): string | null {
|
||||
const startTime = this.getStartTime();
|
||||
return startTime ? formatDateAndTime(startTime) : this._browseMedia.title;
|
||||
}
|
||||
public getThumbnail(): string | null {
|
||||
return this._browseMedia.thumbnail;
|
||||
}
|
||||
public getWhat(): string[] | null {
|
||||
return this._browseMedia._metadata?.what ?? null;
|
||||
}
|
||||
public getScore(): number | null {
|
||||
return null;
|
||||
}
|
||||
public getTags(): string[] | null {
|
||||
return null;
|
||||
}
|
||||
public isGroupableWith(that: EventViewMedia): boolean {
|
||||
return (
|
||||
this.getMediaType() === that.getMediaType() &&
|
||||
isEqual(this.getWhere(), that.getWhere()) &&
|
||||
isEqual(this.getWhat(), that.getWhat())
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export class BrowseMediaViewMediaFactory {
|
||||
static createEventViewMedia(
|
||||
mediaType: 'clip' | 'snapshot',
|
||||
browseMedia: RichBrowseMedia<BrowseMediaMetadata>,
|
||||
cameraID: string,
|
||||
): BrowseMediaEventViewMedia {
|
||||
return new BrowseMediaEventViewMedia(mediaType, cameraID, browseMedia);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
export interface BrowseMediaMetadata {
|
||||
cameraID: string;
|
||||
startDate: Date;
|
||||
endDate: Date;
|
||||
what?: string[];
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import {
|
||||
RichBrowseMedia,
|
||||
MEDIA_CLASS_VIDEO,
|
||||
MEDIA_CLASS_IMAGE,
|
||||
} from '../../../utils/ha/browse-media/types';
|
||||
import { ViewMedia } from '../../../view/media';
|
||||
import { BrowseMediaViewMediaFactory } from '../media';
|
||||
import { BrowseMediaMetadata } from '../types';
|
||||
|
||||
export const getViewMediaFromBrowseMediaArray = (
|
||||
browseMedia: RichBrowseMedia<BrowseMediaMetadata>[],
|
||||
): ViewMedia[] | null => {
|
||||
const lookup: Map<string, ViewMedia> = new Map();
|
||||
for (const browseMediaItem of browseMedia) {
|
||||
const cameraID = browseMediaItem._metadata?.cameraID;
|
||||
if (!cameraID) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const mediaType =
|
||||
browseMediaItem.media_class === MEDIA_CLASS_VIDEO
|
||||
? 'clip'
|
||||
: browseMediaItem.media_class === MEDIA_CLASS_IMAGE
|
||||
? 'snapshot'
|
||||
: null;
|
||||
|
||||
if (!mediaType) {
|
||||
continue;
|
||||
}
|
||||
const media = BrowseMediaViewMediaFactory.createEventViewMedia(
|
||||
mediaType,
|
||||
browseMediaItem,
|
||||
cameraID,
|
||||
);
|
||||
|
||||
const id = media.getID();
|
||||
const existing = lookup.get(id);
|
||||
|
||||
// De-duplicate events with precisely the same ID (same
|
||||
// hour/minute/second) choosing clip > snapshot.
|
||||
if (
|
||||
!existing ||
|
||||
(existing.getMediaType() === 'snapshot' && media.getMediaType() === 'clip')
|
||||
) {
|
||||
lookup.set(id, media);
|
||||
}
|
||||
}
|
||||
return [...lookup.values()];
|
||||
};
|
||||
@@ -1,50 +0,0 @@
|
||||
import { RichBrowseMedia } from '../../../utils/ha/browse-media/types';
|
||||
import { rangesOverlap } from '../../range';
|
||||
import { BrowseMediaMetadata } from '../types';
|
||||
|
||||
/**
|
||||
* A utility method to determine if a browse media object matches against a
|
||||
* start and end date.
|
||||
* @param media The browse media object (with rich metadata).
|
||||
* @param start The optional start date.
|
||||
* @param end The optional end date.
|
||||
* @returns `true` if the media falls within the provided dates.
|
||||
*/
|
||||
|
||||
export const isMediaWithinDates = (
|
||||
media: RichBrowseMedia<BrowseMediaMetadata>,
|
||||
start?: Date,
|
||||
end?: Date,
|
||||
): boolean => {
|
||||
// If there's no metadata, nothing matches.
|
||||
if (!media._metadata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (start && end) {
|
||||
// Determine if:
|
||||
// - The media starts within the query timeframe.
|
||||
// - The media ends within the query timeframe.
|
||||
// - The media entirely encompasses the query timeframe.
|
||||
return rangesOverlap(
|
||||
{
|
||||
start: media._metadata.startDate,
|
||||
end: media._metadata.endDate,
|
||||
},
|
||||
{
|
||||
start: start,
|
||||
end: end,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (!start && end) {
|
||||
return media._metadata.startDate <= end;
|
||||
}
|
||||
if (start && !end) {
|
||||
return media._metadata.startDate >= start;
|
||||
}
|
||||
|
||||
// If no date is specified at all, everything matches.
|
||||
return true;
|
||||
};
|
||||
@@ -1,69 +1,6 @@
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import orderBy from 'lodash-es/orderBy';
|
||||
import sortedUniqBy from 'lodash-es/sortedUniqBy';
|
||||
import { orderBy, sortedUniqBy } from 'lodash-es';
|
||||
import { DateRange, MemoryRangeSet } from './range';
|
||||
import { DataQuery, QueryResults, RecordingSegment } from './types';
|
||||
|
||||
interface RequestCacheItem<Request, Response> {
|
||||
request: Request;
|
||||
response: Response;
|
||||
expires?: Date;
|
||||
}
|
||||
|
||||
interface CameraManagerCache<Request, Response> {
|
||||
get(request: Request): Response | null;
|
||||
has(request: Request): boolean;
|
||||
set(request: Request, response: Response, expiry?: Date): void;
|
||||
}
|
||||
|
||||
export class MemoryRequestCache<Request, Response>
|
||||
implements CameraManagerCache<Request, Response>
|
||||
{
|
||||
protected _data: RequestCacheItem<Request, Response>[] = [];
|
||||
|
||||
public get(request: Request): Response | null {
|
||||
const now = new Date();
|
||||
for (const item of this._data) {
|
||||
if (
|
||||
(!item.expires || now <= item.expires) &&
|
||||
this._contains(request, item.request)
|
||||
) {
|
||||
return item.response;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public clear(): void {
|
||||
this._data = [];
|
||||
}
|
||||
|
||||
public has(request: Request): boolean {
|
||||
return !!this.get(request);
|
||||
}
|
||||
|
||||
public set(request: Request, response: Response, expiry?: Date): void {
|
||||
this._data.push({
|
||||
request: request,
|
||||
response: response,
|
||||
expires: expiry,
|
||||
});
|
||||
|
||||
// Clean up old requests on set.
|
||||
this._expireOldRequests();
|
||||
}
|
||||
|
||||
protected _contains(a: Request, b: Request): boolean {
|
||||
return isEqual(a, b);
|
||||
}
|
||||
|
||||
protected _expireOldRequests(): void {
|
||||
const now = new Date();
|
||||
this._data = this._data.filter((item) => !item.expires || now < item.expires);
|
||||
}
|
||||
}
|
||||
|
||||
export class RequestCache extends MemoryRequestCache<DataQuery, QueryResults> {}
|
||||
import { RecordingSegment } from './types';
|
||||
|
||||
class MemoryRangedCache<Data> {
|
||||
protected _ranges: MemoryRangeSet = new MemoryRangeSet();
|
||||
|
||||
@@ -2,8 +2,9 @@ import { ActionsExecutor } from '../card-controller/actions/types';
|
||||
import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher';
|
||||
import { PTZAction, PTZActionPhase } from '../config/schema/actions/custom/ptz';
|
||||
import { CameraConfig } from '../config/schema/cameras';
|
||||
import { isTriggeredState } from '../ha/is-triggered-state';
|
||||
import { HassStateDifference } from '../ha/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import { HassStateDifference, isTriggeredState } from '../utils/ha';
|
||||
import { Capabilities } from './capabilities';
|
||||
import { CameraManagerEngine } from './engine';
|
||||
import { CameraNoIDError } from './error';
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import { StateWatcherSubscriptionInterface } from '../card-controller/hass/state-watcher';
|
||||
import { CameraConfig } from '../config/schema/cameras';
|
||||
import { BrowseMediaWalker } from '../ha/browse-media/walker';
|
||||
import { EntityRegistryManager } from '../ha/registry/entity/types';
|
||||
import { ResolvedMediaCache } from '../ha/resolved-media';
|
||||
import { HomeAssistant } from '../ha/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import { BrowseMediaManager } from '../utils/ha/browse-media/browse-media-manager';
|
||||
import { EntityRegistryManager } from '../utils/ha/registry/entity/types';
|
||||
import { ResolvedMediaCache } from '../utils/ha/resolved-media';
|
||||
import { RecordingSegmentsCache, RequestCache } from './cache';
|
||||
import { RecordingSegmentsCache } from './cache';
|
||||
import { CameraManagerEngine } from './engine';
|
||||
import { CameraInitializationError } from './error';
|
||||
import { CameraEventCallback, Engine } from './types';
|
||||
import { CameraEventCallback, CameraManagerRequestCache, Engine } from './types';
|
||||
import { getCameraEntityFromConfig } from './utils/camera-entity-from-config';
|
||||
|
||||
interface CameraManagerEngineFactoryOptions {
|
||||
@@ -44,7 +44,7 @@ export class CameraManagerEngineFactory {
|
||||
this._entityRegistryManager,
|
||||
options.stateWatcher,
|
||||
new RecordingSegmentsCache(),
|
||||
new RequestCache(),
|
||||
new CameraManagerRequestCache(),
|
||||
options.eventCallback,
|
||||
);
|
||||
break;
|
||||
@@ -55,9 +55,9 @@ export class CameraManagerEngineFactory {
|
||||
cameraManagerEngine = new MotionEyeCameraManagerEngine(
|
||||
this._entityRegistryManager,
|
||||
options.stateWatcher,
|
||||
new BrowseMediaManager(),
|
||||
new BrowseMediaWalker(),
|
||||
options.resolvedMediaCache,
|
||||
new RequestCache(),
|
||||
new CameraManagerRequestCache(),
|
||||
options.eventCallback,
|
||||
);
|
||||
break;
|
||||
@@ -66,9 +66,9 @@ export class CameraManagerEngineFactory {
|
||||
cameraManagerEngine = new ReolinkCameraManagerEngine(
|
||||
this._entityRegistryManager,
|
||||
options.stateWatcher,
|
||||
new BrowseMediaManager(),
|
||||
new BrowseMediaWalker(),
|
||||
options.resolvedMediaCache,
|
||||
new RequestCache(),
|
||||
new CameraManagerRequestCache(),
|
||||
options.eventCallback,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { CameraConfig } from '../config/schema/cameras';
|
||||
import { HomeAssistant } from '../ha/types';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { Endpoint } from '../types';
|
||||
import { ViewMedia } from '../view/item';
|
||||
import { ViewItemCapabilities } from '../view/types';
|
||||
import { Camera } from './camera';
|
||||
import { CameraManagerReadOnlyConfigStore } from './store';
|
||||
import {
|
||||
CameraEndpoint,
|
||||
CameraEndpoints,
|
||||
CameraEndpointsContext,
|
||||
CameraManagerCameraMetadata,
|
||||
CameraManagerMediaCapabilities,
|
||||
DataQuery,
|
||||
CameraQuery,
|
||||
Engine,
|
||||
EngineOptions,
|
||||
EventQuery,
|
||||
@@ -90,7 +90,7 @@ export interface CameraManagerEngine {
|
||||
hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
media: ViewMedia,
|
||||
): Promise<CameraEndpoint | null>;
|
||||
): Promise<Endpoint | null>;
|
||||
|
||||
favoriteMedia(
|
||||
hass: HomeAssistant,
|
||||
@@ -99,7 +99,7 @@ export interface CameraManagerEngine {
|
||||
favorite: boolean,
|
||||
): Promise<void>;
|
||||
|
||||
getQueryResultMaxAge(query: DataQuery): number | null;
|
||||
getQueryResultMaxAge(query: CameraQuery): number | null;
|
||||
|
||||
getMediaSeekTime(
|
||||
hass: HomeAssistant,
|
||||
@@ -121,7 +121,7 @@ export interface CameraManagerEngine {
|
||||
cameraConfig: CameraConfig,
|
||||
): CameraManagerCameraMetadata;
|
||||
|
||||
getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null;
|
||||
getMediaCapabilities(media: ViewMedia): ViewItemCapabilities | null;
|
||||
|
||||
getCameraEndpoints(
|
||||
cameraConfig: CameraConfig,
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import uniq from 'lodash-es/uniq';
|
||||
import { uniq } from 'lodash-es';
|
||||
import { ActionsExecutor } from '../../card-controller/actions/types';
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { PTZCapabilities, PTZMovementType } from '../../types';
|
||||
import { errorToConsole } from '../../utils/basic';
|
||||
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||
import { Camera, CameraInitializationOptions } from '../camera';
|
||||
import { Capabilities } from '../capabilities';
|
||||
import { CameraManagerEngine } from '../engine';
|
||||
|
||||
@@ -1,22 +1,21 @@
|
||||
import { add, endOfHour, format, fromUnixTime, startOfHour } from 'date-fns';
|
||||
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 { isEqual, orderBy, throttle, uniqWith } from 'lodash-es';
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { getEntityTitle } from '../../ha/get-entity-title';
|
||||
import { EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { Endpoint } from '../../types';
|
||||
import {
|
||||
allPromises,
|
||||
formatDate,
|
||||
prettifyTitle,
|
||||
runWhenIdleIfSupported,
|
||||
} from '../../utils/basic';
|
||||
import { getEntityTitle } from '../../utils/ha';
|
||||
import { EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import { ViewMediaClassifier } from '../../view/media-classifier';
|
||||
import { RecordingSegmentsCache, RequestCache } from '../cache';
|
||||
import { ViewMedia, ViewMediaType } from '../../view/item';
|
||||
import { ViewItemClassifier } from '../../view/item-classifier';
|
||||
import { ViewItemCapabilities } from '../../view/types';
|
||||
import { RecordingSegmentsCache } from '../cache';
|
||||
import { Camera } from '../camera';
|
||||
import {
|
||||
CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT,
|
||||
@@ -26,13 +25,12 @@ import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
||||
import { DateRange } from '../range';
|
||||
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||
import {
|
||||
CameraEndpoint,
|
||||
CameraEndpoints,
|
||||
CameraEndpointsContext,
|
||||
CameraEventCallback,
|
||||
CameraManagerCameraMetadata,
|
||||
CameraManagerMediaCapabilities,
|
||||
DataQuery,
|
||||
CameraManagerRequestCache,
|
||||
CameraQuery,
|
||||
Engine,
|
||||
EngineOptions,
|
||||
EventQuery,
|
||||
@@ -112,7 +110,7 @@ export class FrigateCameraManagerEngine
|
||||
protected _entityRegistryManager: EntityRegistryManager;
|
||||
protected _frigateEventWatcher: FrigateEventWatcher;
|
||||
protected _recordingSegmentsCache: RecordingSegmentsCache;
|
||||
protected _requestCache: RequestCache;
|
||||
protected _requestCache: CameraManagerRequestCache;
|
||||
|
||||
// Garbage collect segments at most once an hour.
|
||||
protected _throttledSegmentGarbageCollector = throttle(
|
||||
@@ -125,7 +123,7 @@ export class FrigateCameraManagerEngine
|
||||
entityRegistryManager: EntityRegistryManager,
|
||||
stateWatcher: StateWatcherSubscriptionInterface,
|
||||
recordingSegmentsCache: RecordingSegmentsCache,
|
||||
requestCache: RequestCache,
|
||||
requestCache: CameraManagerRequestCache,
|
||||
eventCallback?: CameraEventCallback,
|
||||
) {
|
||||
super(stateWatcher, eventCallback);
|
||||
@@ -158,13 +156,13 @@ export class FrigateCameraManagerEngine
|
||||
_hass: HomeAssistant,
|
||||
cameraConfig: CameraConfig,
|
||||
media: ViewMedia,
|
||||
): Promise<CameraEndpoint | null> {
|
||||
): Promise<Endpoint | null> {
|
||||
if (FrigateViewMediaClassifier.isFrigateEvent(media)) {
|
||||
return {
|
||||
endpoint:
|
||||
`/api/frigate/${cameraConfig.frigate.client_id}` +
|
||||
`/notifications/${media.getID()}/` +
|
||||
`${ViewMediaClassifier.isClip(media) ? 'clip.mp4' : 'snapshot.jpg'}` +
|
||||
`${ViewItemClassifier.isClip(media) ? 'clip.mp4' : 'snapshot.jpg'}` +
|
||||
`?download=true`,
|
||||
sign: true,
|
||||
};
|
||||
@@ -541,7 +539,7 @@ export class FrigateCameraManagerEngine
|
||||
|
||||
protected _getCameraIDMatch(
|
||||
store: CameraManagerReadOnlyConfigStore,
|
||||
query: DataQuery,
|
||||
query: CameraQuery,
|
||||
instanceID: string,
|
||||
cameraName: string,
|
||||
): string | null {
|
||||
@@ -588,17 +586,17 @@ export class FrigateCameraManagerEngine
|
||||
if (!cameraConfig) {
|
||||
continue;
|
||||
}
|
||||
let mediaType: 'clip' | 'snapshot' | null = null;
|
||||
let mediaType: ViewMediaType | null = null;
|
||||
if (
|
||||
!query.hasClip &&
|
||||
!query.hasSnapshot &&
|
||||
(event.has_clip || event.has_snapshot)
|
||||
) {
|
||||
mediaType = event.has_clip ? 'clip' : 'snapshot';
|
||||
mediaType = event.has_clip ? ViewMediaType.Clip : ViewMediaType.Snapshot;
|
||||
} else if (query.hasSnapshot && event.has_snapshot) {
|
||||
mediaType = 'snapshot';
|
||||
mediaType = ViewMediaType.Snapshot;
|
||||
} else if (query.hasClip && event.has_clip) {
|
||||
mediaType = 'clip';
|
||||
mediaType = ViewMediaType.Clip;
|
||||
}
|
||||
if (!mediaType) {
|
||||
continue;
|
||||
@@ -646,7 +644,7 @@ export class FrigateCameraManagerEngine
|
||||
return output;
|
||||
}
|
||||
|
||||
public getQueryResultMaxAge(query: DataQuery): number | null {
|
||||
public getQueryResultMaxAge(query: CameraQuery): number | null {
|
||||
if (query.type === QueryType.Event) {
|
||||
return EVENT_REQUEST_CACHE_MAX_AGE_SECONDS;
|
||||
} else if (query.type === QueryType.Recording) {
|
||||
@@ -664,11 +662,12 @@ export class FrigateCameraManagerEngine
|
||||
): Promise<number | null> {
|
||||
const start = media.getStartTime();
|
||||
const end = media.getEndTime();
|
||||
if (!start || !end || target < start || target > end) {
|
||||
const cameraID = media.getCameraID();
|
||||
|
||||
if (!start || !end || target < start || target > end || !cameraID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cameraID = media.getCameraID();
|
||||
const query: RecordingSegmentsQuery = {
|
||||
cameraIDs: new Set([cameraID]),
|
||||
start: start,
|
||||
@@ -898,9 +897,9 @@ export class FrigateCameraManagerEngine
|
||||
return seekMilliseconds / 1000;
|
||||
}
|
||||
|
||||
public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities {
|
||||
public getMediaCapabilities(media: ViewMedia): ViewItemCapabilities {
|
||||
return {
|
||||
canFavorite: ViewMediaClassifier.isEvent(media),
|
||||
canFavorite: ViewItemClassifier.isEvent(media),
|
||||
canDownload: true,
|
||||
};
|
||||
}
|
||||
@@ -926,7 +925,7 @@ export class FrigateCameraManagerEngine
|
||||
cameraConfig: CameraConfig,
|
||||
context?: CameraEndpointsContext,
|
||||
): CameraEndpoints | null {
|
||||
const getUIEndpoint = (): CameraEndpoint | null => {
|
||||
const getUIEndpoint = (): Endpoint | null => {
|
||||
if (!cameraConfig.frigate.url) {
|
||||
return null;
|
||||
}
|
||||
@@ -976,7 +975,7 @@ export class FrigateCameraManagerEngine
|
||||
};
|
||||
};
|
||||
|
||||
const getJSMPEG = (): CameraEndpoint | null => {
|
||||
const getJSMPEG = (): Endpoint | null => {
|
||||
return {
|
||||
endpoint:
|
||||
`/api/frigate/${cameraConfig.frigate.client_id}` +
|
||||
|
||||
@@ -53,6 +53,7 @@ export class FrigateEventWatcher implements FrigateEventWatcherSubscriptionInter
|
||||
let json: unknown;
|
||||
try {
|
||||
json = JSON.parse(data);
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
} catch (e) {
|
||||
console.warn('Received non-JSON payload as Frigate event', data);
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import { ViewMedia } from '../../view/item';
|
||||
import { FrigateEventViewMedia, FrigateRecordingViewMedia } from './media';
|
||||
|
||||
export class FrigateViewMediaClassifier {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fromUnixTime } from 'date-fns';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import {
|
||||
EventViewMedia,
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
VideoContentType,
|
||||
ViewMedia,
|
||||
ViewMediaType,
|
||||
} from '../../view/media';
|
||||
} from '../../view/item';
|
||||
import { FrigateEvent, FrigateRecording } from './types';
|
||||
import {
|
||||
getEventMediaContentID,
|
||||
@@ -36,7 +36,7 @@ export class FrigateEventViewMedia extends ViewMedia implements EventViewMedia {
|
||||
// sublabels (`_splitSubLabels` in engine-frigate.ts).
|
||||
subLabels?: string[],
|
||||
) {
|
||||
super(mediaType, cameraID);
|
||||
super(mediaType, { cameraID });
|
||||
this._event = event;
|
||||
this._contentID = contentID;
|
||||
this._thumbnail = thumbnail;
|
||||
@@ -88,8 +88,6 @@ export class FrigateEventViewMedia extends ViewMedia implements EventViewMedia {
|
||||
public getTags(): string[] | null {
|
||||
return this._subLabels;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
public isGroupableWith(that: EventViewMedia): boolean {
|
||||
return (
|
||||
this.getMediaType() === that.getMediaType() &&
|
||||
@@ -113,7 +111,7 @@ export class FrigateRecordingViewMedia extends ViewMedia implements RecordingVie
|
||||
contentID: string,
|
||||
title: string,
|
||||
) {
|
||||
super(mediaType, cameraID);
|
||||
super(mediaType, { cameraID });
|
||||
this._recording = recording;
|
||||
this._id = id;
|
||||
this._contentID = contentID;
|
||||
@@ -150,7 +148,7 @@ export class FrigateRecordingViewMedia extends ViewMedia implements RecordingVie
|
||||
|
||||
export class FrigateViewMediaFactory {
|
||||
static createEventViewMedia(
|
||||
mediaType: 'clip' | 'snapshot',
|
||||
mediaType: ViewMediaType,
|
||||
cameraID: string,
|
||||
cameraConfig: CameraConfig,
|
||||
event: FrigateEvent,
|
||||
@@ -191,7 +189,7 @@ export class FrigateViewMediaFactory {
|
||||
}
|
||||
|
||||
return new FrigateRecordingViewMedia(
|
||||
'recording',
|
||||
ViewMediaType.Recording,
|
||||
cameraID,
|
||||
recording,
|
||||
getRecordingID(cameraConfig, recording),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { homeAssistantWSRequest } from '../../ha/ws-request';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { AdvancedCameraCardError } from '../../types';
|
||||
import { homeAssistantWSRequest } from '../../utils/ha/ws-request';
|
||||
import { RecordingSegment } from '../types';
|
||||
import {
|
||||
EventSummary,
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||
|
||||
import { StateWatcherSubscriptionInterface } from '../../card-controller/hass/state-watcher';
|
||||
import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { getEntityTitle } from '../../ha/get-entity-title';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { getEntityTitle } from '../../utils/ha';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
import { Endpoint } from '../../types';
|
||||
import { ViewMedia } from '../../view/item';
|
||||
import { ViewItemCapabilities } from '../../view/types';
|
||||
import { Camera } from '../camera';
|
||||
import { Capabilities } from '../capabilities';
|
||||
import { CameraManagerEngine } from '../engine';
|
||||
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||
import {
|
||||
CameraEndpoint,
|
||||
CameraEndpoints,
|
||||
CameraEndpointsContext,
|
||||
CameraEventCallback,
|
||||
CameraManagerCameraMetadata,
|
||||
CameraManagerMediaCapabilities,
|
||||
DataQuery,
|
||||
CameraQuery,
|
||||
Engine,
|
||||
EngineOptions,
|
||||
EventQuery,
|
||||
@@ -155,7 +154,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
_hass: HomeAssistant,
|
||||
_cameraConfig: CameraConfig,
|
||||
_media: ViewMedia,
|
||||
): Promise<CameraEndpoint | null> {
|
||||
): Promise<Endpoint | null> {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -168,7 +167,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
return;
|
||||
}
|
||||
|
||||
public getQueryResultMaxAge(_query: DataQuery): number | null {
|
||||
public getQueryResultMaxAge(_query: CameraQuery): number | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -211,7 +210,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
};
|
||||
}
|
||||
|
||||
public getMediaCapabilities(_media: ViewMedia): CameraManagerMediaCapabilities | null {
|
||||
public getMediaCapabilities(_media: ViewMedia): ViewItemCapabilities | null {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -219,7 +218,7 @@ export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||
cameraConfig: CameraConfig,
|
||||
_context?: CameraEndpointsContext,
|
||||
): CameraEndpoints | null {
|
||||
const getWebRTCCard = (): CameraEndpoint | null => {
|
||||
const getWebRTCCard = (): Endpoint | null => {
|
||||
// The user may override this in their webrtc_card configuration.
|
||||
const endpoint = cameraConfig.camera_entity ? cameraConfig.camera_entity : null;
|
||||
return endpoint ? { endpoint: endpoint } : null;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { add } from 'date-fns';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import sum from 'lodash-es/sum';
|
||||
import { cloneDeep, sum } from 'lodash-es';
|
||||
import PQueue from 'p-queue';
|
||||
import { CardCameraAPI } from '../card-controller/types.js';
|
||||
import { sortItems } from '../card-controller/view/sort.js';
|
||||
import { PTZAction, PTZActionPhase } from '../config/schema/actions/custom/ptz.js';
|
||||
import { CameraConfig, CamerasConfig } from '../config/schema/cameras.js';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import { Endpoint } from '../types.js';
|
||||
import {
|
||||
allPromises,
|
||||
arrayify,
|
||||
@@ -16,19 +17,19 @@ import {
|
||||
} from '../utils/basic.js';
|
||||
import { getCameraID } from '../utils/camera.js';
|
||||
import { log } from '../utils/debug.js';
|
||||
import { ViewMedia } from '../view/media.js';
|
||||
import { ViewItemClassifier } from '../view/item-classifier.js';
|
||||
import { ViewItem, ViewMedia } from '../view/item.js';
|
||||
import { ViewItemCapabilities } from '../view/types.js';
|
||||
import { Capabilities } from './capabilities.js';
|
||||
import { CameraManagerEngineFactory } from './engine-factory.js';
|
||||
import { CameraManagerEngine } from './engine.js';
|
||||
import { CameraInitializationError } from './error.js';
|
||||
import { CameraManagerReadOnlyConfigStore, CameraManagerStore } from './store.js';
|
||||
import {
|
||||
CameraEndpoint,
|
||||
CameraEndpoints,
|
||||
CameraEndpointsContext,
|
||||
CameraManagerCameraMetadata,
|
||||
CameraManagerMediaCapabilities,
|
||||
DataQuery,
|
||||
CameraQuery,
|
||||
Engine,
|
||||
EngineOptions,
|
||||
EventQuery,
|
||||
@@ -38,7 +39,7 @@ import {
|
||||
MediaMetadataQuery,
|
||||
MediaMetadataQueryResults,
|
||||
MediaQuery,
|
||||
PartialDataQuery,
|
||||
PartialCameraQuery,
|
||||
PartialEventQuery,
|
||||
PartialQueryConcreteType,
|
||||
PartialRecordingQuery,
|
||||
@@ -55,24 +56,25 @@ import {
|
||||
RecordingSegmentsQueryResultsMap,
|
||||
ResultsMap,
|
||||
} from './types.js';
|
||||
import { sortMedia } from './utils/sort-media.js';
|
||||
|
||||
export class QueryClassifier {
|
||||
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
|
||||
export class CameraQueryClassifier {
|
||||
public static isEventQuery(
|
||||
query: CameraQuery | PartialCameraQuery,
|
||||
): query is EventQuery {
|
||||
return query.type === QueryType.Event;
|
||||
}
|
||||
public static isRecordingQuery(
|
||||
query: DataQuery | PartialDataQuery,
|
||||
query: CameraQuery | PartialCameraQuery,
|
||||
): query is RecordingQuery {
|
||||
return query.type === QueryType.Recording;
|
||||
}
|
||||
public static isRecordingSegmentsQuery(
|
||||
query: DataQuery | PartialDataQuery,
|
||||
query: CameraQuery | PartialCameraQuery,
|
||||
): query is RecordingSegmentsQuery {
|
||||
return query.type === QueryType.RecordingSegments;
|
||||
}
|
||||
public static isMediaMetadataQuery(
|
||||
query: DataQuery | PartialDataQuery,
|
||||
query: CameraQuery | PartialCameraQuery,
|
||||
): query is MediaMetadataQuery {
|
||||
return query.type === QueryType.MediaMetadata;
|
||||
}
|
||||
@@ -103,7 +105,7 @@ export class QueryResultClassifier {
|
||||
|
||||
export interface ExtendedMediaQueryResult<T extends MediaQuery> {
|
||||
queries: T[];
|
||||
results: ViewMedia[];
|
||||
results: ViewItem[];
|
||||
}
|
||||
|
||||
export class CameraManager {
|
||||
@@ -318,7 +320,7 @@ export class CameraManager {
|
||||
});
|
||||
}
|
||||
|
||||
protected _generateDefaultQueries<PQT extends PartialDataQuery>(
|
||||
protected _generateDefaultQueries<PQT extends PartialCameraQuery>(
|
||||
cameraIDs: string | Set<string>,
|
||||
partialQuery: PQT,
|
||||
): PartialQueryConcreteType<PQT>[] | null {
|
||||
@@ -330,17 +332,17 @@ export class CameraManager {
|
||||
}
|
||||
|
||||
for (const [engine, cameraIDs] of engines) {
|
||||
let queries: DataQuery[] | null = null;
|
||||
let queries: CameraQuery[] | null = null;
|
||||
/* istanbul ignore else: the else path cannot be reached -- @preserve */
|
||||
if (QueryClassifier.isEventQuery(partialQuery)) {
|
||||
if (CameraQueryClassifier.isEventQuery(partialQuery)) {
|
||||
queries = engine.generateDefaultEventQuery(this._store, cameraIDs, partialQuery);
|
||||
} else if (QueryClassifier.isRecordingQuery(partialQuery)) {
|
||||
} else if (CameraQueryClassifier.isRecordingQuery(partialQuery)) {
|
||||
queries = engine.generateDefaultRecordingQuery(
|
||||
this._store,
|
||||
cameraIDs,
|
||||
partialQuery,
|
||||
);
|
||||
} else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
|
||||
} else if (CameraQueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
|
||||
queries = engine.generateDefaultRecordingSegmentsQuery(
|
||||
this._store,
|
||||
cameraIDs,
|
||||
@@ -426,7 +428,7 @@ export class CameraManager {
|
||||
|
||||
public async extendMediaQueries<T extends MediaQuery>(
|
||||
queries: T[],
|
||||
results: ViewMedia[],
|
||||
results: ViewItem[],
|
||||
direction: 'earlier' | 'later',
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<ExtendedMediaQueryResult<T> | null> {
|
||||
@@ -438,6 +440,9 @@ export class CameraManager {
|
||||
const getTimeFromResults = (want: 'earliest' | 'latest'): Date | null => {
|
||||
let output: Date | null = null;
|
||||
for (const result of results) {
|
||||
if (!ViewItemClassifier.isMedia(result)) {
|
||||
continue;
|
||||
}
|
||||
const startTime = result.getStartTime();
|
||||
if (
|
||||
startTime &&
|
||||
@@ -495,7 +500,7 @@ export class CameraManager {
|
||||
return null;
|
||||
}
|
||||
|
||||
const outputMedia = sortMedia(results.concat(newChunkMedia));
|
||||
const outputMedia = sortItems(results.concat(newChunkMedia));
|
||||
|
||||
// If the media did not _ACTUALLY_ get longer, there is no new media despite
|
||||
// the increased limit, so just return null.
|
||||
@@ -509,7 +514,7 @@ export class CameraManager {
|
||||
};
|
||||
}
|
||||
|
||||
public async getMediaDownloadPath(media: ViewMedia): Promise<CameraEndpoint | null> {
|
||||
public async getMediaDownloadPath(media: ViewMedia): Promise<Endpoint | null> {
|
||||
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
||||
const engine = this._store.getEngineForMedia(media);
|
||||
const hass = this._api.getHASSManager().getHASS();
|
||||
@@ -520,7 +525,7 @@ export class CameraManager {
|
||||
return await engine.getMediaDownloadPath(hass, cameraConfig, media);
|
||||
}
|
||||
|
||||
public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null {
|
||||
public getMediaCapabilities(media: ViewMedia): ViewItemCapabilities | null {
|
||||
const engine = this._store.getEngineForMedia(media);
|
||||
if (!engine) {
|
||||
return null;
|
||||
@@ -603,7 +608,7 @@ export class CameraManager {
|
||||
);
|
||||
}
|
||||
|
||||
protected async _handleQuery<QT extends DataQuery>(
|
||||
protected async _handleQuery<QT extends CameraQuery>(
|
||||
query: QT | QT[],
|
||||
engineOptions?: EngineOptions,
|
||||
): Promise<Map<QT, QueryReturnType<QT>>> {
|
||||
@@ -623,28 +628,28 @@ export class CameraManager {
|
||||
let engineResult: Map<QT, QueryReturnType<QT>> | null = null;
|
||||
|
||||
/* istanbul ignore else: the else path cannot be reached -- @preserve */
|
||||
if (QueryClassifier.isEventQuery(query)) {
|
||||
if (CameraQueryClassifier.isEventQuery(query)) {
|
||||
engineResult = (await engine.getEvents(
|
||||
hass,
|
||||
this._store,
|
||||
query,
|
||||
engineOptions,
|
||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||
} else if (QueryClassifier.isRecordingQuery(query)) {
|
||||
} else if (CameraQueryClassifier.isRecordingQuery(query)) {
|
||||
engineResult = (await engine.getRecordings(
|
||||
hass,
|
||||
this._store,
|
||||
query,
|
||||
engineOptions,
|
||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||
} else if (QueryClassifier.isRecordingSegmentsQuery(query)) {
|
||||
} else if (CameraQueryClassifier.isRecordingSegmentsQuery(query)) {
|
||||
engineResult = (await engine.getRecordingSegments(
|
||||
hass,
|
||||
this._store,
|
||||
query,
|
||||
engineOptions,
|
||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||
} else if (QueryClassifier.isMediaMetadataQuery(query)) {
|
||||
} else if (CameraQueryClassifier.isMediaMetadataQuery(query)) {
|
||||
engineResult = (await engine.getMediaMetadata(
|
||||
hass,
|
||||
this._store,
|
||||
@@ -697,7 +702,7 @@ export class CameraManager {
|
||||
return results;
|
||||
}
|
||||
|
||||
protected _convertQueryResultsToMedia<QT extends DataQuery>(
|
||||
protected _convertQueryResultsToMedia<QT extends CameraQuery>(
|
||||
results: ResultsMap<QT>,
|
||||
): ViewMedia[] {
|
||||
const mediaArray: ViewMedia[] = [];
|
||||
@@ -714,12 +719,12 @@ export class CameraManager {
|
||||
let media: ViewMedia[] | null = null;
|
||||
/* istanbul ignore else: the else path cannot be reached -- @preserve */
|
||||
if (
|
||||
QueryClassifier.isEventQuery(query) &&
|
||||
CameraQueryClassifier.isEventQuery(query) &&
|
||||
QueryResultClassifier.isEventQueryResult(result)
|
||||
) {
|
||||
media = engine.generateMediaFromEvents(hass, this._store, query, result);
|
||||
} else if (
|
||||
QueryClassifier.isRecordingQuery(query) &&
|
||||
CameraQueryClassifier.isRecordingQuery(query) &&
|
||||
QueryResultClassifier.isRecordingQueryResult(result)
|
||||
) {
|
||||
media = engine.generateMediaFromRecordings(hass, this._store, query, result);
|
||||
@@ -729,7 +734,7 @@ export class CameraManager {
|
||||
}
|
||||
}
|
||||
}
|
||||
return sortMedia(mediaArray);
|
||||
return sortItems(mediaArray);
|
||||
}
|
||||
|
||||
public getCameraEndpoints(
|
||||
|
||||
@@ -1,32 +1,29 @@
|
||||
import { add, endOfDay, parse, startOfDay } from 'date-fns';
|
||||
import orderBy from 'lodash-es/orderBy';
|
||||
import { orderBy } from 'lodash-es';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { allPromises, formatDate, isValidDate } from '../../utils/basic';
|
||||
import {
|
||||
BrowseMediaStep,
|
||||
BrowseMediaTarget,
|
||||
} from '../../utils/ha/browse-media/browse-media-manager';
|
||||
import { getViewMediaFromBrowseMediaArray } from '../../ha/browse-media/browse-media-to-view-media';
|
||||
import {
|
||||
BROWSE_MEDIA_CACHE_SECONDS,
|
||||
BrowseMedia,
|
||||
BrowseMediaCache,
|
||||
BrowseMediaMetadata,
|
||||
MEDIA_CLASS_IMAGE,
|
||||
MEDIA_CLASS_VIDEO,
|
||||
RichBrowseMedia,
|
||||
} from '../../utils/ha/browse-media/types';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
} from '../../ha/browse-media/types';
|
||||
import { BrowseMediaStep, BrowseMediaTarget } from '../../ha/browse-media/walker';
|
||||
import { isMediaWithinDates } from '../../ha/browse-media/within-dates';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { Endpoint } from '../../types';
|
||||
import { allPromises, formatDate, isValidDate } from '../../utils/basic';
|
||||
import { ViewMedia } from '../../view/item';
|
||||
import { BrowseMediaCamera } from '../browse-media/camera';
|
||||
import { BrowseMediaCameraManagerEngine } from '../browse-media/engine-browse-media';
|
||||
import { BrowseMediaMetadata } from '../browse-media/types';
|
||||
import { getViewMediaFromBrowseMediaArray } from '../browse-media/utils/browse-media-to-view-media';
|
||||
import { isMediaWithinDates } from '../browse-media/utils/within-dates';
|
||||
import { MemoryRequestCache } from '../cache';
|
||||
import { Camera } from '../camera';
|
||||
import { Capabilities } from '../capabilities';
|
||||
import { CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT } from '../engine';
|
||||
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||
import {
|
||||
CameraEndpoint,
|
||||
CameraEndpoints,
|
||||
CameraEndpointsContext,
|
||||
CameraManagerCameraMetadata,
|
||||
@@ -67,8 +64,8 @@ const MOTIONEYE_REPL_SUBSTITUTIONS: Record<string, string> = {
|
||||
const MOTIONEYE_REPL_REGEXP = new RegExp(/(%Y|%m|%d|%H|%M|%S)/g);
|
||||
|
||||
export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
protected _directoryCache = new MemoryRequestCache<string, BrowseMedia>();
|
||||
protected _fileCache = new MemoryRequestCache<string, BrowseMedia>();
|
||||
protected _directoryCache = new BrowseMediaCache<BrowseMediaMetadata>();
|
||||
protected _fileCache = new BrowseMediaCache<BrowseMediaMetadata>();
|
||||
|
||||
public getEngineType(): Engine {
|
||||
return Engine.MotionEye;
|
||||
@@ -224,7 +221,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
};
|
||||
|
||||
// For motionEye snapshots and clips are mutually exclusive.
|
||||
return await this._browseMediaManager.walkBrowseMedias(
|
||||
return await this._browseMediaWalker.walk(
|
||||
hass,
|
||||
[
|
||||
...(matchOptions?.hasClip !== false && !matchOptions?.hasSnapshot
|
||||
@@ -291,7 +288,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
);
|
||||
|
||||
const limit = perCameraQuery.limit ?? CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT;
|
||||
const media = await this._browseMediaManager.walkBrowseMedias(
|
||||
const media = await this._browseMediaWalker.walk(
|
||||
hass,
|
||||
[
|
||||
{
|
||||
@@ -431,7 +428,7 @@ export class MotionEyeCameraManagerEngine extends BrowseMediaCameraManagerEngine
|
||||
cameraConfig: CameraConfig,
|
||||
context?: CameraEndpointsContext,
|
||||
): CameraEndpoints | null {
|
||||
const getUIEndpoint = (): CameraEndpoint | null => {
|
||||
const getUIEndpoint = (): Endpoint | null => {
|
||||
return cameraConfig.motioneye?.url
|
||||
? {
|
||||
endpoint: cameraConfig.motioneye.url,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { RichBrowseMedia } from '../../utils/ha/browse-media/types';
|
||||
import { BrowseMediaMetadata } from '../browse-media/types';
|
||||
import { BrowseMediaMetadata, RichBrowseMedia } from '../../ha/browse-media/types';
|
||||
import { Engine, EventQueryResults } from '../types';
|
||||
|
||||
// ================================
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import orderBy from 'lodash-es/orderBy';
|
||||
import { orderBy } from 'lodash-es';
|
||||
|
||||
interface Range<T extends Date | number> {
|
||||
start: T;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ActionsExecutor } from '../../card-controller/actions/types';
|
||||
import { PTZAction, PTZActionPhase } from '../../config/schema/actions/custom/ptz';
|
||||
import { Entity, EntityRegistryManager } from '../../ha/registry/entity/types';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { localize } from '../../localize/localize';
|
||||
import { PTZCapabilities, PTZMovementType } from '../../types';
|
||||
import { createSelectOptionAction } from '../../utils/action.js';
|
||||
import { Entity, EntityRegistryManager } from '../../utils/ha/registry/entity/types';
|
||||
import { BrowseMediaCamera } from '../browse-media/camera';
|
||||
import { Camera, CameraInitializationOptions } from '../camera';
|
||||
import { Capabilities } from '../capabilities';
|
||||
|
||||
@@ -1,26 +1,26 @@
|
||||
import { add, endOfDay, parse, startOfDay } from 'date-fns';
|
||||
import { orderBy } from 'lodash-es';
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { allPromises, formatDate, isValidDate } from '../../utils/basic';
|
||||
import { sortMediaByStartDate } from '../../utils/ha/browse-media/browse-media-manager';
|
||||
import { getViewMediaFromBrowseMediaArray } from '../../ha/browse-media/browse-media-to-view-media';
|
||||
import { sortMediaByStartDate } from '../../ha/browse-media/sort-browse-media-by-start-date';
|
||||
import {
|
||||
BROWSE_MEDIA_CACHE_SECONDS,
|
||||
BrowseMedia,
|
||||
BrowseMediaCache,
|
||||
BrowseMediaMetadata,
|
||||
MEDIA_CLASS_VIDEO,
|
||||
RichBrowseMedia,
|
||||
} from '../../utils/ha/browse-media/types';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
} from '../../ha/browse-media/types';
|
||||
import { isMediaWithinDates } from '../../ha/browse-media/within-dates';
|
||||
import { HomeAssistant } from '../../ha/types';
|
||||
import { Endpoint } from '../../types';
|
||||
import { allPromises, formatDate, isValidDate } from '../../utils/basic';
|
||||
import { ViewMedia } from '../../view/item';
|
||||
import { BrowseMediaCameraManagerEngine } from '../browse-media/engine-browse-media';
|
||||
import { BrowseMediaMetadata } from '../browse-media/types';
|
||||
import { getViewMediaFromBrowseMediaArray } from '../browse-media/utils/browse-media-to-view-media';
|
||||
import { isMediaWithinDates } from '../browse-media/utils/within-dates';
|
||||
import { MemoryRequestCache } from '../cache';
|
||||
import { Camera } from '../camera';
|
||||
import { CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT } from '../engine';
|
||||
import { CameraManagerReadOnlyConfigStore } from '../store';
|
||||
import {
|
||||
CameraEndpoint,
|
||||
CameraEndpoints,
|
||||
CameraEndpointsContext,
|
||||
CameraManagerCameraMetadata,
|
||||
@@ -48,7 +48,8 @@ export class ReolinkQueryResultsClassifier {
|
||||
}
|
||||
|
||||
export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
protected _cache = new MemoryRequestCache<string, BrowseMedia>();
|
||||
protected _camerasCache = new BrowseMediaCache<BrowseMediaReolinkCameraMetadata>();
|
||||
protected _cache = new BrowseMediaCache<BrowseMediaMetadata>();
|
||||
|
||||
public getEngineType(): Engine {
|
||||
return Engine.Reolink;
|
||||
@@ -172,7 +173,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
// that match the expected camera. Some Reolink cameras will not show up
|
||||
// here causing errors.
|
||||
// https://github.com/dermotduffy/advanced-camera-card/issues/1723
|
||||
const camerasWithMedia = await this._browseMediaManager.walkBrowseMedias(
|
||||
const camerasWithMedia = await this._browseMediaWalker.walk(
|
||||
hass,
|
||||
[
|
||||
{
|
||||
@@ -188,7 +189,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
},
|
||||
],
|
||||
{
|
||||
...(engineOptions?.useCache !== false && { cache: this._cache }),
|
||||
...(engineOptions?.useCache !== false && { cache: this._camerasCache }),
|
||||
},
|
||||
);
|
||||
|
||||
@@ -196,7 +197,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
return null;
|
||||
}
|
||||
|
||||
return await this._browseMediaManager.walkBrowseMedias(
|
||||
return await this._browseMediaWalker.walk(
|
||||
hass,
|
||||
[
|
||||
{
|
||||
@@ -263,7 +264,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
let media: RichBrowseMedia<BrowseMediaMetadata>[] = [];
|
||||
|
||||
if (directories?.length) {
|
||||
media = await this._browseMediaManager.walkBrowseMedias(
|
||||
media = await this._browseMediaWalker.walk(
|
||||
hass,
|
||||
[
|
||||
{
|
||||
@@ -395,7 +396,7 @@ export class ReolinkCameraManagerEngine extends BrowseMediaCameraManagerEngine {
|
||||
cameraConfig: CameraConfig,
|
||||
context?: CameraEndpointsContext,
|
||||
): CameraEndpoints | null {
|
||||
const getUIEndpoint = (): CameraEndpoint | null => {
|
||||
const getUIEndpoint = (): Endpoint | null => {
|
||||
return cameraConfig.reolink?.url
|
||||
? {
|
||||
endpoint: cameraConfig.reolink.url,
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { RichBrowseMedia } from '../../utils/ha/browse-media/types';
|
||||
import { BrowseMediaMetadata } from '../browse-media/types';
|
||||
import { BrowseMediaMetadata, RichBrowseMedia } from '../../ha/browse-media/types';
|
||||
import { Engine, EventQueryResults } from '../types';
|
||||
|
||||
export interface BrowseMediaReolinkCameraMetadata {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { CameraConfig } from '../config/schema/cameras';
|
||||
import { CapabilityKey } from '../types';
|
||||
import { allPromises } from '../utils/basic';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { ViewMedia } from '../view/item';
|
||||
import { Camera } from './camera';
|
||||
import { CameraManagerEngine } from './engine';
|
||||
import { CapabilitySearchOptions, Engine } from './types';
|
||||
@@ -133,7 +133,8 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
||||
}
|
||||
|
||||
public getCameraConfigForMedia(media: ViewMedia): CameraConfig | null {
|
||||
return this.getCameraConfig(media.getCameraID());
|
||||
const cameraID = media.getCameraID();
|
||||
return cameraID ? this.getCameraConfig(cameraID) : null;
|
||||
}
|
||||
|
||||
public getEngineOfType(engine: Engine): CameraManagerEngine | null {
|
||||
@@ -163,7 +164,8 @@ export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
||||
}
|
||||
|
||||
public getEngineForMedia(media: ViewMedia): CameraManagerEngine | null {
|
||||
return this.getEngineForCameraID(media.getCameraID());
|
||||
const cameraID = media.getCameraID();
|
||||
return cameraID ? this.getEngineForCameraID(cameraID) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+17
-21
@@ -1,7 +1,8 @@
|
||||
import { ExpiringEqualityCache } from '../cache/expiring-cache';
|
||||
import { SSLCiphers } from '../config/schema/cameras';
|
||||
import { AdvancedCameraCardView } from '../config/schema/common/const';
|
||||
import { CapabilityKey, Icon } from '../types';
|
||||
import { ViewMedia } from '../view/media';
|
||||
import { CapabilityKey, Endpoint, Icon } from '../types';
|
||||
import { ViewMedia } from '../view/item';
|
||||
|
||||
// ====
|
||||
// Base
|
||||
@@ -28,11 +29,11 @@ export enum Engine {
|
||||
Reolink = 'reolink',
|
||||
}
|
||||
|
||||
export interface DataQuery {
|
||||
export interface CameraQuery {
|
||||
type: QueryType;
|
||||
cameraIDs: Set<string>;
|
||||
}
|
||||
export type PartialDataQuery = Partial<DataQuery>;
|
||||
export type PartialCameraQuery = Partial<CameraQuery>;
|
||||
|
||||
interface TimeBasedDataQuery {
|
||||
start: Date;
|
||||
@@ -44,7 +45,7 @@ interface LimitedDataQuery {
|
||||
}
|
||||
|
||||
export interface MediaQuery
|
||||
extends DataQuery,
|
||||
extends CameraQuery,
|
||||
Partial<TimeBasedDataQuery>,
|
||||
Partial<LimitedDataQuery> {
|
||||
favorite?: boolean;
|
||||
@@ -100,11 +101,6 @@ interface CapabilitySearchAllAny {
|
||||
}
|
||||
export type CapabilitySearchOptions = CapabilityKey | CapabilitySearchAllAny;
|
||||
|
||||
export interface CameraManagerMediaCapabilities {
|
||||
canFavorite: boolean;
|
||||
canDownload: boolean;
|
||||
}
|
||||
|
||||
export interface CameraManagerCameraMetadata {
|
||||
title: string;
|
||||
icon: Icon;
|
||||
@@ -118,16 +114,11 @@ export interface CameraEndpointsContext {
|
||||
view?: AdvancedCameraCardView;
|
||||
}
|
||||
|
||||
export interface CameraEndpoint {
|
||||
endpoint: string;
|
||||
sign?: boolean;
|
||||
}
|
||||
|
||||
export interface CameraEndpoints {
|
||||
ui?: CameraEndpoint;
|
||||
go2rtc?: CameraEndpoint;
|
||||
jsmpeg?: CameraEndpoint;
|
||||
webrtcCard?: CameraEndpoint;
|
||||
ui?: Endpoint;
|
||||
go2rtc?: Endpoint;
|
||||
jsmpeg?: Endpoint;
|
||||
webrtcCard?: Endpoint;
|
||||
}
|
||||
|
||||
export interface CameraProxyConfig {
|
||||
@@ -157,6 +148,11 @@ export interface CameraEvent {
|
||||
}
|
||||
export type CameraEventCallback = (ev: CameraEvent) => void;
|
||||
|
||||
export class CameraManagerRequestCache extends ExpiringEqualityCache<
|
||||
CameraQuery,
|
||||
QueryResults
|
||||
> {}
|
||||
|
||||
// ===========
|
||||
// Event Query
|
||||
// ===========
|
||||
@@ -202,7 +198,7 @@ export interface RecordingQueryResults extends QueryResults {
|
||||
// Recording Segments Query
|
||||
// ========================
|
||||
|
||||
export interface RecordingSegmentsQuery extends DataQuery, TimeBasedDataQuery {
|
||||
export interface RecordingSegmentsQuery extends CameraQuery, TimeBasedDataQuery {
|
||||
type: QueryType.RecordingSegments;
|
||||
}
|
||||
export type PartialRecordingSegmentsQuery = Partial<RecordingSegmentsQuery>;
|
||||
@@ -216,7 +212,7 @@ export interface RecordingSegmentsQueryResults extends QueryResults {
|
||||
// Media metadata Query
|
||||
// ====================
|
||||
|
||||
export interface MediaMetadataQuery extends DataQuery {
|
||||
export interface MediaMetadataQuery extends CameraQuery {
|
||||
type: QueryType.MediaMetadata;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CameraConfig } from '../../config/schema/cameras';
|
||||
import { CameraEndpoint } from '../types';
|
||||
import { Endpoint } from '../../types';
|
||||
|
||||
export const getDefaultGo2RTCEndpoint = (
|
||||
cameraConfig: CameraConfig,
|
||||
@@ -7,7 +7,7 @@ export const getDefaultGo2RTCEndpoint = (
|
||||
url?: string;
|
||||
stream?: string;
|
||||
},
|
||||
): CameraEndpoint | null => {
|
||||
): Endpoint | null => {
|
||||
const url = options?.url ?? cameraConfig.go2rtc?.url;
|
||||
const stream = options?.stream ?? cameraConfig.go2rtc?.stream;
|
||||
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import orderBy from 'lodash-es/orderBy';
|
||||
import uniqBy from 'lodash-es/uniqBy';
|
||||
import { ViewMedia } from '../../view/media';
|
||||
|
||||
export const sortMedia = (mediaArray: ViewMedia[]): ViewMedia[] => {
|
||||
return orderBy(
|
||||
// Ensure uniqueness by the ID (if specified), otherwise all elements
|
||||
// are assumed to be unique.
|
||||
uniqBy(mediaArray, (media) => media.getID() ?? media),
|
||||
|
||||
// Sort all items leading oldest -> youngest (so media is loaded in this
|
||||
// order in the viewer which matches the left-to-right timeline order).
|
||||
(media) => media.getStartTime() ?? media.getID(),
|
||||
'asc',
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user