feat: Implement basic general folder support (#2051)

- Related: #1748
This commit is contained in:
Dermot Duffy
2025-05-21 19:59:21 -07:00
committed by GitHub
parent 2eb0d9e35e
commit c6a4c8aea2
350 changed files with 12837 additions and 4509 deletions
+2 -2
View File
@@ -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';
+27 -28
View File
@@ -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 {
+6 -8
View File
@@ -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 -1
View File
@@ -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,