Fetch metadata from engine.
This commit is contained in:
@@ -62,6 +62,13 @@ export class CameraManagerEngineFactory {
|
|||||||
}
|
}
|
||||||
output.get(engine)?.add(cameraID);
|
output.get(engine)?.add(cameraID);
|
||||||
}
|
}
|
||||||
return output;
|
return output.size ? output : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllEngines(
|
||||||
|
cameras: Map<string, CameraConfig>,
|
||||||
|
): CameraManagerEngine[] | null {
|
||||||
|
const engines = this.getEnginesForCameraIDs(cameras, new Set(cameras.keys()));
|
||||||
|
return engines ? [...engines.keys()] : null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
DataQuery,
|
DataQuery,
|
||||||
EventQuery,
|
EventQuery,
|
||||||
EventQueryResultsMap,
|
EventQueryResultsMap,
|
||||||
|
MediaMetadata,
|
||||||
PartialEventQuery,
|
PartialEventQuery,
|
||||||
PartialRecordingQuery,
|
PartialRecordingQuery,
|
||||||
PartialRecordingSegmentsQuery,
|
PartialRecordingSegmentsQuery,
|
||||||
@@ -75,9 +76,7 @@ export interface CameraManagerEngine {
|
|||||||
favorite: boolean,
|
favorite: boolean,
|
||||||
): Promise<void>;
|
): Promise<void>;
|
||||||
|
|
||||||
getQueryResultMaxAge(
|
getQueryResultMaxAge(query: DataQuery): number | null;
|
||||||
query: DataQuery
|
|
||||||
): number | null;
|
|
||||||
|
|
||||||
getMediaSeekTime(
|
getMediaSeekTime(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@@ -85,4 +84,9 @@ export interface CameraManagerEngine {
|
|||||||
media: ViewMedia,
|
media: ViewMedia,
|
||||||
target: Date,
|
target: Date,
|
||||||
): Promise<number | null>;
|
): Promise<number | null>;
|
||||||
|
|
||||||
|
getMediaMetadata(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cameras: Map<string, CameraConfig>,
|
||||||
|
): Promise<MediaMetadata | null>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import {
|
|||||||
FrigateEventQueryResults,
|
FrigateEventQueryResults,
|
||||||
FrigateRecordingQueryResults,
|
FrigateRecordingQueryResults,
|
||||||
FrigateRecordingSegmentsQueryResults,
|
FrigateRecordingSegmentsQueryResults,
|
||||||
|
MediaMetadata,
|
||||||
PartialEventQuery,
|
PartialEventQuery,
|
||||||
PartialRecordingQuery,
|
PartialRecordingQuery,
|
||||||
PartialRecordingSegmentsQuery,
|
PartialRecordingSegmentsQuery,
|
||||||
@@ -36,6 +37,7 @@ import {
|
|||||||
import { FrigateRecording } from './types';
|
import { FrigateRecording } from './types';
|
||||||
import {
|
import {
|
||||||
getEvents,
|
getEvents,
|
||||||
|
getEventSummary,
|
||||||
getRecordingSegments,
|
getRecordingSegments,
|
||||||
getRecordingsSummary,
|
getRecordingsSummary,
|
||||||
NativeFrigateEventQuery,
|
NativeFrigateEventQuery,
|
||||||
@@ -44,7 +46,7 @@ import {
|
|||||||
} from './requests';
|
} from './requests';
|
||||||
import orderBy from 'lodash-es/orderBy';
|
import orderBy from 'lodash-es/orderBy';
|
||||||
import throttle from 'lodash-es/throttle';
|
import throttle from 'lodash-es/throttle';
|
||||||
import { runWhenIdleIfSupported } from '../../utils/basic';
|
import { allPromises, runWhenIdleIfSupported } from '../../utils/basic';
|
||||||
import { fromUnixTime } from 'date-fns';
|
import { fromUnixTime } from 'date-fns';
|
||||||
import { sum } from 'lodash-es';
|
import { sum } from 'lodash-es';
|
||||||
import { FrigateViewMediaClassifier } from './media-classifier';
|
import { FrigateViewMediaClassifier } from './media-classifier';
|
||||||
@@ -214,10 +216,10 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
protected _buildInstanceToCameraIDMapFromQuery(
|
protected _buildInstanceToCameraIDMapFromQuery(
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: Map<string, CameraConfig>,
|
||||||
query: DataQuery,
|
cameraIDs: Set<string>,
|
||||||
): Map<string, Set<string>> {
|
): Map<string, Set<string>> {
|
||||||
const output: Map<string, Set<string>> = new Map();
|
const output: Map<string, Set<string>> = new Map();
|
||||||
for (const cameraID of query.cameraIDs) {
|
for (const cameraID of cameraIDs) {
|
||||||
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
const cameraConfig = this._getQueryableCameraConfig(cameras, cameraID);
|
||||||
const clientID = cameraConfig?.frigate.client_id;
|
const clientID = cameraConfig?.frigate.client_id;
|
||||||
if (clientID) {
|
if (clientID) {
|
||||||
@@ -295,7 +297,10 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
// Frigate allows multiple cameras to be searched for events in a single
|
// Frigate allows multiple cameras to be searched for events in a single
|
||||||
// query. Break them down into groups of cameras per Frigate instance, then
|
// query. Break them down into groups of cameras per Frigate instance, then
|
||||||
// query once per instance for all cameras in that instance.
|
// query once per instance for all cameras in that instance.
|
||||||
const instances = this._buildInstanceToCameraIDMapFromQuery(cameras, query);
|
const instances = this._buildInstanceToCameraIDMapFromQuery(
|
||||||
|
cameras,
|
||||||
|
query.cameraIDs,
|
||||||
|
);
|
||||||
|
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
Array.from(instances.keys()).map((instanceID) =>
|
Array.from(instances.keys()).map((instanceID) =>
|
||||||
@@ -611,6 +616,56 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
return cameraConfig;
|
return cameraConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getMediaMetadata(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cameras: Map<string, CameraConfig>,
|
||||||
|
): Promise<MediaMetadata | null> {
|
||||||
|
const what: Set<string> = new Set();
|
||||||
|
const where: Set<string> = new Set();
|
||||||
|
const days: Set<string> = new Set();
|
||||||
|
|
||||||
|
const instances = this._buildInstanceToCameraIDMapFromQuery(
|
||||||
|
cameras,
|
||||||
|
new Set(cameras.keys()),
|
||||||
|
);
|
||||||
|
|
||||||
|
const processQuery = async (
|
||||||
|
instanceID: string,
|
||||||
|
cameraIDs: Set<string>,
|
||||||
|
): Promise<void> => {
|
||||||
|
const cameraNames = this._getFrigateCameraNamesForCameraIDs(cameras, cameraIDs);
|
||||||
|
for (const entry of await getEventSummary(hass, instanceID)) {
|
||||||
|
if (!cameraNames.has(entry.camera)) {
|
||||||
|
// If this entry applies to a camera that *is* in this Frigate
|
||||||
|
// instance, but is *not* a configured camera in the card, skip it.
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.label) {
|
||||||
|
what.add(entry.label);
|
||||||
|
}
|
||||||
|
if (entry.zones.length) {
|
||||||
|
entry.zones.forEach(where.add, where);
|
||||||
|
}
|
||||||
|
if (entry.day) {
|
||||||
|
days.add(entry.day);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
await allPromises([...instances.entries()], ([instanceID, cameraIDs]) =>
|
||||||
|
processQuery(instanceID, cameraIDs),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!what.size && !where.size && !days.size) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...(what.size && { what: what }),
|
||||||
|
...(where.size && { where: where }),
|
||||||
|
...(days.size && { days: days }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Garbage collect recording segments that no longer feature in the recordings
|
* Garbage collect recording segments that no longer feature in the recordings
|
||||||
* returned by the Frigate backend.
|
* returned by the Frigate backend.
|
||||||
|
|||||||
@@ -1,22 +1,29 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { localize } from '../../localize/localize';
|
import { localize } from '../../localize/localize';
|
||||||
import {
|
import { FrigateCardError, RecordingSegment } from '../../types';
|
||||||
FrigateCardError,
|
|
||||||
RecordingSegment,
|
|
||||||
} from '../../types';
|
|
||||||
import { homeAssistantWSRequest } from '../../utils/ha';
|
import { homeAssistantWSRequest } from '../../utils/ha';
|
||||||
import { FrigateEvent, frigateEventsSchema, recordingSegmentsSchema, RecordingSummary, recordingSummarySchema, RetainResult, retainResultSchema } from './types';
|
import {
|
||||||
|
EventSummary,
|
||||||
|
eventSummarySchema,
|
||||||
|
FrigateEvent,
|
||||||
|
frigateEventsSchema,
|
||||||
|
recordingSegmentsSchema,
|
||||||
|
RecordingSummary,
|
||||||
|
recordingSummarySchema,
|
||||||
|
RetainResult,
|
||||||
|
retainResultSchema,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the recordings summary. May throw.
|
* Get the recordings summary. May throw.
|
||||||
* @param hass The Home Assistant object.
|
* @param hass The Home Assistant object.
|
||||||
* @param client_id The Frigate client_id.
|
* @param clientID The Frigate clientID.
|
||||||
* @param camera_name The Frigate camera name.
|
* @param camera_name The Frigate camera name.
|
||||||
* @returns A RecordingSummary object.
|
* @returns A RecordingSummary object.
|
||||||
*/
|
*/
|
||||||
export const getRecordingsSummary = async (
|
export const getRecordingsSummary = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
client_id: string,
|
clientID: string,
|
||||||
camera_name: string,
|
camera_name: string,
|
||||||
): Promise<RecordingSummary> => {
|
): Promise<RecordingSummary> => {
|
||||||
return await homeAssistantWSRequest(
|
return await homeAssistantWSRequest(
|
||||||
@@ -24,7 +31,7 @@ export const getRecordingsSummary = async (
|
|||||||
recordingSummarySchema,
|
recordingSummarySchema,
|
||||||
{
|
{
|
||||||
type: 'frigate/recordings/summary',
|
type: 'frigate/recordings/summary',
|
||||||
instance_id: client_id,
|
instance_id: clientID,
|
||||||
camera: camera_name,
|
camera: camera_name,
|
||||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
},
|
},
|
||||||
@@ -63,19 +70,19 @@ export const getRecordingSegments = async (
|
|||||||
/**
|
/**
|
||||||
* Request that Frigate retain an event. May throw.
|
* Request that Frigate retain an event. May throw.
|
||||||
* @param hass The HomeAssistant object.
|
* @param hass The HomeAssistant object.
|
||||||
* @param client_id The Frigate client_id.
|
* @param clientID The Frigate clientID.
|
||||||
* @param eventID The event ID to retain.
|
* @param eventID The event ID to retain.
|
||||||
* @param retain `true` to retain or `false` to unretain.
|
* @param retain `true` to retain or `false` to unretain.
|
||||||
*/
|
*/
|
||||||
export async function retainEvent(
|
export async function retainEvent(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
client_id: string,
|
clientID: string,
|
||||||
eventID: string,
|
eventID: string,
|
||||||
retain: boolean,
|
retain: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const retainRequest = {
|
const retainRequest = {
|
||||||
type: 'frigate/event/retain',
|
type: 'frigate/event/retain',
|
||||||
instance_id: client_id,
|
instance_id: clientID,
|
||||||
event_id: eventID,
|
event_id: eventID,
|
||||||
retain: retain,
|
retain: retain,
|
||||||
};
|
};
|
||||||
@@ -126,3 +133,19 @@ export const getEvents = async (
|
|||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getEventSummary = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
clientID: string,
|
||||||
|
): Promise<EventSummary> => {
|
||||||
|
return await homeAssistantWSRequest(
|
||||||
|
hass,
|
||||||
|
eventSummarySchema,
|
||||||
|
{
|
||||||
|
type: 'frigate/events/summary',
|
||||||
|
instance_id: clientID,
|
||||||
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
import { dayToDate } from '../../utils/basic';
|
||||||
|
|
||||||
|
const dayStringToDate = (arg: unknown): Date | unknown => {
|
||||||
|
return typeof arg === 'string' ? dayToDate(arg) : arg;
|
||||||
|
};
|
||||||
|
|
||||||
export const eventSchema = z.object({
|
export const eventSchema = z.object({
|
||||||
camera: z.string(),
|
camera: z.string(),
|
||||||
@@ -25,11 +30,7 @@ const recordingSummaryHourSchema = z.object({
|
|||||||
|
|
||||||
export const recordingSummarySchema = z
|
export const recordingSummarySchema = z
|
||||||
.object({
|
.object({
|
||||||
day: z.preprocess((arg) => {
|
day: z.preprocess(dayStringToDate, z.date()),
|
||||||
// Must provide the hour:minute:second on parsing or Javascript will
|
|
||||||
// assume *UTC* midnight.
|
|
||||||
return typeof arg === 'string' ? new Date(`${arg}T00:00:00`) : arg;
|
|
||||||
}, z.date()),
|
|
||||||
events: z.number(),
|
events: z.number(),
|
||||||
hours: recordingSummaryHourSchema.array(),
|
hours: recordingSummaryHourSchema.array(),
|
||||||
})
|
})
|
||||||
@@ -54,4 +55,14 @@ export interface FrigateRecording {
|
|||||||
startTime: Date;
|
startTime: Date;
|
||||||
endTime: Date;
|
endTime: Date;
|
||||||
events: number;
|
events: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const eventSummarySchema = z
|
||||||
|
.object({
|
||||||
|
camera: z.string(),
|
||||||
|
day: z.string(),
|
||||||
|
label: z.string(),
|
||||||
|
zones: z.string().array(),
|
||||||
|
})
|
||||||
|
.array();
|
||||||
|
export type EventSummary = z.infer<typeof eventSummarySchema>;
|
||||||
|
|||||||
+41
-1
@@ -1,11 +1,12 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { CameraConfig } from '../types.js';
|
import { CameraConfig } from '../types.js';
|
||||||
import { arrayify, setify } from '../utils/basic.js';
|
import { allPromises, arrayify, setify } from '../utils/basic.js';
|
||||||
import {
|
import {
|
||||||
DataQuery,
|
DataQuery,
|
||||||
EventQuery,
|
EventQuery,
|
||||||
EventQueryResults,
|
EventQueryResults,
|
||||||
EventQueryResultsMap,
|
EventQueryResultsMap,
|
||||||
|
MediaMetadata,
|
||||||
MediaQuery,
|
MediaQuery,
|
||||||
PartialDataQuery,
|
PartialDataQuery,
|
||||||
PartialEventQuery,
|
PartialEventQuery,
|
||||||
@@ -115,6 +116,45 @@ export class CameraManager {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async getMediaMetadata(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
): Promise<MediaMetadata | null> {
|
||||||
|
const what: Set<string> = new Set();
|
||||||
|
const where: Set<string> = new Set();
|
||||||
|
const days: Set<string> = new Set();
|
||||||
|
|
||||||
|
const engines = this._engineFactory.getAllEngines(this._cameras);
|
||||||
|
if (!engines) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const processMetadata = async (engine: CameraManagerEngine): Promise<void> => {
|
||||||
|
const engineMetadata = await engine.getMediaMetadata(hass, this._cameras);
|
||||||
|
if (engineMetadata) {
|
||||||
|
if (engineMetadata.what) {
|
||||||
|
engineMetadata.what.forEach(what.add, what);
|
||||||
|
}
|
||||||
|
if (engineMetadata.where) {
|
||||||
|
engineMetadata.where.forEach(where.add, where);
|
||||||
|
}
|
||||||
|
if (engineMetadata.days) {
|
||||||
|
engineMetadata.days.forEach(days.add, days);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await allPromises(engines, (engine) => processMetadata(engine));
|
||||||
|
|
||||||
|
if (!what.size && !where.size && !days.size) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...(what.size && { what: what }),
|
||||||
|
...(where.size && { where: where }),
|
||||||
|
...(days.size && { days: days }),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected _generateDefaultQueries<PQT extends PartialDataQuery>(
|
protected _generateDefaultQueries<PQT extends PartialDataQuery>(
|
||||||
cameraIDs: string | Set<string>,
|
cameraIDs: string | Set<string>,
|
||||||
partialQuery: PQT,
|
partialQuery: PQT,
|
||||||
|
|||||||
@@ -68,6 +68,12 @@ export type EventQueryResultsMap = ResultsMap<EventQuery>;
|
|||||||
export type RecordingQueryResultsMap = ResultsMap<RecordingQuery>;
|
export type RecordingQueryResultsMap = ResultsMap<RecordingQuery>;
|
||||||
export type RecordingSegmentsQueryResultsMap = ResultsMap<RecordingSegmentsQuery>;
|
export type RecordingSegmentsQueryResultsMap = ResultsMap<RecordingSegmentsQuery>;
|
||||||
|
|
||||||
|
export interface MediaMetadata {
|
||||||
|
where?: Set<string>;
|
||||||
|
what?: Set<string>;
|
||||||
|
days?: Set<string>;
|
||||||
|
}
|
||||||
|
|
||||||
// ===========
|
// ===========
|
||||||
// Event Query
|
// Event Query
|
||||||
// ===========
|
// ===========
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import {
|
|||||||
html,
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
|
ReactiveController,
|
||||||
|
ReactiveControllerHost,
|
||||||
TemplateResult,
|
TemplateResult,
|
||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
@@ -31,6 +33,11 @@ import './timeline-core.js';
|
|||||||
import { EventQuery, QueryType } from '../camera/types';
|
import { EventQuery, QueryType } from '../camera/types';
|
||||||
import { EventMediaQueries } from '../view/media-queries';
|
import { EventMediaQueries } from '../view/media-queries';
|
||||||
import { createViewForEvents } from '../utils/media-to-view.js';
|
import { createViewForEvents } from '../utils/media-to-view.js';
|
||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { prettifyTitle } from '../utils/basic';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import parse from 'date-fns/parse';
|
||||||
|
import endOfMonth from 'date-fns/endOfMonth';
|
||||||
|
|
||||||
@customElement('frigate-card-media-filter')
|
@customElement('frigate-card-media-filter')
|
||||||
export class FrigateCardMediaFilter extends LitElement {
|
export class FrigateCardMediaFilter extends LitElement {
|
||||||
@@ -50,6 +57,7 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
public mediaLimit?: number;
|
public mediaLimit?: number;
|
||||||
|
|
||||||
protected _cameraOptions: ValueLabel<string>[] = [];
|
protected _cameraOptions: ValueLabel<string>[] = [];
|
||||||
|
protected _mediaMetadataController?: MediaMetadataController;
|
||||||
|
|
||||||
protected _convertWhenToDateRange(
|
protected _convertWhenToDateRange(
|
||||||
value?: MediaFilterCoreWhenSelection,
|
value?: MediaFilterCoreWhenSelection,
|
||||||
@@ -127,35 +135,23 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (changedProps.has('cameraManager') && this.hass && this.cameraManager) {
|
||||||
|
this._mediaMetadataController = new MediaMetadataController(
|
||||||
|
this,
|
||||||
|
this.hass,
|
||||||
|
this.cameraManager,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
// TODO Replace with real custom when options
|
|
||||||
// TODO Replace with real custom what options
|
|
||||||
// TODO Replace with real custom where options
|
|
||||||
const whereOptions = [{ value: 'steps', label: 'Front Steps' }];
|
|
||||||
|
|
||||||
const whatOptions = [
|
|
||||||
{ value: 'car', label: 'Car' },
|
|
||||||
{ value: 'person', label: 'Person' },
|
|
||||||
];
|
|
||||||
|
|
||||||
const whenOptions = [
|
|
||||||
{
|
|
||||||
value: {
|
|
||||||
selection: MediaFilterCoreWhen.Custom,
|
|
||||||
custom: { start: startOfToday(), end: endOfToday() },
|
|
||||||
},
|
|
||||||
label: 'December 2021',
|
|
||||||
},
|
|
||||||
];
|
|
||||||
|
|
||||||
return html` <frigate-card-media-filter-core
|
return html` <frigate-card-media-filter-core
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.whenOptions=${whenOptions}
|
.whenOptions=${this._mediaMetadataController?.whenOptions}
|
||||||
.cameraOptions=${this._cameraOptions}
|
.cameraOptions=${this._cameraOptions}
|
||||||
.whatOptions=${whatOptions}
|
.whatOptions=${this._mediaMetadataController?.whatOptions}
|
||||||
.whereOptions=${whereOptions}
|
.whereOptions=${this._mediaMetadataController?.whereOptions}
|
||||||
@frigate-card:media-filter-core:change=${this._mediaFilterHandler.bind(this)}
|
@frigate-card:media-filter-core:change=${this._mediaFilterHandler.bind(this)}
|
||||||
>
|
>
|
||||||
</frigate-card-media-filter-core>`;
|
</frigate-card-media-filter-core>`;
|
||||||
@@ -170,6 +166,65 @@ export class FrigateCardMediaFilter extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class MediaMetadataController implements ReactiveController {
|
||||||
|
protected _host: ReactiveControllerHost;
|
||||||
|
protected _hass: HomeAssistant;
|
||||||
|
protected _cameraManager: CameraManager;
|
||||||
|
|
||||||
|
public whenOptions: ValueLabel<MediaFilterCoreWhenSelection>[] = [];
|
||||||
|
public whatOptions: ValueLabel<string>[] = [];
|
||||||
|
public whereOptions: ValueLabel<string>[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
host: ReactiveControllerHost,
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cameraManager: CameraManager,
|
||||||
|
) {
|
||||||
|
this._host = host;
|
||||||
|
this._hass = hass;
|
||||||
|
this._cameraManager = cameraManager;
|
||||||
|
host.addController(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
async hostConnected() {
|
||||||
|
const metadata = await this._cameraManager.getMediaMetadata(this._hass);
|
||||||
|
if (metadata) {
|
||||||
|
if (metadata.what) {
|
||||||
|
this.whatOptions = [...metadata.what]
|
||||||
|
.sort()
|
||||||
|
.map((what) => ({ value: what, label: prettifyTitle(what) }));
|
||||||
|
}
|
||||||
|
if (metadata.where) {
|
||||||
|
this.whereOptions = [...metadata.where]
|
||||||
|
.sort()
|
||||||
|
.map((where) => ({ value: where, label: prettifyTitle(where) }));
|
||||||
|
}
|
||||||
|
if (metadata.days) {
|
||||||
|
const yearMonths: Set<string> = new Set();
|
||||||
|
[...metadata.days].forEach((day) => {
|
||||||
|
// An efficient conversion: 2023-01-26 -> 2023-01
|
||||||
|
yearMonths.add(day.substring(0, 7));
|
||||||
|
});
|
||||||
|
const monthStarts: Date[] = [];
|
||||||
|
yearMonths.forEach((yearMonth) => {
|
||||||
|
monthStarts.push(parse(yearMonth, 'yyyy-MM', new Date()));
|
||||||
|
});
|
||||||
|
this.whenOptions = monthStarts
|
||||||
|
.sort()
|
||||||
|
.reverse()
|
||||||
|
.map((monthStart) => ({
|
||||||
|
label: format(monthStart, 'MMMM yyyy'),
|
||||||
|
value: {
|
||||||
|
selection: MediaFilterCoreWhen.Custom,
|
||||||
|
custom: { start: monthStart, end: endOfMonth(monthStart) },
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
this._host.requestUpdate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface HTMLElementTagNameMap {
|
interface HTMLElementTagNameMap {
|
||||||
'frigate-card-media-filter': FrigateCardMediaFilter;
|
'frigate-card-media-filter': FrigateCardMediaFilter;
|
||||||
|
|||||||
+19
-2
@@ -32,6 +32,7 @@ export function dispatchFrigateCardEvent<T>(
|
|||||||
* @param input The input Frigate (camera/label/zone) name.
|
* @param input The input Frigate (camera/label/zone) name.
|
||||||
* @returns A prettified name.
|
* @returns A prettified name.
|
||||||
*/
|
*/
|
||||||
|
export function prettifyTitle(input: string): string;
|
||||||
export function prettifyTitle(input?: string): string | undefined {
|
export function prettifyTitle(input?: string): string | undefined {
|
||||||
if (!input) {
|
if (!input) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -158,8 +159,24 @@ export function getDurationString(start: Date, end: Date): string {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* For debug purposes only.
|
* For debug purposes only.
|
||||||
* @param seconds
|
* @param seconds
|
||||||
*/
|
*/
|
||||||
export const sleep = async (seconds: number) => {
|
export const sleep = async (seconds: number) => {
|
||||||
await new Promise(r => setTimeout(r, seconds * 1000));
|
await new Promise((r) => setTimeout(r, seconds * 1000));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const allPromises = async <T>(
|
||||||
|
items: T[],
|
||||||
|
func: (arg: T) => void,
|
||||||
|
): Promise<void> => {
|
||||||
|
await Promise.all(Array.from(items).map((item) => func(item)));
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Simple efficient YYYY-MM-DD -> date converter.
|
||||||
|
*/
|
||||||
|
export const dayToDate = (day: string): Date => {
|
||||||
|
// Must provide the hour:minute:second on parsing or Javascript will assume
|
||||||
|
// *UTC* midnight.
|
||||||
|
return new Date(`${day}T00:00:00`);
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// - TODO: ts-prune https://camchenry.com/blog/deleting-dead-code-in-typescript
|
// - TODO: ts-prune https://camchenry.com/blog/deleting-dead-code-in-typescript
|
||||||
// - TODO: getRecordingTitle should use getCameraTitle but need hass.
|
// - TODO: getRecordingTitle should use getCameraTitle but need hass.
|
||||||
// - TODO: Take MediaQueries wrappers out of the camera manager.
|
// - TODO: Take MediaQueries wrappers out of the camera manager.
|
||||||
|
// - TODO: View a media in the gallery from September, then notice timeline missing the item.
|
||||||
|
|
||||||
// Gallery:
|
// Gallery:
|
||||||
// - TODO: Event gallery show_details default does not work.
|
// - TODO: Event gallery show_details default does not work.
|
||||||
|
|||||||
Reference in New Issue
Block a user