Add 'dependent camera' support.
This commit is contained in:
+219
-50
@@ -1,16 +1,18 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
|
||||
import type {
|
||||
BrowseMediaQueryParametersBase,
|
||||
import {
|
||||
BrowseMediaQueryParameters,
|
||||
FrigateBrowseMediaSource,
|
||||
CameraConfig,
|
||||
MEDIA_CLASS_PLAYLIST,
|
||||
MEDIA_TYPE_PLAYLIST,
|
||||
} from './types.js';
|
||||
import { View } from './view.js';
|
||||
import { frigateBrowseMediaSourceSchema } from './types.js';
|
||||
import {
|
||||
dispatchErrorMessageEvent,
|
||||
dispatchMessageEvent,
|
||||
getCameraTitle,
|
||||
homeAssistantWSRequest,
|
||||
} from './common.js';
|
||||
import { localize } from './localize/localize.js';
|
||||
@@ -73,11 +75,9 @@ export class BrowseMediaUtil {
|
||||
type: 'media_source/browse_media',
|
||||
media_content_id: media_content_id,
|
||||
};
|
||||
return homeAssistantWSRequest(hass, frigateBrowseMediaSourceSchema, request);
|
||||
return await homeAssistantWSRequest(hass, frigateBrowseMediaSourceSchema, request);
|
||||
}
|
||||
|
||||
// Browse Frigate media with query parameters.
|
||||
|
||||
/**
|
||||
* Browse Frigate media with a media query. May throw.
|
||||
* @param hass The HomeAssistant object.
|
||||
@@ -109,13 +109,100 @@ export class BrowseMediaUtil {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Browse multiple Frigate media queries. May throw.
|
||||
* @param hass The HomeAssistant object.
|
||||
* @param params An array of search parameters to use to search for media.
|
||||
* @returns A map of FrigateBrowseMediaSource object or null on malformed.
|
||||
*/
|
||||
static async multipleBrowseMediaQuery(
|
||||
hass: HomeAssistant,
|
||||
params: BrowseMediaQueryParameters | BrowseMediaQueryParameters[],
|
||||
): Promise<Map<BrowseMediaQueryParameters, FrigateBrowseMediaSource>> {
|
||||
params = Array.isArray(params) ? params : [params];
|
||||
const output: Map<BrowseMediaQueryParameters, FrigateBrowseMediaSource> = new Map();
|
||||
await Promise.all(
|
||||
params.map(async (param: BrowseMediaQueryParameters): Promise<void> => {
|
||||
output.set(param, await this.browseMediaQuery(hass, param));
|
||||
}),
|
||||
);
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Browse multiple Frigate media queries, then merged them. May throw.
|
||||
* @param hass The HomeAssistant object.
|
||||
* @param params An array of search parameters to use to search for media.
|
||||
* @returns A single FrigateBrowseMediaSource object or null on malformed.
|
||||
*/
|
||||
static async multipleBrowseMediaQueryMerged(
|
||||
hass: HomeAssistant,
|
||||
params: BrowseMediaQueryParameters | BrowseMediaQueryParameters[],
|
||||
): Promise<FrigateBrowseMediaSource> {
|
||||
return this.mergeFrigateBrowseMediaSources(
|
||||
await this.multipleBrowseMediaQuery(hass, params),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge multiple FrigateBrowseMediaSource into a single. Note that this may
|
||||
* use information from the query to differentiate results that may otherwise
|
||||
* be identical.
|
||||
* @param input A map of query -> result.
|
||||
* @returns A single FrigateBrowseMediaSource object.
|
||||
*/
|
||||
static mergeFrigateBrowseMediaSources(
|
||||
input: Map<BrowseMediaQueryParameters, FrigateBrowseMediaSource>,
|
||||
): FrigateBrowseMediaSource {
|
||||
const children: FrigateBrowseMediaSource[] = [];
|
||||
|
||||
for (const [query, result] of input.entries()) {
|
||||
for (const child of result.children || []) {
|
||||
if (this.isTrueMedia(child)) {
|
||||
children.push(child);
|
||||
} else {
|
||||
if (query.title) {
|
||||
children.push({ ...child, title: `[${query.title}] ${child.title}` });
|
||||
} else {
|
||||
children.push(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const eventSort = (
|
||||
a: FrigateBrowseMediaSource,
|
||||
b: FrigateBrowseMediaSource,
|
||||
): number => {
|
||||
if (
|
||||
!a.frigate?.event ||
|
||||
(b.frigate?.event && b.frigate.event.start_time < a.frigate.event.start_time)
|
||||
) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (
|
||||
!b.frigate?.event ||
|
||||
(a.frigate?.event && b.frigate.event.start_time > a.frigate.event.start_time)
|
||||
) {
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
return this.createEventParentForChildren('Merged events', children.sort(eventSort));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters to search for media.
|
||||
* @returns A BrowseMediaQueryParameters object.
|
||||
*/
|
||||
static getBrowseMediaQueryParametersBase(
|
||||
static getBrowseMediaQueryParameters(
|
||||
hass: HomeAssistant,
|
||||
cameraID: string,
|
||||
cameraConfig?: CameraConfig,
|
||||
): BrowseMediaQueryParametersBase | null {
|
||||
overrides?: Partial<BrowseMediaQueryParameters>,
|
||||
): BrowseMediaQueryParameters | null {
|
||||
if (!cameraConfig || !cameraConfig.camera_name) {
|
||||
return null;
|
||||
}
|
||||
@@ -124,84 +211,143 @@ export class BrowseMediaUtil {
|
||||
cameraName: cameraConfig.camera_name,
|
||||
label: cameraConfig.label,
|
||||
zone: cameraConfig.zone,
|
||||
title: getCameraTitle(hass, cameraConfig),
|
||||
cameraID: cameraID,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the mediaType parameter from the current view.
|
||||
* @param browseMediaQueryParametersBase The base media query parameters object.
|
||||
* @param view The current view.
|
||||
* @returns A fully populated BrowseMediaQueryParameters or null.
|
||||
* Apply overrides to multiple query parameters.
|
||||
* @param parameters An array of query parameters.
|
||||
* @param overrides The overrides to apply.
|
||||
* @returns The override query parameters.
|
||||
*/
|
||||
static setMediaTypeFromView(
|
||||
browseMediaQueryParametersBase: BrowseMediaQueryParametersBase | null,
|
||||
view: View,
|
||||
): BrowseMediaQueryParameters | null {
|
||||
if (
|
||||
!browseMediaQueryParametersBase ||
|
||||
!(view.isClipRelatedView() || view.isSnapshotRelatedView())
|
||||
) {
|
||||
static overrideMultiBrowseMediaQueryParameters(
|
||||
parameters: BrowseMediaQueryParameters[],
|
||||
overrides: Partial<BrowseMediaQueryParameters>,
|
||||
): BrowseMediaQueryParameters[] {
|
||||
const output: BrowseMediaQueryParameters[] = [];
|
||||
parameters.forEach((param) => {
|
||||
output.push({ ...param, ...overrides });
|
||||
});
|
||||
return output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get BrowseMediaQueryParameters for a camera (including its dependencies).
|
||||
* @param hass Home Assistant object.
|
||||
* @param cameras Cameras map.
|
||||
* @param camera Name of the current camera.
|
||||
* @param mediaType Optional media type to include in the parameters.
|
||||
* @returns An array of query parameters.
|
||||
*/
|
||||
static getFullDependentBrowseMediaQueryParameters(
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
camera: string,
|
||||
mediaType?: 'clips' | 'snapshots',
|
||||
): BrowseMediaQueryParameters[] | null {
|
||||
const cameraIDs: Set<string> = new Set();
|
||||
const getDependentCameras = (camera: string): void => {
|
||||
const cameraConfig = cameras.get(camera);
|
||||
if (cameraConfig) {
|
||||
cameraIDs.add(camera);
|
||||
for (const eventCameraID of cameraConfig.dependent_cameras || []) {
|
||||
if (!cameraIDs.has(eventCameraID)) {
|
||||
getDependentCameras(eventCameraID);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
getDependentCameras(camera);
|
||||
|
||||
const params: BrowseMediaQueryParameters[] = [];
|
||||
for (const cameraID of cameraIDs) {
|
||||
const param = BrowseMediaUtil.getBrowseMediaQueryParameters(
|
||||
hass,
|
||||
cameraID,
|
||||
cameras.get(cameraID),
|
||||
mediaType ? { mediaType: mediaType } : {},
|
||||
);
|
||||
// Fail on a single bad camera, as it's almost certainly a user error that
|
||||
// should be fixed rather than hidden.
|
||||
if (!param) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...browseMediaQueryParametersBase,
|
||||
mediaType: view.isClipRelatedView() ? 'clips' : 'snapshots',
|
||||
};
|
||||
params.push(param);
|
||||
}
|
||||
return params.length ? params : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the parameters to search for media related to the current view.
|
||||
* @returns A BrowseMediaQueryParameters object.
|
||||
* Get BrowseMediaQueryParameters for a camera (including its dependencies) or dispatch an error.
|
||||
* @param element The element from which to dispatch the error.
|
||||
* @param hass Home Assistant object.
|
||||
* @param cameras Cameras map.
|
||||
* @param camera Name of the current camera.
|
||||
* @param mediaType Optional media type to include in the parameters.
|
||||
* @returns An array of query parameters.
|
||||
*/
|
||||
static getBrowseMediaQueryParametersBaseOrDispatchError(
|
||||
node: HTMLElement,
|
||||
cameraConfig: CameraConfig,
|
||||
): BrowseMediaQueryParametersBase | null {
|
||||
// Verify there is a camera name, otherwise getBrowseMediaQueryParametersBase()
|
||||
// will return undefined.
|
||||
if (!cameraConfig.camera_name) {
|
||||
static getFullDependentBrowseMediaQueryParametersOrDispatchError(
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
cameras: Map<string, CameraConfig>,
|
||||
camera: string,
|
||||
mediaType?: 'clips' | 'snapshots',
|
||||
): BrowseMediaQueryParameters[] | null {
|
||||
const params = this.getFullDependentBrowseMediaQueryParameters(
|
||||
hass,
|
||||
cameras,
|
||||
camera,
|
||||
mediaType,
|
||||
);
|
||||
if (!params) {
|
||||
dispatchErrorMessageEvent(
|
||||
node,
|
||||
localize('error.no_camera_name') + `: ${JSON.stringify(cameraConfig)}`,
|
||||
element,
|
||||
localize('error.no_camera_name'),
|
||||
cameras.get(camera),
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
return BrowseMediaUtil.getBrowseMediaQueryParametersBase(cameraConfig);
|
||||
return params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the latest media and dispatch a change view event to reflect the
|
||||
* results. If no media is found a suitable message event will be triggered
|
||||
* instead.
|
||||
* @param node The HTMLElement to dispatch events from.
|
||||
* @param element The HTMLElement to dispatch events from.
|
||||
* @param hass The Home Assistant object.
|
||||
* @param view The current view to evolve.
|
||||
* @param browseMediaQueryParameters The media parameters to query with.
|
||||
* @returns
|
||||
*/
|
||||
static async fetchLatestMediaAndDispatchViewChange(
|
||||
node: HTMLElement,
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
view: Readonly<View>,
|
||||
browseMediaQueryParameters: BrowseMediaQueryParameters,
|
||||
browseMediaQueryParameters:
|
||||
| BrowseMediaQueryParameters
|
||||
| BrowseMediaQueryParameters[],
|
||||
): Promise<void> {
|
||||
let parent: FrigateBrowseMediaSource | null;
|
||||
try {
|
||||
parent = await BrowseMediaUtil.browseMediaQuery(hass, browseMediaQueryParameters);
|
||||
parent = await BrowseMediaUtil.multipleBrowseMediaQueryMerged(
|
||||
hass,
|
||||
browseMediaQueryParameters,
|
||||
);
|
||||
} catch (e) {
|
||||
return dispatchErrorMessageEvent(node, (e as Error).message);
|
||||
return dispatchErrorMessageEvent(element, (e as Error).message);
|
||||
}
|
||||
const childIndex = BrowseMediaUtil.getFirstTrueMediaChildIndex(parent);
|
||||
if (!parent || !parent.children || childIndex == null) {
|
||||
return dispatchMessageEvent(
|
||||
node,
|
||||
browseMediaQueryParameters.mediaType == 'clips'
|
||||
element,
|
||||
view.isClipRelatedView()
|
||||
? localize('common.no_clip')
|
||||
: localize('common.no_snapshot'),
|
||||
browseMediaQueryParameters.mediaType == 'clips'
|
||||
? 'mdi:filmstrip-off'
|
||||
: 'mdi:camera-off',
|
||||
view.isClipRelatedView() ? 'mdi:filmstrip-off' : 'mdi:camera-off',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -210,7 +356,7 @@ export class BrowseMediaUtil {
|
||||
target: parent,
|
||||
childIndex: childIndex,
|
||||
})
|
||||
.dispatchChangeEvent(node);
|
||||
.dispatchChangeEvent(element);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -223,7 +369,7 @@ export class BrowseMediaUtil {
|
||||
* @returns
|
||||
*/
|
||||
static async fetchChildMediaAndDispatchViewChange(
|
||||
node: HTMLElement,
|
||||
element: HTMLElement,
|
||||
hass: HomeAssistant,
|
||||
view: Readonly<View>,
|
||||
child: Readonly<FrigateBrowseMediaSource>,
|
||||
@@ -232,13 +378,36 @@ export class BrowseMediaUtil {
|
||||
try {
|
||||
parent = await BrowseMediaUtil.browseMedia(hass, child.media_content_id);
|
||||
} catch (e) {
|
||||
return dispatchErrorMessageEvent(node, (e as Error).message);
|
||||
return dispatchErrorMessageEvent(element, (e as Error).message);
|
||||
}
|
||||
|
||||
view
|
||||
.evolve({
|
||||
target: parent,
|
||||
})
|
||||
.dispatchChangeEvent(node);
|
||||
.dispatchChangeEvent(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of media children, create a parent for them.
|
||||
* @param title The title to use for the parent.
|
||||
* @param children The children media items.
|
||||
* @returns A single parent containing the children.
|
||||
*/
|
||||
static createEventParentForChildren(
|
||||
title: string,
|
||||
children: FrigateBrowseMediaSource[],
|
||||
): FrigateBrowseMediaSource {
|
||||
return {
|
||||
title: title,
|
||||
media_class: MEDIA_CLASS_PLAYLIST,
|
||||
media_content_type: MEDIA_TYPE_PLAYLIST,
|
||||
media_content_id: '',
|
||||
can_play: false,
|
||||
can_expand: true,
|
||||
children_media_class: MEDIA_CLASS_PLAYLIST,
|
||||
thumbnail: null,
|
||||
children: children,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
+9
-7
@@ -8,7 +8,7 @@ import {
|
||||
} from 'lit';
|
||||
import { HomeAssistant, LovelaceCardEditor, getLovelace } from 'custom-card-helpers';
|
||||
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||
import screenfull from 'screenfull';
|
||||
@@ -328,11 +328,12 @@ export class FrigateCard extends LitElement {
|
||||
const cameraConfig = this._getSelectedCameraConfig();
|
||||
|
||||
// Don't show `clips` button if there's no `camera_name` (e.g. non-Frigate
|
||||
// cameras), or is birdseye.
|
||||
// cameras), or is birdseye (unless there are dependent cameras).
|
||||
if (
|
||||
this._getConfig().menu.buttons.clips &&
|
||||
cameraConfig?.camera_name &&
|
||||
cameraConfig?.camera_name !== 'birdseye'
|
||||
(cameraConfig?.camera_name !== 'birdseye' ||
|
||||
cameraConfig?.dependent_cameras?.length)
|
||||
) {
|
||||
buttons.push({
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
@@ -345,11 +346,12 @@ export class FrigateCard extends LitElement {
|
||||
}
|
||||
|
||||
// Don't show `snapshots` button if there's no `camera_name` (e.g. non-Frigate
|
||||
// cameras), or is birdseye.
|
||||
// cameras), or is birdseye (unless there are dependent cameras).
|
||||
if (
|
||||
this._getConfig().menu.buttons.snapshots &&
|
||||
cameraConfig?.camera_name &&
|
||||
cameraConfig?.camera_name !== 'birdseye'
|
||||
(cameraConfig?.camera_name !== 'birdseye' ||
|
||||
cameraConfig?.dependent_cameras?.length)
|
||||
) {
|
||||
buttons.push({
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
@@ -1299,7 +1301,7 @@ export class FrigateCard extends LitElement {
|
||||
? html` <frigate-card-gallery
|
||||
.hass=${this._hass}
|
||||
.view=${this._view}
|
||||
.cameraConfig=${cameraConfig}
|
||||
.cameras=${this._cameras}
|
||||
.galleryConfig=${this._getConfig().event_gallery}
|
||||
>
|
||||
</frigate-card-gallery>`
|
||||
@@ -1308,7 +1310,7 @@ export class FrigateCard extends LitElement {
|
||||
? html` <frigate-card-viewer
|
||||
.hass=${this._hass}
|
||||
.view=${this._view}
|
||||
.cameraConfig=${cameraConfig}
|
||||
.cameras=${this._cameras}
|
||||
.viewerConfig=${this._getConfig().event_viewer}
|
||||
.resolvedMediaCache=${this._resolvedMediaCache}
|
||||
>
|
||||
|
||||
@@ -477,6 +477,7 @@ export function getCameraTitle(
|
||||
config?.title ||
|
||||
(config?.camera_entity ? getEntityTitle(hass, config.camera_entity) : '') ||
|
||||
(config?.camera_name ? prettifyFrigateName(config.camera_name) : '') ||
|
||||
config?.id ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
+18
-13
@@ -11,11 +11,7 @@ import {
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
|
||||
import {
|
||||
CameraConfig,
|
||||
GalleryConfig,
|
||||
frigateCardConfigDefaults,
|
||||
} from '../types.js';
|
||||
import { CameraConfig, GalleryConfig, frigateCardConfigDefaults } from '../types.js';
|
||||
import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||
import { View } from '../view.js';
|
||||
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
||||
@@ -35,28 +31,37 @@ export class FrigateCardGallery extends LitElement {
|
||||
protected view?: Readonly<View>;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected cameraConfig?: CameraConfig;
|
||||
protected galleryConfig?: GalleryConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected galleryConfig?: GalleryConfig;
|
||||
protected cameras?: Map<string, CameraConfig>;
|
||||
|
||||
/**
|
||||
* Master render method.
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass || !this.view || !this.cameraConfig || !this.view.isGalleryView()) {
|
||||
const mediaType = this.view?.getMediaType();
|
||||
if (
|
||||
!this.hass ||
|
||||
!this.view ||
|
||||
!this.cameras ||
|
||||
!this.view.isGalleryView() ||
|
||||
!mediaType
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.view.target) {
|
||||
const browseMediaQueryParameters = BrowseMediaUtil.setMediaTypeFromView(
|
||||
BrowseMediaUtil.getBrowseMediaQueryParametersBaseOrDispatchError(
|
||||
const browseMediaQueryParameters =
|
||||
BrowseMediaUtil.getFullDependentBrowseMediaQueryParametersOrDispatchError(
|
||||
this,
|
||||
this.cameraConfig,
|
||||
),
|
||||
this.view,
|
||||
this.hass,
|
||||
this.cameras,
|
||||
this.view.camera,
|
||||
mediaType,
|
||||
);
|
||||
|
||||
if (!browseMediaQueryParameters) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
LiveProvider,
|
||||
TransitionEffect,
|
||||
frigateCardConfigDefaults,
|
||||
BrowseMediaQueryParameters,
|
||||
} from '../types.js';
|
||||
import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
@@ -127,16 +126,18 @@ export class FrigateCardLive extends LitElement {
|
||||
this.conditionState,
|
||||
) as LiveConfig;
|
||||
|
||||
const browseMediaParamsBase = BrowseMediaUtil.getBrowseMediaQueryParametersBase(
|
||||
this.cameras.get(this.view.camera),
|
||||
const browseMediaParams =
|
||||
BrowseMediaUtil.getFullDependentBrowseMediaQueryParametersOrDispatchError(
|
||||
this,
|
||||
this.hass,
|
||||
this.cameras,
|
||||
this.view.camera,
|
||||
config.controls.thumbnails.media,
|
||||
);
|
||||
if (!browseMediaParamsBase) {
|
||||
|
||||
if (!browseMediaParams) {
|
||||
return;
|
||||
}
|
||||
const browseMediaParams: BrowseMediaQueryParameters = {
|
||||
...browseMediaParamsBase,
|
||||
mediaType: config.controls.thumbnails.media,
|
||||
}
|
||||
|
||||
// Note use of liveConfig and not config below -- the carousel will
|
||||
// independently override the liveconfig to reflect the camera in the
|
||||
|
||||
@@ -34,7 +34,9 @@ export class FrigateCardSurround extends LitElement {
|
||||
protected targetView?: FrigateCardView;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected browseMediaParams?: BrowseMediaQueryParameters;
|
||||
protected browseMediaParams?:
|
||||
| BrowseMediaQueryParameters
|
||||
| BrowseMediaQueryParameters[];
|
||||
|
||||
// A task to await the load of the WebRTC component.
|
||||
protected _browseTask = new Task(this, this._fetchMedia.bind(this), () => [
|
||||
@@ -53,22 +55,25 @@ export class FrigateCardSurround extends LitElement {
|
||||
| HomeAssistant
|
||||
| Readonly<View>
|
||||
| BrowseMediaQueryParameters
|
||||
| BrowseMediaQueryParameters[]
|
||||
| undefined
|
||||
)[]): Promise<void> {
|
||||
hass = hass as HomeAssistant;
|
||||
view = view as Readonly<View>;
|
||||
browseMediaParams = browseMediaParams as BrowseMediaQueryParameters;
|
||||
browseMediaParams = browseMediaParams as
|
||||
| BrowseMediaQueryParameters
|
||||
| BrowseMediaQueryParameters[];
|
||||
|
||||
if (!hass || !view || view.target || !browseMediaParams) {
|
||||
return;
|
||||
}
|
||||
let parent: FrigateBrowseMediaSource | null;
|
||||
try {
|
||||
parent = await BrowseMediaUtil.browseMediaQuery(hass, browseMediaParams);
|
||||
parent = await BrowseMediaUtil.multipleBrowseMediaQueryMerged(hass, browseMediaParams);
|
||||
} catch (e) {
|
||||
return dispatchErrorMessageEvent(this, (e as Error).message);
|
||||
}
|
||||
if (BrowseMediaUtil.getFirstTrueMediaChildIndex(parent) != null) {
|
||||
if (BrowseMediaUtil.getFirstTrueMediaChildIndex(parent) !== null) {
|
||||
this.view
|
||||
?.evolve({
|
||||
...(this.targetView && { view: this.targetView }),
|
||||
|
||||
+35
-46
@@ -25,10 +25,9 @@ import { isEqual } from 'lodash-es';
|
||||
|
||||
import { BrowseMediaUtil } from '../browse-media-util';
|
||||
import {
|
||||
BrowseMediaQueryParameters,
|
||||
CameraConfig,
|
||||
FrigateBrowseMediaSource,
|
||||
MEDIA_CLASS_PLAYLIST,
|
||||
MEDIA_TYPE_PLAYLIST,
|
||||
TimelineConfig,
|
||||
FrigateEvent,
|
||||
frigateCardConfigDefaults,
|
||||
@@ -278,49 +277,47 @@ class TimelineEventManager {
|
||||
}
|
||||
this._dateFetch = new Date();
|
||||
|
||||
const fetchCameraEvents = async (
|
||||
camera: string,
|
||||
fetchMedia: 'clips' | 'snapshots',
|
||||
): Promise<void> => {
|
||||
const cameraConfig = cameras.get(camera);
|
||||
if (!cameraConfig || !this._dateStart || !this._dateEnd) {
|
||||
return;
|
||||
}
|
||||
const browseMediaQueryParametersBase =
|
||||
BrowseMediaUtil.getBrowseMediaQueryParametersBase(cameraConfig);
|
||||
if (!browseMediaQueryParametersBase) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
this._addMediaSource(
|
||||
camera,
|
||||
media,
|
||||
await BrowseMediaUtil.browseMediaQuery(hass, {
|
||||
...browseMediaQueryParametersBase,
|
||||
|
||||
const params: BrowseMediaQueryParameters[] = [];
|
||||
cameras.forEach((cameraConfig, cameraID) => {
|
||||
(media === 'all' ? ['clips', 'snapshots'] : [media]).forEach((mediaType) => {
|
||||
if (this._dateEnd && this._dateStart) {
|
||||
const param = BrowseMediaUtil.getBrowseMediaQueryParameters(
|
||||
hass,
|
||||
cameraID,
|
||||
cameraConfig,
|
||||
{
|
||||
// Events are always fetched for the maximum extent of the managed
|
||||
// range. This is because events may change at any point in time
|
||||
// (e.g. a long-running event that ends).
|
||||
before: this._dateEnd.getTime() / 1000,
|
||||
after: this._dateStart.getTime() / 1000,
|
||||
unlimited: true,
|
||||
mediaType: fetchMedia,
|
||||
}),
|
||||
mediaType: mediaType as 'clips' | 'snapshots',
|
||||
},
|
||||
);
|
||||
if (param) {
|
||||
params.push(param);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
if (!params.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
let results: Map<BrowseMediaQueryParameters, FrigateBrowseMediaSource>;
|
||||
try {
|
||||
results = await BrowseMediaUtil.multipleBrowseMediaQuery(hass, params);
|
||||
} catch (e) {
|
||||
return dispatchErrorMessageEvent(element, (e as Error).message);
|
||||
}
|
||||
};
|
||||
|
||||
const promises: Promise<void>[] = [];
|
||||
(media === 'all' ? ['clips', 'snapshots'] : [media]).forEach((mediaType) =>
|
||||
promises.push(
|
||||
...Array.from(cameras.keys()).map((camera) =>
|
||||
fetchCameraEvents(camera, mediaType as 'clips' | 'snapshots'),
|
||||
),
|
||||
),
|
||||
);
|
||||
await Promise.all(promises);
|
||||
for (const [query, result] of results.entries()) {
|
||||
if (query.cameraID) {
|
||||
this._addMediaSource(query.cameraID, media, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -574,18 +571,10 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
return null;
|
||||
}
|
||||
|
||||
const target = {
|
||||
title: `Timeline events`,
|
||||
media_class: MEDIA_CLASS_PLAYLIST,
|
||||
media_content_type: MEDIA_TYPE_PLAYLIST,
|
||||
media_content_id: '',
|
||||
can_play: false,
|
||||
can_expand: true,
|
||||
children_media_class: MEDIA_CLASS_PLAYLIST,
|
||||
thumbnail: null,
|
||||
children: children,
|
||||
};
|
||||
|
||||
const target = BrowseMediaUtil.createEventParentForChildren(
|
||||
'Timeline events',
|
||||
children,
|
||||
);
|
||||
return {
|
||||
target: target,
|
||||
childIndex: childIndex < 0 ? null : childIndex,
|
||||
|
||||
+19
-17
@@ -17,7 +17,7 @@ import { ref } from 'lit/directives/ref.js';
|
||||
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
||||
import type {
|
||||
BrowseMediaNeighbors,
|
||||
BrowseMediaQueryParametersBase,
|
||||
BrowseMediaQueryParameters,
|
||||
FrigateBrowseMediaSource,
|
||||
CameraConfig,
|
||||
MediaShowInfo,
|
||||
@@ -54,7 +54,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
protected viewerConfig?: ViewerConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected cameraConfig?: CameraConfig;
|
||||
protected cameras?: Map<string, CameraConfig>;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected resolvedMediaCache?: ResolvedMediaCache;
|
||||
@@ -64,21 +64,19 @@ export class FrigateCardViewer extends LitElement {
|
||||
* @returns A rendered template.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.hass || !this.view || !this.cameraConfig || !this.viewerConfig) {
|
||||
if (!this.hass || !this.view || !this.cameras || !this.viewerConfig) {
|
||||
return;
|
||||
}
|
||||
|
||||
const browseMediaQueryParametersBase =
|
||||
BrowseMediaUtil.getBrowseMediaQueryParametersBaseOrDispatchError(
|
||||
const browseMediaQueryParameters =
|
||||
BrowseMediaUtil.getFullDependentBrowseMediaQueryParametersOrDispatchError(
|
||||
this,
|
||||
this.cameraConfig,
|
||||
this.hass,
|
||||
this.cameras,
|
||||
this.view.camera,
|
||||
);
|
||||
|
||||
if (!this.view.target) {
|
||||
const browseMediaQueryParameters = BrowseMediaUtil.setMediaTypeFromView(
|
||||
browseMediaQueryParametersBase,
|
||||
this.view,
|
||||
);
|
||||
if (!browseMediaQueryParameters) {
|
||||
return;
|
||||
}
|
||||
@@ -101,7 +99,7 @@ export class FrigateCardViewer extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.view=${this.view}
|
||||
.viewerConfig=${this.viewerConfig}
|
||||
.browseMediaQueryParametersBase=${browseMediaQueryParametersBase}
|
||||
.browseMediaQueryParameters=${browseMediaQueryParameters}
|
||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||
>
|
||||
</frigate-card-viewer-carousel>
|
||||
@@ -133,7 +131,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
protected viewerConfig?: ViewerConfig;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected browseMediaQueryParametersBase?: BrowseMediaQueryParametersBase;
|
||||
protected browseMediaQueryParameters?: BrowseMediaQueryParameters[] | null;
|
||||
|
||||
@property({ attribute: false })
|
||||
protected resolvedMediaCache?: ResolvedMediaCache;
|
||||
@@ -338,7 +336,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
!this.view.target ||
|
||||
!this.view.target.children ||
|
||||
!this.view.target.children.length ||
|
||||
!this.browseMediaQueryParametersBase
|
||||
!this.browseMediaQueryParameters
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
@@ -379,13 +377,17 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
||||
|
||||
let clips: FrigateBrowseMediaSource | null;
|
||||
|
||||
try {
|
||||
clips = await BrowseMediaUtil.browseMediaQuery(this.hass, {
|
||||
...this.browseMediaQueryParametersBase,
|
||||
const params = BrowseMediaUtil.overrideMultiBrowseMediaQueryParameters(
|
||||
this.browseMediaQueryParameters,
|
||||
{
|
||||
mediaType: 'clips',
|
||||
before: latest,
|
||||
after: earliest,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
clips = await BrowseMediaUtil.multipleBrowseMediaQueryMerged(this.hass, params);
|
||||
} catch (e) {
|
||||
// This is best effort.
|
||||
return null;
|
||||
|
||||
@@ -276,7 +276,7 @@
|
||||
"invalid_elements_config": "Invalid picture elements configuration",
|
||||
"jsmpeg_no_sign": "Could not retrieve or sign JSMPEG websocket path",
|
||||
"jsmpeg_no_player": "Could not start JSMPEG player",
|
||||
"no_camera_name": "Could not determine Frigate camera name for camera, please specify either 'camera_entity' or 'camera_name' for the following camera",
|
||||
"no_camera_name": "Could not determine a Frigate camera name for camera (or one of its dependents), please specify either 'camera_entity' or 'camera_name'",
|
||||
"download_no_media": "No media to download",
|
||||
"download_no_event_id": "Could not extract Frigate event id from media",
|
||||
"download_sign_failed": "Could not sign media URL for download"
|
||||
|
||||
+18
-5
@@ -327,6 +327,9 @@ const cameraConfigSchema = z
|
||||
|
||||
// Camera identifiers for WebRTC.
|
||||
webrtc_card: webrtcCardCameraConfigSchema.optional(),
|
||||
|
||||
// Set of cameras IDs upon which this camera depends.
|
||||
dependent_cameras: z.string().array().optional(),
|
||||
})
|
||||
.default(cameraConfigDefault);
|
||||
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
||||
@@ -936,11 +939,14 @@ export type MenuButton = z.infer<typeof menuButtonSchema>;
|
||||
export interface ExtendedHomeAssistant extends HomeAssistant {
|
||||
hassUrl(path?): string;
|
||||
themes: Themes & {
|
||||
darkMode?: boolean
|
||||
darkMode?: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export interface BrowseMediaQueryParametersBase {
|
||||
export interface BrowseMediaQueryParameters {
|
||||
// ========================================
|
||||
// Parameters used to construct media query
|
||||
// ========================================
|
||||
mediaType?: 'clips' | 'snapshots';
|
||||
clientId: string;
|
||||
cameraName: string;
|
||||
@@ -949,10 +955,17 @@ export interface BrowseMediaQueryParametersBase {
|
||||
before?: number;
|
||||
after?: number;
|
||||
unlimited?: boolean;
|
||||
}
|
||||
|
||||
export interface BrowseMediaQueryParameters extends BrowseMediaQueryParametersBase {
|
||||
mediaType: 'clips' | 'snapshots';
|
||||
// ========================================
|
||||
// Parameters used to differentiate results
|
||||
// ========================================
|
||||
// Optional title to be used for separating results when merging multiple
|
||||
// sets of results. See `mergeFrigateBrowseMediaSources()` .
|
||||
title?: string;
|
||||
|
||||
// Optional camera-id to which this query is associated. May be used to map
|
||||
// results to a particular camera within the card.
|
||||
cameraID?: string;
|
||||
}
|
||||
|
||||
export interface BrowseMediaNeighbors {
|
||||
|
||||
+12
@@ -110,6 +110,18 @@ export class View {
|
||||
return ['snapshot', 'snapshots'].includes(this.view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the media type for this view if available.
|
||||
* @returns Whether the media is `clips` or `snapshots` or unknown (`null`)
|
||||
*/
|
||||
public getMediaType(): 'clips' | 'snapshots' | null {
|
||||
return this.isClipRelatedView()
|
||||
? 'clips'
|
||||
: this.isSnapshotRelatedView()
|
||||
? 'snapshots'
|
||||
: null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the media item that should be played.
|
||||
**/
|
||||
|
||||
Reference in New Issue
Block a user