{
+ if (!this._hass || !this._cameraManager) {
+ return;
+ }
+
+ try {
+ await this._cameraManager.initializeCameras(
+ this._hass,
+ this._entityRegistryManager,
+ this._getConfig().cameras,
+ );
+ } catch (e: unknown) {
+ if (e instanceof Error) {
+ errorToConsole(e);
+ }
+ if (e instanceof FrigateCardError) {
+ this._setMessageAndUpdate({
+ message: e.message,
+ type: 'error',
+ context: e.context,
+ });
+ }
+ }
+
+ // Don't reset the message which may be set to an error above. This sets the
+ // first view using the newly loaded cameras.
+ this._changeView({ resetMessage: false });
+ }
+
/**
* Called before each update.
*/
protected willUpdate(changedProps: PropertyValues): void {
if (
- this._cameras &&
this._cardWideConfig &&
- (changedProps.has('_config') ||
- changedProps.has('_cameras') ||
+ (!this._cameraManager ||
+ changedProps.has('_config') ||
changedProps.has('_cardWideConfig'))
) {
this._cameraManager = new CameraManager(
- new CameraManagerEngineFactory(this._cardWideConfig),
- this._cameras,
+ new CameraManagerEngineFactory(
+ this._entityRegistryManager,
+ this._cardWideConfig,
+ ),
this._cardWideConfig,
);
+ this._initializeCameras().then(() => this.requestUpdate());
}
if (changedProps.has('_cardWideConfig')) {
@@ -1162,7 +955,8 @@ class FrigateCard extends LitElement {
let changedCamera = false;
let triggerChanges = false;
- for (const [camera, config] of this._cameras?.entries() ?? []) {
+ const cameras = this._cameraManager?.getCameras();
+ for (const [cameraID, config] of cameras?.entries() ?? []) {
const triggerEntities = config.triggers.entities ?? [];
const diffs = getHassDifferences(this._hass, oldHass, triggerEntities, {
stateOnly: true,
@@ -1172,10 +966,10 @@ class FrigateCard extends LitElement {
(entity) => !isTriggeredState(this._hass?.states[entity]),
);
if (shouldTrigger) {
- this._triggers.set(camera, now);
+ this._triggers.set(cameraID, now);
triggerChanges = true;
- } else if (shouldUntrigger && this._triggers.has(camera)) {
- this._triggers.delete(camera);
+ } else if (shouldUntrigger && this._triggers.has(cameraID)) {
+ this._triggers.delete(cameraID);
triggerChanges = true;
}
}
@@ -1419,7 +1213,7 @@ class FrigateCard extends LitElement {
const cameraEntity = cameraConfig.camera_entity ?? null;
const media = this._view.queryResults?.getSelectedResult();
- if (this._view.isViewerView() && media && this._cameras) {
+ if (this._view.isViewerView() && media) {
media_content_id = media.getContentID();
media_content_type = media.getContentType();
title = media.getTitle();
@@ -1491,9 +1285,9 @@ class FrigateCard extends LitElement {
this._downloadViewerMedia();
break;
case 'frigate_ui':
- const frigate_url = this._getFrigateURLFromContext();
- if (frigate_url) {
- window.open(frigate_url);
+ const url = this._getCameraURLFromContext();
+ if (url) {
+ window.open(url);
}
break;
case 'fullscreen':
@@ -1508,22 +1302,15 @@ class FrigateCard extends LitElement {
this._refMenu.value?.toggleMenu();
break;
case 'camera_select':
- const camera = frigateCardAction.camera;
- if (this._cameras?.has(camera) && this._view) {
- const targetView = View.selectBestViewForUserSpecified(
- this._getConfig().view.camera_select === 'current'
- ? this._view.view
- : (this._getConfig().view.camera_select as FrigateCardView),
- );
- this._changeView({
- view: new View({
- view: this._cameras?.get(camera)?.frigate.camera_name
- ? targetView
- : // Fallback to supported views for non-Frigate cameras.
- View.selectBestViewForNonFrigateCameras(targetView),
- camera: camera,
- }),
- });
+ const cameraID = frigateCardAction.camera;
+ if (this._cameraManager?.hasCameraID(cameraID) && this._view) {
+ const viewOnCameraSelect = this._getConfig().view.camera_select;
+ const targetView =
+ viewOnCameraSelect === 'current' ? this._view.view : viewOnCameraSelect;
+ const actualView = this.isViewSupportedByCamera(cameraID, targetView)
+ ? targetView
+ : FRIGATE_CARD_VIEW_DEFAULT;
+ this._changeView({ view: new View({ view: actualView, camera: cameraID }) });
}
break;
case 'media_player':
@@ -1540,6 +1327,33 @@ class FrigateCard extends LitElement {
}
}
+ public isViewSupportedByCamera(cameraID: string, view: FrigateCardView): boolean {
+ const capabilities = this._cameraManager?.getCameraCapabilities(cameraID);
+ switch (view) {
+ case 'live':
+ case 'image':
+ return true;
+ case 'clip':
+ case 'clips':
+ return !!capabilities?.supportsClips;
+ case 'snapshot':
+ case 'snapshots':
+ return !!capabilities?.supportsSnapshots;
+ case 'recording':
+ case 'recordings':
+ return !!capabilities?.supportsRecordings;
+ case 'timeline':
+ return !!capabilities?.supportsTimeline;
+ case 'media':
+ return (
+ !!capabilities?.supportsClips ||
+ !!capabilities?.supportsSnapshots ||
+ !!capabilities?.supportsRecordings
+ );
+ }
+ return false;
+ }
+
/**
* Generate diagnostics for issue reports.
*/
@@ -1593,18 +1407,16 @@ class FrigateCard extends LitElement {
* Get the Frigate UI URL from context.
* @returns The URL or null if unavailable.
*/
- protected _getFrigateURLFromContext(): string | null {
- const cameraConfig = this._getSelectedCameraConfig();
- if (!cameraConfig || !cameraConfig.frigate.url || !this._view) {
- return null;
- }
- if (!cameraConfig.frigate.camera_name) {
- return cameraConfig.frigate.url;
- }
- if (this._view.isViewerView() || this._view.isGalleryView()) {
- return `${cameraConfig.frigate.url}/events?camera=${cameraConfig.frigate.camera_name}`;
- }
- return `${cameraConfig.frigate.url}/cameras/${cameraConfig.frigate.camera_name}`;
+ protected _getCameraURLFromContext(): string | null {
+ const view = this._view;
+ const selectedCameraID = view?.camera;
+ const media = view?.queryResults?.getSelectedResult() ?? null;
+ return this._hass && view && selectedCameraID
+ ? this._cameraManager?.getCameraURL(selectedCameraID, {
+ ...(media && { media: media }),
+ ...(view && { view: view.view }),
+ }) ?? null
+ : null;
}
/**
@@ -1961,23 +1773,14 @@ class FrigateCard extends LitElement {
>
${renderMenuAbove ? this._renderMenu() : ''}
- ${this._cameras === undefined && !this._message
- ? until(
- (async () => {
- await this._loadCameras();
- // Don't reset messages as errors may have been generated
- // during the camera load.
- this._changeView({ resetMessage: false });
- return this._render();
- })(),
- renderProgressIndicator({ cardWideConfig: this._cardWideConfig }),
- )
+ ${!this._cameraManager?.isInitialized() && !this._message
+ ? renderProgressIndicator({ cardWideConfig: this._cardWideConfig })
: // Always want to call render even if there's a message, to
// ensure live preload is always present (even if not displayed).
this._render()}
${
- // Keep message rendering to last to show messages that may have
- // been generated during the render.
+ // Keep message rendering to last to show messages that may have been
+ // generated during the render.
this._message ? renderMessage(this._message) : ''
}
@@ -2011,7 +1814,7 @@ class FrigateCard extends LitElement {
protected _render(): TemplateResult | void {
const cameraConfig = this._getSelectedCameraConfig();
- if (!this._hass || !this._view || !cameraConfig || !this._cameras) {
+ if (!this._hass || !this._view || !cameraConfig) {
return html``;
}
@@ -2037,7 +1840,6 @@ class FrigateCard extends LitElement {
? html`
@@ -2082,7 +1882,6 @@ class FrigateCard extends LitElement {
.liveConfig=${this._config.live}
.conditionState=${this._conditionState}
.liveOverrides=${getOverridesByKey(this._getConfig().overrides, 'live')}
- .cameras=${this._cameras}
.cameraManager=${this._cameraManager}
.cardWideConfig=${this._cardWideConfig}
class="${classMap(liveClasses)}"
diff --git a/src/components/gallery.ts b/src/components/gallery.ts
index 27158c30..591ce222 100644
--- a/src/components/gallery.ts
+++ b/src/components/gallery.ts
@@ -10,7 +10,6 @@ import {
import { customElement, property, state } from 'lit/decorators.js';
import galleryStyle from '../scss/gallery.scss';
import {
- CameraConfig,
CardWideConfig,
ExtendedHomeAssistant,
frigateCardConfigDefaults,
@@ -24,7 +23,7 @@ import {
} from '../utils/media-to-view.js';
import { CameraManager, ExtendedMediaQueryResult } from '../camera-manager/manager.js';
import { View } from '../view/view.js';
-import { renderProgressIndicator } from './message.js';
+import { dispatchMessageEvent, renderProgressIndicator } from './message.js';
import './thumbnail.js';
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
import { createRef, Ref } from 'lit/directives/ref.js';
@@ -36,6 +35,7 @@ import { errorToConsole } from '../utils/basic';
import './media-filter';
import "./surround-basic";
import { ViewMedia } from '../view/media';
+import { localize } from '../localize/localize';
const GALLERY_MEDIA_CHUNK_SIZE = 100;
@@ -55,9 +55,6 @@ export class FrigateCardGallery extends LitElement {
@property({ attribute: false })
public galleryConfig?: GalleryConfig;
- @property({ attribute: false })
- public cameras?: Map;
-
@property({ attribute: false })
public cameraManager?: CameraManager;
@@ -72,7 +69,6 @@ export class FrigateCardGallery extends LitElement {
if (
!this.hass ||
!this.view ||
- !this.cameras ||
!this.view.isGalleryView() ||
!this.cameraManager
) {
@@ -85,7 +81,6 @@ export class FrigateCardGallery extends LitElement {
this,
this.hass,
this.cameraManager,
- this.cameras,
this.view,
);
} else {
@@ -98,7 +93,6 @@ export class FrigateCardGallery extends LitElement {
this,
this.hass,
this.cameraManager,
- this.cameras,
this.view,
{
...(mediaType && { mediaType: mediaType }),
@@ -120,7 +114,6 @@ export class FrigateCardGallery extends LitElement {
${this.galleryConfig && this.galleryConfig.controls.filter.mode !== 'none'
? html`
@@ -166,9 +158,6 @@ export class FrigateCardGalleryCore extends LitElement {
@property({ attribute: false })
public galleryConfig?: GalleryConfig;
- @property({ attribute: false })
- public cameras?: Map;
-
@property({ attribute: false })
public cameraManager?: CameraManager;
@@ -330,12 +319,17 @@ export class FrigateCardGalleryCore extends LitElement {
!this._media ||
!this.hass ||
!this.view ||
- !this.view.isGalleryView() ||
- !this.cameras
+ !this.view.isGalleryView()
) {
return html``;
}
+ if ((this.view?.queryResults?.getResultsCount() ?? 0) === 0) {
+ return dispatchMessageEvent(this, localize('common.no_media'), 'info', {
+ icon: 'mdi:multimedia',
+ });
+ }
+
return html`
${this._media.map(
(media, index) =>
diff --git a/src/components/live/live.ts b/src/components/live/live.ts
index 2b5da9a5..769fe5cd 100644
--- a/src/components/live/live.ts
+++ b/src/components/live/live.ts
@@ -104,9 +104,6 @@ export class FrigateCardLive extends LitElement {
@property({ attribute: false })
public view?: Readonly;
- @property({ attribute: false })
- public cameras?: Map;
-
@property({ attribute: false })
public liveConfig?: LiveConfig;
@@ -200,7 +197,7 @@ export class FrigateCardLive extends LitElement {
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
- if (!this.hass || !this.liveConfig || !this.cameras || !this.view) {
+ if (!this.hass || !this.liveConfig || !this.cameraManager || !this.view) {
return;
}
@@ -229,7 +226,6 @@ export class FrigateCardLive extends LitElement {
.fetchMedia=${config.controls.thumbnails.media}
.thumbnailConfig=${config.controls.thumbnails}
.timelineConfig=${config.controls.timeline}
- .cameras=${this.cameras}
.cameraManager=${this.cameraManager}
.inBackground=${this._inBackground}
@frigate-card:message=${(ev: CustomEvent) => {
@@ -254,7 +250,6 @@ export class FrigateCardLive extends LitElement {
;
- @property({ attribute: false })
- public cameras?: Map;
-
@property({ attribute: false })
public liveConfig?: LiveConfig;
@@ -345,10 +337,11 @@ export class FrigateCardLiveCarousel extends LitElement {
}
protected _getSelectedCameraIndex(): number {
- if (!this.cameras || !this.view) {
+ const cameraIDs = this.cameraManager?.getCameraIDs();
+ if (!cameraIDs || !this.view) {
return 0;
}
- return Math.max(0, Array.from(this.cameras.keys()).indexOf(this.view.camera));
+ return Math.max(0, Array.from(cameraIDs).indexOf(this.view.camera));
}
/**
@@ -367,9 +360,10 @@ export class FrigateCardLiveCarousel extends LitElement {
* @returns A list of EmblaOptionsTypes.
*/
protected _getPlugins(): EmblaCarouselPlugins {
+ const cameras = this.cameraManager?.getCameraIDs();
return [
// Only enable wheel plugin if there is more than one camera.
- ...(this.cameras && this.cameras.size > 1
+ ...(cameras && cameras.size > 1
? [
WheelGesturesPlugin({
// Whether the carousel is vertical or horizontal, interpret y-axis wheel
@@ -424,14 +418,15 @@ export class FrigateCardLiveCarousel extends LitElement {
* name to slide number.
*/
protected _getSlides(): [TemplateResult[], Record] {
- if (!this.cameras) {
+ const cameras = this.cameraManager?.getCameras();
+ if (!cameras) {
return [[], {}];
}
const slides: TemplateResult[] = [];
const cameraToSlide: Record = {};
- for (const [camera, cameraConfig] of this.cameras) {
+ for (const [camera, cameraConfig] of cameras) {
const slide = this._renderLive(camera, cameraConfig, slides.length);
if (slide) {
cameraToSlide[camera] = slides.length;
@@ -445,8 +440,9 @@ export class FrigateCardLiveCarousel extends LitElement {
* Handle the user selecting a new slide in the carousel.
*/
protected _setViewHandler(ev: CustomEvent): void {
- if (this.cameras && ev.detail.index !== this._getSelectedCameraIndex()) {
- this._setViewCameraID(Array.from(this.cameras.keys())[ev.detail.index]);
+ const cameras = this.cameraManager?.getCameras();
+ if (cameras && ev.detail.index !== this._getSelectedCameraIndex()) {
+ this._setViewCameraID(Array.from(cameras.keys())[ev.detail.index]);
}
}
@@ -534,19 +530,20 @@ export class FrigateCardLiveCarousel extends LitElement {
}
protected _getCameraIDsOfNeighbors(): [string | null, string | null] {
- if (!this.cameras || !this.view || !this.hass) {
+ const cameras = this.cameraManager?.getCameras();
+ if (!cameras || !this.view || !this.hass) {
return [null, null];
}
- const keys = Array.from(this.cameras.keys());
+ const keys = Array.from(cameras.keys());
const currentIndex = keys.indexOf(this.view.camera);
- if (currentIndex < 0 || this.cameras.size <= 1) {
+ if (currentIndex < 0 || cameras.size <= 1) {
return [null, null];
}
return [
- keys[currentIndex > 0 ? currentIndex - 1 : this.cameras.size - 1],
- keys[currentIndex + 1 < this.cameras.size ? currentIndex + 1 : 0],
+ keys[currentIndex > 0 ? currentIndex - 1 : cameras.size - 1],
+ keys[currentIndex + 1 < cameras.size ? currentIndex + 1 : 0],
];
}
@@ -602,11 +599,11 @@ export class FrigateCardLiveCarousel extends LitElement {
;
-
@property({ attribute: false })
public cameraManager?: CameraManager;
@@ -173,7 +169,8 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_ev: CustomEvent<{ value: unknown }>,
): Promise {
- if (!this.hass || !this.cameras || !this.cameraManager || !this.view) {
+ const cameras = this.cameraManager?.getCameras();
+ if (!this.hass || !cameras || !this.cameraManager || !this.view) {
return;
}
@@ -187,7 +184,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
};
const cameraIDs =
- getArrayValueAsSet(this._refCamera.value?.value) ?? new Set(this.cameras.keys());
+ getArrayValueAsSet(this._refCamera.value?.value) ?? new Set(cameras.keys());
const mediaType = this._refMediaType.value?.value as
| MediaFilterMediaType
| undefined;
@@ -229,20 +226,13 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
];
(
- await createViewForEvents(
- this,
- this.hass,
- this.cameraManager,
- this.cameras,
- this.view,
- {
- query: new EventMediaQueries(queries),
+ await createViewForEvents(this, this.hass, this.cameraManager, this.view, {
+ query: new EventMediaQueries(queries),
- // See 'A note on views' above for these two arguments.
- ...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
- targetView: mediaType === MediaFilterMediaType.Clips ? 'clips' : 'snapshots',
- },
- )
+ // See 'A note on views' above for these two arguments.
+ ...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
+ targetView: mediaType === MediaFilterMediaType.Clips ? 'clips' : 'snapshots',
+ })
)?.dispatchChangeEvent(this);
} else if (mediaType === MediaFilterMediaType.Recordings) {
const query: RecordingQuery = {
@@ -252,32 +242,28 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
};
(
- await createViewForRecordings(
- this,
- this.hass,
- this.cameraManager,
- this.cameras,
- this.view,
- {
- query: new RecordingMediaQueries([query]),
+ await createViewForRecordings(this, this.hass, this.cameraManager, this.view, {
+ query: new RecordingMediaQueries([query]),
- // See 'A note on views' above for these two arguments.
- ...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
- targetView: 'recordings',
- },
- )
+ // See 'A note on views' above for these two arguments.
+ ...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
+ targetView: 'recordings',
+ })
)?.dispatchChangeEvent(this);
}
}
protected willUpdate(changedProps: PropertyValues): void {
- if (changedProps.has('cameras') && this.cameras) {
- this._cameraOptions = Array.from(this.cameras.keys()).map((cameraID) => ({
- value: cameraID,
- label: this.hass
- ? this.cameraManager?.getCameraMetadata(this.hass, cameraID)?.title ?? ''
- : '',
- }));
+ if (changedProps.has('cameraManager')) {
+ const cameras = this.cameraManager?.getCameras();
+ if (cameras) {
+ this._cameraOptions = Array.from(cameras.keys()).map((cameraID) => ({
+ value: cameraID,
+ label: this.hass
+ ? this.cameraManager?.getCameraMetadata(this.hass, cameraID)?.title ?? ''
+ : '',
+ }));
+ }
}
if (changedProps.has('cameraManager') && this.hass && this.cameraManager) {
@@ -321,7 +307,8 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
protected _getDefaultsFromView(): MediaFilterCoreDefaults | null {
const queries = this.view?.query?.getQueries();
- if (!this.view || !queries) {
+ const cameras = this.cameraManager?.getCameras();
+ if (!this.view || !queries || !cameras) {
return null;
}
@@ -337,7 +324,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
);
// Special note: If all cameras are selected, this is the same as no
// selector at all.
- if (cameraIDSets.length === 1 && queries[0].cameraIDs.size !== this.cameras?.size) {
+ if (cameraIDSets.length === 1 && queries[0].cameraIDs.size !== cameras.size) {
cameraIDs = [...queries[0].cameraIDs];
}
diff --git a/src/components/surround.ts b/src/components/surround.ts
index 9e85a249..12ee3b60 100644
--- a/src/components/surround.ts
+++ b/src/components/surround.ts
@@ -9,7 +9,6 @@ import {
import { customElement, property } from 'lit/decorators.js';
import surroundStyle from '../scss/surround.scss';
import {
- CameraConfig,
ClipsOrSnapshotsOrAll,
ExtendedHomeAssistant,
MiniTimelineControlConfig,
@@ -55,9 +54,6 @@ export class FrigateCardSurround extends LitElement {
@property({ attribute: false, hasChanged: contentsChanged })
public fetchMedia?: ClipsOrSnapshotsOrAll;
- @property({ attribute: false })
- public cameras?: Map;
-
@property({ attribute: false })
public cameraManager?: CameraManager;
@@ -71,7 +67,6 @@ export class FrigateCardSurround extends LitElement {
*/
protected async _fetchMedia(): Promise {
if (
- !this.cameras ||
!this.cameraManager ||
!this.fetchMedia ||
this.inBackground ||
@@ -88,7 +83,6 @@ export class FrigateCardSurround extends LitElement {
this,
this.hass,
this.cameraManager,
- this.cameras,
this.view,
{
targetView: this.view.view,
@@ -138,11 +132,12 @@ export class FrigateCardSurround extends LitElement {
}
protected _getCameraIDsForTimeline(): Set | null {
- if (!this.view || !this.cameras) {
+ const cameras = this.cameraManager?.getCameras();
+ if (!this.view || !cameras) {
return null;
}
if (this.view?.is('live')) {
- return getAllDependentCameras(this.cameras, this.view.camera);
+ return getAllDependentCameras(cameras, this.view.camera);
}
if (this.view.isViewerView()) {
return new Set(
@@ -160,7 +155,7 @@ export class FrigateCardSurround extends LitElement {
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
- if (!this.hass || !this.view || !this.thumbnailConfig || !this.cameras) {
+ if (!this.hass || !this.view || !this.thumbnailConfig) {
return;
}
@@ -220,7 +215,6 @@ export class FrigateCardSurround extends LitElement {
slot=${this.timelineConfig.mode}
.hass=${this.hass}
.view=${this.view}
- .cameras=${this.cameras}
.cameraIDs=${this._cameraIDsForTimeline}
.mini=${true}
.timelineConfig=${this.timelineConfig}
diff --git a/src/components/thumbnail-carousel.ts b/src/components/thumbnail-carousel.ts
index b0283f6b..4fb63264 100644
--- a/src/components/thumbnail-carousel.ts
+++ b/src/components/thumbnail-carousel.ts
@@ -13,7 +13,6 @@ import { classMap } from 'lit/directives/class-map.js';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
import {
- CameraConfig,
ExtendedHomeAssistant,
ThumbnailsControlConfig,
} from '../types.js';
diff --git a/src/components/timeline-core.ts b/src/components/timeline-core.ts
index ccc708b5..7e68c7a7 100644
--- a/src/components/timeline-core.ts
+++ b/src/components/timeline-core.ts
@@ -26,7 +26,6 @@ import {
TimelineOptionsCluster,
TimelineWindow,
} from 'vis-timeline/esnext';
-import { CAMERA_BIRDSEYE } from '../const';
import { localize } from '../localize/localize';
import timelineCoreStyle from '../scss/timeline-core.scss';
import {
@@ -165,9 +164,6 @@ export class FrigateCardTimelineCore extends LitElement {
@property({ attribute: false })
public view?: Readonly;
- @property({ attribute: false })
- public cameras?: Map;
-
@property({ attribute: false, hasChanged: contentsChanged })
public timelineConfig?: TimelineCoreConfig;
@@ -240,11 +236,12 @@ export class FrigateCardTimelineCore extends LitElement {
protected _handleThumbnailDataRequest(request: ThumbnailDataRequestEvent): void {
const item = request.detail.item;
const media = this._timelineSource?.dataset.get(item)?.media;
+ const cameraConfig = media
+ ? this.cameraManager?.getCameraConfig(media.getCameraID()) ?? undefined
+ : undefined;
request.detail.hass = this.hass;
- request.detail.cameraConfig = media
- ? this.cameras?.get(media.getCameraID())
- : undefined;
+ request.detail.cameraConfig = cameraConfig;
request.detail.cameraManager = this.cameraManager;
request.detail.media = media;
request.detail.view = this.view;
@@ -255,13 +252,13 @@ export class FrigateCardTimelineCore extends LitElement {
* @returns A rendered template.
*/
protected render(): TemplateResult | void {
- if (!this.hass || !this.view || !this.timelineConfig) {
+ const cameraIDs = this._getTimelineCameraIDs();
+
+ if (!this.hass || !this.view || !this.timelineConfig || !cameraIDs) {
return;
}
- const capabilities = this.cameraManager?.getAggregateCameraCapabilities(
- this._getTimelineCameraIDs(),
- );
+ const capabilities = this.cameraManager?.getAggregateCameraCapabilities(cameraIDs);
return html` ${capabilities?.supportsTimeline
? html` {
+ protected _getTimelineCameraIDs(): Set
| null {
return this.cameraIDs ?? this._getAllCameraIDs();
}
@@ -300,8 +297,8 @@ export class FrigateCardTimelineCore extends LitElement {
* Get all the keys of all cameras.
* @returns A set of camera ids (may be empty).
*/
- protected _getAllCameraIDs(): Set {
- return new Set(this.cameras?.keys());
+ protected _getAllCameraIDs(): Set | null {
+ return this.cameraManager?.getCameraIDs() ?? null;
}
/**
@@ -395,6 +392,7 @@ export class FrigateCardTimelineCore extends LitElement {
): Promise {
const results = this.view?.queryResults;
const media = results?.getResults();
+ const cameraIDs = this._getTimelineCameraIDs();
if (
!media ||
!results ||
@@ -402,7 +400,7 @@ export class FrigateCardTimelineCore extends LitElement {
!this.view ||
!this.hass ||
!this.cameraManager ||
- !this.cameraManager ||
+ !cameraIDs ||
// Skip range changes that do not have hammerjs pan directions associated
// with them, as these outliers cause media matching issues below.
!properties.event.additionalEvent
@@ -420,7 +418,7 @@ export class FrigateCardTimelineCore extends LitElement {
findClosestMediaIndex(
media,
targetTime,
- this._getTimelineCameraIDs(),
+ cameraIDs,
properties.event.additionalEvent === 'panright' ? 'end' : 'start',
),
);
@@ -462,13 +460,14 @@ export class FrigateCardTimelineCore extends LitElement {
stopEventFromActivatingCardWideActions(properties.event);
}
+ const timelineCameraIDs = this._getTimelineCameraIDs();
if (
this._ignoreClick ||
!this.hass ||
!this._timeline ||
- !this.cameras ||
!this.view ||
!this.cameraManager ||
+ !timelineCameraIDs ||
!properties.what
) {
return;
@@ -484,7 +483,6 @@ export class FrigateCardTimelineCore extends LitElement {
this,
this.hass,
this.cameraManager,
- this.cameras,
this.view,
{
targetTime:
@@ -501,10 +499,9 @@ export class FrigateCardTimelineCore extends LitElement {
this,
this.hass,
this.cameraManager,
- this.cameras,
this.view,
{
- cameraIDs: this._getAllCameraIDs(),
+ cameraIDs: timelineCameraIDs,
start: startOfHour(properties.time),
end: endOfHour(properties.time),
targetTime: properties.time,
@@ -514,9 +511,7 @@ export class FrigateCardTimelineCore extends LitElement {
const newResults = this.view.queryResults
?.clone()
.resetSelectedResult()
- .selectResultIfFound(
- (media) => !!this.cameras && media.getID() === properties.item,
- );
+ .selectResultIfFound((media) => media.getID() === properties.item);
if (!newResults || !newResults.hasSelectedResult()) {
// This can happen if this is a recording query (with recorded hours)
@@ -588,7 +583,7 @@ export class FrigateCardTimelineCore extends LitElement {
}
this._removeTargetBar();
- if (!this.hass || !this.cameras) {
+ if (!this.hass) {
return;
}
@@ -646,14 +641,13 @@ export class FrigateCardTimelineCore extends LitElement {
selectedItem?: IdType;
},
): Promise {
- if (!this.hass || !this.cameraManager || !this.cameras || !this.view || !query) {
+ if (!this.hass || !this.cameraManager || !this.view || !query) {
return null;
}
const view = await createViewForEvents(
this,
this.hass,
this.cameraManager,
- this.cameras,
this.view,
{
query: query,
@@ -666,7 +660,7 @@ export class FrigateCardTimelineCore extends LitElement {
}
if (options?.selectedItem) {
view.queryResults?.selectResultIfFound(
- (media) => !!this.cameras && media.getID() === options.selectedItem,
+ (media) => media.getID() === options.selectedItem,
);
} else {
// If not asked to select a new item, persist the currently selected item
@@ -687,10 +681,8 @@ export class FrigateCardTimelineCore extends LitElement {
*/
protected _getGroups(): DataGroupCollectionType {
const groups: FrigateCardGroupData[] = [];
-
- this._getTimelineCameraIDs().forEach((cameraID) => {
- const cameraConfig = this.cameras?.get(cameraID);
- if (!this.hass || !cameraConfig || !this.cameraManager) {
+ (this._getTimelineCameraIDs() ?? []).forEach((cameraID) => {
+ if (!this.hass || !this.cameraManager) {
return;
}
const cameraMetadata = this.cameraManager.getCameraMetadata(this.hass, cameraID);
@@ -806,10 +798,6 @@ export class FrigateCardTimelineCore extends LitElement {
maxItems: this.timelineConfig.clustering_threshold,
clusterCriteria: (first: TimelineItem, second: TimelineItem): boolean => {
- if (!this.cameras) {
- return false;
- }
-
const media = this.view?.queryResults?.getSelectedResult();
const selectedId = media?.getID();
const firstMedia = (first).media;
@@ -866,7 +854,7 @@ export class FrigateCardTimelineCore extends LitElement {
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
protected shouldUpdate(_changedProps: PropertyValues): boolean {
- return !!this.hass && !!this.cameras && this.cameras.size > 0;
+ return !!this.hass && !!this.cameraManager;
}
/**
@@ -875,7 +863,6 @@ export class FrigateCardTimelineCore extends LitElement {
protected async _updateTimelineFromView(): Promise {
if (
!this.hass ||
- !this.cameras ||
!this.view ||
!this.timelineConfig ||
!this._timelineSource ||
@@ -1040,10 +1027,11 @@ export class FrigateCardTimelineCore extends LitElement {
changedProps.has('timelineConfig') ||
changedProps.has('cameraIDs')
) {
- if (this.cameraManager && this.cameras && this.timelineConfig) {
+ const cameraIDs = this._getTimelineCameraIDs();
+ if (cameraIDs && this.cameraManager && this.timelineConfig) {
this._timelineSource = new TimelineDataSource(
this.cameraManager,
- this._getTimelineCameraIDs(),
+ cameraIDs,
this.timelineConfig.media,
);
} else {
diff --git a/src/components/timeline.ts b/src/components/timeline.ts
index b8a8446b..31aafbe1 100644
--- a/src/components/timeline.ts
+++ b/src/components/timeline.ts
@@ -1,7 +1,7 @@
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
import { customElement, property } from 'lit/decorators.js';
import timelineStyle from '../scss/timeline.scss';
-import { CameraConfig, ExtendedHomeAssistant, TimelineConfig } from '../types';
+import { ExtendedHomeAssistant, TimelineConfig } from '../types';
import { CameraManager } from '../camera-manager/manager';
import { View } from '../view/view';
import './surround.js';
@@ -20,9 +20,6 @@ export class FrigateCardTimeline extends LitElement {
@property({ attribute: false })
public view?: Readonly;
- @property({ attribute: false })
- public cameras?: Map;
-
@property({ attribute: false })
public timelineConfig?: TimelineConfig;
@@ -43,12 +40,10 @@ export class FrigateCardTimeline extends LitElement {
.view=${this.view}
.thumbnailConfig=${this.timelineConfig.controls.thumbnails}
.cameraManager=${this.cameraManager}
- .cameras=${this.cameras}
>
;
-
@property({ attribute: false })
public resolvedMediaCache?: ResolvedMediaCache;
@@ -98,7 +94,6 @@ export class FrigateCardViewer extends LitElement {
if (
!this.hass ||
!this.view ||
- !this.cameras ||
!this.viewerConfig ||
!this.cameraManager
) {
@@ -120,7 +115,6 @@ export class FrigateCardViewer extends LitElement {
this,
this.hass,
this.cameraManager,
- this.cameras,
this.view,
{
targetView: 'recording',
@@ -131,7 +125,6 @@ export class FrigateCardViewer extends LitElement {
this,
this.hass,
this.cameraManager,
- this.cameras,
this.view,
{
targetView: 'media',
@@ -148,12 +141,10 @@ export class FrigateCardViewer extends LitElement {
.thumbnailConfig=${this.viewerConfig.controls.thumbnails}
.timelineConfig=${this.viewerConfig.controls.timeline}
.cameraManager=${this.cameraManager}
- .cameras=${this.cameras}
>
;
-
@property({ attribute: false })
public cameraManager?: CameraManager;
@@ -205,19 +193,17 @@ export class FrigateCardViewerCarousel extends LitElement {
// A task to resolve target media if lazy loading is disabled.
protected _mediaResolutionTask = new Task<
- [ViewerConfig | undefined, Map | undefined, View | undefined],
+ [ViewerConfig | undefined, View | undefined],
void
>(
this,
- async ([viewerConfig, cameras, view]: [
+ async ([viewerConfig, view]: [
ViewerConfig | undefined,
- Map | undefined,
View | undefined,
]): Promise => {
if (
!this.hass ||
!viewerConfig?.lazy_load ||
- !cameras ||
!view ||
!view.queryResults?.hasResults()
) {
@@ -234,7 +220,7 @@ export class FrigateCardViewerCarousel extends LitElement {
});
await Promise.all(promises);
},
- () => [this.viewerConfig, this.cameras, this.view],
+ () => [this.viewerConfig, this.view],
);
/**
@@ -453,7 +439,7 @@ export class FrigateCardViewerCarousel extends LitElement {
* @param slide The slide to lazy load.
*/
protected _lazyloadSlide(index: number, slide: HTMLElement): void {
- if (!this.hass || !this.view || !this.view.query || !this.cameras) {
+ if (!this.hass || !this.view || !this.view.query) {
return;
}
@@ -512,7 +498,7 @@ export class FrigateCardViewerCarousel extends LitElement {
* Determine if all the media in the carousel are resolved.
*/
protected _isMediaFullyResolved(): boolean {
- if (!this.resolvedMediaCache || !this.cameras) {
+ if (!this.resolvedMediaCache) {
return false;
}
for (const media of this.view?.queryResults?.getResults() ?? []) {
@@ -560,7 +546,7 @@ export class FrigateCardViewerCarousel extends LitElement {
}
const media = this.view?.queryResults?.getSelectedResult();
- if (!media || !this.cameras || !this.view || !this.view.queryResults) {
+ if (!media || !this.view || !this.view.queryResults) {
return;
}
@@ -649,7 +635,7 @@ export class FrigateCardViewerCarousel extends LitElement {
*/
protected _renderMediaItem(media: ViewMedia, index: number): TemplateResult | null {
// Skip folders as they cannot be rendered by this viewer.
- if (!this.hass || !this.view || !this.viewerConfig || !this.cameras) {
+ if (!this.hass || !this.view || !this.viewerConfig) {
return null;
}
diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json
index c2dafc51..33c69413 100644
--- a/src/localize/languages/en.json
+++ b/src/localize/languages/en.json
@@ -354,6 +354,8 @@
"jsmpeg_no_sign": "Could not retrieve or sign JSMPEG websocket path",
"live_camera_not_found": "The configured camera_entity was not found",
"live_camera_unavailable": "Camera unavailable",
+ "no_camera_engine": "Could not determine suitable engine for camera",
+ "no_camera_entity": "Could not find camera entity",
"no_camera_id": "Could not determine camera id for the following camera, may need to set 'id' parameter manually",
"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'",
"no_cameras": "No valid cameras found, you must configure at least one camera entry",
diff --git a/src/localize/languages/it.json b/src/localize/languages/it.json
index dd735149..d1a05a9e 100644
--- a/src/localize/languages/it.json
+++ b/src/localize/languages/it.json
@@ -325,6 +325,8 @@
"jsmpeg_no_sign": "Impossibile recuperare o firmare il percorso WebSocket JSMPEG",
"live_camera_not_found": "La telecamera configurata non è stata trovata",
"live_camera_unavailable": "Telecamera non disponibile",
+ "no_camera_engine": "",
+ "no_camera_entity": "",
"no_camera_id": "Impossibile determinare l'ID della telecamera , potrebbe essere necessario impostare manualmente il parametro 'ID'",
"no_camera_name": "Impossibile determinare un nome della telecamera in Frigate, si prega di specificare 'camera_enty' o 'camera_name'",
"no_cameras": "Nessuna telecamera valida trovata, è necessario configurare almeno una voce della telecamera",
diff --git a/src/localize/languages/pt-BR.json b/src/localize/languages/pt-BR.json
index 24b125b3..453183e7 100644
--- a/src/localize/languages/pt-BR.json
+++ b/src/localize/languages/pt-BR.json
@@ -325,6 +325,8 @@
"jsmpeg_no_sign": "Não foi possível recuperar ou assinar o caminho do websocket JSMPEG",
"live_camera_not_found": "",
"live_camera_unavailable": "",
+ "no_camera_engine": "",
+ "no_camera_entity": "",
"no_camera_id": "Não foi possível determinar o ID da câmera para a câmera a seguir, pode ser necessário definir o parâmetro 'id' manualmente",
"no_camera_name": "Não foi possível determinar o nome da câmera da Frigate, especifique 'camera_entity' ou 'camera_name' para a câmera a seguir",
"no_cameras": "Nenhuma câmera válida encontrada, você deve configurar pelo menos uma câmera",
diff --git a/src/types.ts b/src/types.ts
index 9f1d433a..67f94e90 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -49,8 +49,6 @@ const FRIGATE_CARD_VIEWS = [
] as const;
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number];
-export type FrigateCardUserSpecifiedView =
- typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED[number];
export const FRIGATE_CARD_VIEW_DEFAULT = 'live' as const;
const FRIGATE_MENU_STYLES = [
@@ -89,6 +87,12 @@ const MEDIA_ACTION_POSITIVE_CONDITIONS = [
export type AutoUnmuteCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
export type AutoPlayCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
+const ENGINES = [
+ 'auto',
+ 'frigate',
+ 'generic',
+] as const;
+
export class FrigateCardError extends Error {
context?: unknown;
@@ -382,6 +386,7 @@ const customSchema = z
*/
const cameraConfigDefault = {
live_provider: 'auto' as const,
+ engine: 'auto' as const,
frigate: {
client_id: 'frigate' as const,
},
@@ -413,6 +418,8 @@ const cameraConfigSchema = z
// this card.
id: z.string().optional(),
+ engine: z.enum(ENGINES).default('auto'),
+
frigate: z
.object({
// No URL validation to allow relative URLs within HA (e.g. Frigate addon).
@@ -445,6 +452,9 @@ const cameraConfigSchema = z
.default(cameraConfigDefault);
export type CameraConfig = z.infer;
+const camerasConfigSchema = cameraConfigSchema.array().nonempty();
+export type CamerasConfig = z.infer;
+
/**
* Custom Element Types.
*/
@@ -1234,7 +1244,7 @@ export interface CardWideConfig {
*/
export const frigateCardConfigSchema = z.object({
// Main configuration sections.
- cameras: cameraConfigSchema.array().nonempty(),
+ cameras: camerasConfigSchema,
view: viewConfigSchema,
menu: menuConfigSchema,
live: liveConfigSchema,
@@ -1351,20 +1361,3 @@ export const signedPathSchema = z.object({
path: z.string(),
});
export type SignedPath = z.infer;
-
-const entitySchema = z.object({
- config_entry_id: z.string().nullable(),
- disabled_by: z.string().nullable(),
- entity_id: z.string(),
- platform: z.string(),
-});
-export type Entity = z.infer;
-
-export const extendedEntitySchema = entitySchema.extend({
- // Extended entity results.
- unique_id: z.string().optional(),
-});
-export type ExtendedEntity = z.infer;
-
-export const entityListSchema = entitySchema.array();
-export type EntityList = z.infer;
diff --git a/src/utils/basic.ts b/src/utils/basic.ts
index dc89e6d8..57316fc7 100644
--- a/src/utils/basic.ts
+++ b/src/utils/basic.ts
@@ -167,11 +167,11 @@ export function getDurationString(start: Date, end: Date): string {
return duration;
}
-export const allPromises = async (
- items: T[],
- func: (arg: T) => void,
-): Promise => {
- await Promise.all(Array.from(items).map((item) => func(item)));
+export const allPromises = async (
+ items: Iterable,
+ func: (arg: T) => R,
+): Promise[]> => {
+ return await Promise.all(Array.from(items).map((item) => func(item)));
};
/**
diff --git a/src/utils/camera.ts b/src/utils/camera.ts
index bcc19710..71af9945 100644
--- a/src/utils/camera.ts
+++ b/src/utils/camera.ts
@@ -26,7 +26,7 @@ export function getCameraID(
/**
* Get all cameras that depend on a given camera.
* @param cameras Cameras map.
- * @param camera Name of the target camera.
+ * @param cameraID ID of the target camera.
* @returns A set of query parameters.
*/
export const getAllDependentCameras = (
diff --git a/src/utils/ha/entity-registry.ts b/src/utils/ha/entity-registry.ts
deleted file mode 100644
index e77f5edf..00000000
--- a/src/utils/ha/entity-registry.ts
+++ /dev/null
@@ -1,109 +0,0 @@
-import { HomeAssistant } from 'custom-card-helpers';
-import { homeAssistantWSRequest } from '.';
-import {
- Entity,
- EntityList,
- entityListSchema,
- ExtendedEntity,
- extendedEntitySchema,
-} from '../../types.js';
-
-export class ExtendedEntityCache {
- protected _cache: Map = new Map();
-
- /**
- * Determine if the cache has a given entity_id.
- * @param id
- * @returns `true` if the id is in the cache, `false` otherwise.
- */
- public has(id: string): boolean {
- return this._cache.has(id);
- }
-
- /**
- * Get the first value that returns true for the given predicate.
- * @param func A callback function that returns a boolean.
- * @returns The first matching value.
- */
- public getMatch(func: (arg: ExtendedEntity) => boolean): ExtendedEntity | null {
- return [...this._cache.values()].find(func) ?? null;
- }
-
- /**
- * Get entity information given an id.
- * @param id The entity id.
- * @returns The `ExtendedEntity` for this id.
- */
- public get(id: string): ExtendedEntity | undefined {
- return this._cache.get(id);
- }
-
- /**
- * Add a given ExtendedEntity to the cache.
- * @param extendedEntity
- */
- public set(extendedEntity: ExtendedEntity): void {
- this._cache.set(extendedEntity.entity_id, extendedEntity);
- }
-}
-
-/**
- * Get the extended entity information for an entity. May throw.
- * @param hass The Home Assistant object.
- * @param entity The entity id.
- * @param cache An optional ExtendedEntityCache.
- * @returns The ExtendedEntity information.
- */
-export const getExtendedEntity = async (
- hass: HomeAssistant,
- entity: string,
- cache?: ExtendedEntityCache,
-): Promise => {
- const cachedValue = cache ? cache.get(entity) : undefined;
- if (cachedValue) {
- return cachedValue;
- }
- const result = await homeAssistantWSRequest(
- hass,
- extendedEntitySchema,
- {
- type: 'config/entity_registry/get',
- entity_id: entity,
- },
- );
- if (cache) {
- cache.set(result);
- }
- return result;
-};
-
-/**
- * Get the extended entity information for an array of entities. May throw.
- * @param hass The Home Assistant object.
- * @param entities An array of entity ids.
- * @param cache An optional ExtendedEntityCache.
- * @returns A map of entity id to ExtendedEntity objects.
- */
-export const getExtendedEntities = async (
- hass: HomeAssistant,
- entities: string[],
- cache?: ExtendedEntityCache,
-): Promise