Initial version of substream support.

This commit is contained in:
Dermot Duffy
2023-02-24 20:41:11 -08:00
parent f8c824b135
commit 812c3e1018
20 changed files with 295 additions and 147 deletions
-1
View File
@@ -6,7 +6,6 @@ import {
DataQuery, DataQuery,
EventQuery, EventQuery,
EventQueryResultsMap, EventQueryResultsMap,
MediaMetadata,
PartialEventQuery, PartialEventQuery,
PartialRecordingQuery, PartialRecordingQuery,
PartialRecordingSegmentsQuery, PartialRecordingSegmentsQuery,
@@ -19,7 +19,6 @@ import {
EventQuery, EventQuery,
EventQueryResults, EventQueryResults,
EventQueryResultsMap, EventQueryResultsMap,
MediaMetadata,
PartialEventQuery, PartialEventQuery,
PartialRecordingQuery, PartialRecordingQuery,
PartialRecordingSegmentsQuery, PartialRecordingSegmentsQuery,
+22 -28
View File
@@ -1,5 +1,9 @@
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import { CameraConfig, CamerasConfig, CardWideConfig } from '../types.js'; import {
CameraConfig,
CamerasConfig,
CardWideConfig,
} from '../types.js';
import { allPromises, arrayify, setify } from '../utils/basic.js'; import { allPromises, arrayify, setify } from '../utils/basic.js';
import { import {
CameraManagerCameraCapabilities, CameraManagerCameraCapabilities,
@@ -46,7 +50,7 @@ import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
import { getCameraID } from '../utils/camera.js'; import { getCameraID } from '../utils/camera.js';
import { localize } from '../localize/localize.js'; import { localize } from '../localize/localize.js';
import { CameraInitializationError } from './error.js'; import { CameraInitializationError } from './error.js';
import { CameraManagerStore } from './store.js'; import { CameraManagerReadOnlyConfigStore, CameraManagerStore } from './store.js';
import cloneDeep from 'lodash-es/cloneDeep'; import cloneDeep from 'lodash-es/cloneDeep';
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js'; import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
@@ -126,11 +130,15 @@ export class CameraManager {
const output: Map<CameraConfig, CameraManagerEngine> = new Map(); const output: Map<CameraConfig, CameraManagerEngine> = new Map();
const engines: Map<Engine, CameraManagerEngine> = new Map(); const engines: Map<Engine, CameraManagerEngine> = new Map();
for (const cameraConfig of camerasConfig) { const getEngineTypes = async (configs: CameraConfig[]) => {
const engineType = await this._engineFactory.getEngineForCamera( return await allPromises(configs, (config) =>
hass, this._engineFactory.getEngineForCamera(hass, config),
cameraConfig,
); );
};
const engineTypes = await getEngineTypes(camerasConfig);
for (const [index, cameraConfig] of camerasConfig.entries()) {
const engineType = engineTypes[index];
const engine = engineType const engine = engineType
? engines.get(engineType) ?? this._engineFactory.createEngine(engineType) ? engines.get(engineType) ?? this._engineFactory.createEngine(engineType)
: null; : null;
@@ -218,8 +226,8 @@ export class CameraManager {
this._store.addCamera(id, result.initializedConfig, result.engine); this._store.addCamera(id, result.initializedConfig, result.engine);
}); });
if (!this._store.getCameraCount()) { if (!this._store.getVisibleCameraCount()) {
throw new CameraInitializationError(localize('error.no_cameras')); throw new CameraInitializationError(localize('error.no_visible_cameras'));
} }
} }
@@ -227,22 +235,8 @@ export class CameraManager {
return this._store.getCameraCount() > 0; return this._store.getCameraCount() > 0;
} }
public getCameras(): Map<string, CameraConfig> | null { public getStore(): CameraManagerReadOnlyConfigStore {
return this._store.getCameras(); return this._store;
}
public getCameraConfig(cameraID: string): CameraConfig | null {
return this._store.getCameraConfig(cameraID);
}
public getCameraIDs(): Set<string> | null {
return this._store.getCameraCount()
? new Set(this._store.getCameras().keys())
: null;
}
public hasCameraID(cameraID: string): boolean {
return this._store.hasCameraID(cameraID);
} }
public generateDefaultEventQueries( public generateDefaultEventQueries(
@@ -287,7 +281,7 @@ export class CameraManager {
const results = await this._handleQuery(hass, query); const results = await this._handleQuery(hass, query);
for (const [query, result] of results?.entries() ?? []) { for (const result of results?.values() ?? []) {
if (result.metadata.what) { if (result.metadata.what) {
result.metadata.what.forEach(what.add, what); result.metadata.what.forEach(what.add, what);
} }
@@ -325,19 +319,19 @@ export class CameraManager {
let queries: DataQuery[] | null = null; let queries: DataQuery[] | null = null;
if (QueryClassifier.isEventQuery(partialQuery)) { if (QueryClassifier.isEventQuery(partialQuery)) {
queries = engine.generateDefaultEventQuery( queries = engine.generateDefaultEventQuery(
this._store.getCameras(), this._store.getVisibleCameras(),
cameraIDs, cameraIDs,
partialQuery, partialQuery,
); );
} else if (QueryClassifier.isRecordingQuery(partialQuery)) { } else if (QueryClassifier.isRecordingQuery(partialQuery)) {
queries = engine.generateDefaultRecordingQuery( queries = engine.generateDefaultRecordingQuery(
this._store.getCameras(), this._store.getVisibleCameras(),
cameraIDs, cameraIDs,
partialQuery, partialQuery,
); );
} else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) { } else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
queries = engine.generateDefaultRecordingSegmentsQuery( queries = engine.generateDefaultRecordingSegmentsQuery(
this._store.getCameras(), this._store.getVisibleCameras(),
cameraIDs, cameraIDs,
partialQuery, partialQuery,
); );
+43 -10
View File
@@ -5,8 +5,26 @@ import { CameraConfigs, Engine } from './types';
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>; type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
export class CameraManagerStore { export interface CameraManagerReadOnlyConfigStore {
protected _configs: Map<string, CameraConfig> = new Map(); getCameraConfig(cameraID: string): CameraConfig | null;
getCameraConfigForMedia(media: ViewMedia): CameraConfig | null;
hasCameraID(cameraID: string): boolean;
hasVisibleCameraID(cameraID: string): boolean;
getCameraCount(): number;
getVisibleCameraCount(): number;
getCameras(): CameraConfigs;
getVisibleCameras(): CameraConfigs;
getCameraIDs(): Set<string>;
getVisibleCameraIDs(): Set<string>;
}
export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
protected _allConfigs: Map<string, CameraConfig> = new Map();
protected _visibleConfigs: Map<string, CameraConfig> = new Map();
protected _enginesByCamera: Map<string, CameraManagerEngine> = new Map(); protected _enginesByCamera: Map<string, CameraManagerEngine> = new Map();
protected _enginesByType: Map<Engine, CameraManagerEngine> = new Map(); protected _enginesByType: Map<Engine, CameraManagerEngine> = new Map();
@@ -15,29 +33,44 @@ export class CameraManagerStore {
cameraConfig: CameraConfig, cameraConfig: CameraConfig,
engine: CameraManagerEngine, engine: CameraManagerEngine,
): void { ): void {
this._configs.set(cameraID, cameraConfig); if (!cameraConfig.hide) {
this._visibleConfigs.set(cameraID, cameraConfig);
}
this._allConfigs.set(cameraID, cameraConfig);
this._enginesByCamera.set(cameraID, engine); this._enginesByCamera.set(cameraID, engine);
this._enginesByType.set(engine.getEngineType(), engine); this._enginesByType.set(engine.getEngineType(), engine);
} }
public getCameraCount(): number { public getCameraConfig(cameraID: string): CameraConfig | null {
return this._configs.size; return this._allConfigs.get(cameraID) ?? null;
} }
public hasCameraID(cameraID: string): boolean { public hasCameraID(cameraID: string): boolean {
return this._configs.has(cameraID); return this._allConfigs.has(cameraID);
}
public hasVisibleCameraID(cameraID: string): boolean {
return this._visibleConfigs.has(cameraID);
} }
public getCameraConfig(cameraID: string): CameraConfig | null { public getCameraCount(): number {
return this._configs.get(cameraID) ?? null; return this._allConfigs.size;
}
public getVisibleCameraCount(): number {
return this._visibleConfigs.size;
} }
public getCameras(): CameraConfigs { public getCameras(): CameraConfigs {
return this._configs; return this._allConfigs;
}
public getVisibleCameras(): CameraConfigs {
return this._visibleConfigs;
} }
public getCameraIDs(): Set<string> { public getCameraIDs(): Set<string> {
return new Set(this._configs.keys()); return new Set(this._allConfigs.keys());
}
public getVisibleCameraIDs(): Set<string> {
return new Set(this._visibleConfigs.keys());
} }
public getCameraConfigForMedia(media: ViewMedia): CameraConfig | null { public getCameraConfigForMedia(media: ViewMedia): CameraConfig | null {
+87 -18
View File
@@ -384,13 +384,14 @@ class FrigateCard extends LitElement {
protected _getMenuButtons(): MenuButton[] { protected _getMenuButtons(): MenuButton[] {
const buttons: MenuButton[] = []; const buttons: MenuButton[] = [];
const cameras = this._cameraManager?.getCameras(); const visibleCameras = this._cameraManager?.getStore().getVisibleCameras();
const selectedCameraID = this._view?.camera; const selectedCameraID = this._view?.camera;
const selectedCameraConfig = this._getSelectedCameraConfig(); const selectedCameraConfig = this._getSelectedCameraConfig();
const allSelectedCameraIDs = const allSelectedCameraIDs = getAllDependentCameras(
cameras && selectedCameraID this._cameraManager,
? getAllDependentCameras(cameras, selectedCameraID) selectedCameraID,
: null; );
const cameraCapabilities = allSelectedCameraIDs const cameraCapabilities = allSelectedCameraIDs
? this._cameraManager?.getAggregateCameraCapabilities(allSelectedCameraIDs) ? this._cameraManager?.getAggregateCameraCapabilities(allSelectedCameraIDs)
: null; : null;
@@ -410,8 +411,8 @@ class FrigateCard extends LitElement {
) as FrigateCardCustomAction, ) as FrigateCardCustomAction,
}); });
if (cameras) { if (visibleCameras) {
const menuItems = Array.from(cameras, ([cameraID, config]) => { const menuItems = Array.from(visibleCameras, ([cameraID, config]) => {
const action = createFrigateCardCustomAction('camera_select', { const action = createFrigateCardCustomAction('camera_select', {
camera: cameraID, camera: cameraID,
}); });
@@ -439,6 +440,61 @@ class FrigateCard extends LitElement {
}); });
} }
if (selectedCameraID && allSelectedCameraIDs && this._view?.is('live')) {
const dependencies = [...allSelectedCameraIDs];
const override = this._view?.context?.live?.overrides?.get(selectedCameraID);
if (dependencies.length === 2) {
// If there are only two dependencies (the main camera, and 1 other)
// then use a button not a menu to toggle.
buttons.push({
icon: 'mdi:video-input-component',
style:
override && override !== selectedCameraID ? this._getEmphasizedStyle() : {},
title: localize('config.menu.buttons.substreams'),
...this._getConfig().menu.buttons.substreams,
type: 'custom:frigate-card-menu-icon',
tap_action: createFrigateCardCustomAction('live_substream_select', {
camera:
override === undefined || override === dependencies[0]
? dependencies[1]
: dependencies[0],
}) as FrigateCardCustomAction,
});
} else if (dependencies.length > 2) {
const menuItems = Array.from(dependencies, (cameraID) => {
const action = createFrigateCardCustomAction('live_substream_select', {
camera: cameraID,
});
const metadata = this._hass
? this._cameraManager?.getCameraMetadata(this._hass, cameraID) ?? undefined
: undefined;
const cameraConfig = this._cameraManager?.getStore().getCameraConfig(cameraID);
return {
enabled: true,
icon: metadata?.icon,
entity: cameraConfig?.camera_entity,
state_color: true,
title: metadata?.title,
selected:
(this._view?.context?.live?.overrides?.get(selectedCameraID) ??
selectedCameraID) === cameraID,
...(action && { tap_action: action }),
};
});
buttons.push({
icon: 'mdi:video-input-component',
title: localize('config.menu.buttons.substreams'),
style:
override && override !== selectedCameraID ? this._getEmphasizedStyle() : {},
...this._getConfig().menu.buttons.substreams,
type: 'custom:frigate-card-menu-submenu',
items: menuItems,
});
}
}
buttons.push({ buttons.push({
icon: 'mdi:cctv', icon: 'mdi:cctv',
...this._getConfig().menu.buttons.live, ...this._getConfig().menu.buttons.live,
@@ -634,7 +690,7 @@ class FrigateCard extends LitElement {
if (!this._view || !this._cameraManager) { if (!this._view || !this._cameraManager) {
return null; return null;
} }
return this._cameraManager.getCameraConfig(this._view.camera); return this._cameraManager.getStore().getCameraConfig(this._view.camera);
} }
/** /**
@@ -767,7 +823,7 @@ class FrigateCard extends LitElement {
protected _changeView(args?: { view?: View; resetMessage?: boolean }): void { protected _changeView(args?: { view?: View; resetMessage?: boolean }): void {
log(this._cardWideConfig, `Frigate Card view change: `, args?.view ?? '[default]'); log(this._cardWideConfig, `Frigate Card view change: `, args?.view ?? '[default]');
const changeView = (view: View): void => { const changeView = (view: View): void => {
if (View.isMediaChange(this._view, view)) { if (View.isMajorMediaChange(this._view, view)) {
this._currentMediaLoadedInfo = null; this._currentMediaLoadedInfo = null;
} }
if (this._view?.view !== view.view) { if (this._view?.view !== view.view) {
@@ -785,7 +841,7 @@ class FrigateCard extends LitElement {
// Load the default view. // Load the default view.
let cameraID: string | null = null; let cameraID: string | null = null;
if (this._cameraManager) { if (this._cameraManager) {
const cameras = this._cameraManager.getCameras(); const cameras = this._cameraManager.getStore().getVisibleCameras();
if (cameras) { if (cameras) {
if (this._view?.camera && this._getConfig().view.update_cycle_camera) { if (this._view?.camera && this._getConfig().view.update_cycle_camera) {
const keys = Array.from(cameras.keys()); const keys = Array.from(cameras.keys());
@@ -1007,7 +1063,7 @@ class FrigateCard extends LitElement {
let changedCamera = false; let changedCamera = false;
let triggerChanges = false; let triggerChanges = false;
const cameras = this._cameraManager?.getCameras(); const cameras = this._cameraManager?.getStore().getVisibleCameras();
for (const [cameraID, config] of cameras?.entries() ?? []) { for (const [cameraID, config] of cameras?.entries() ?? []) {
const triggerEntities = config.triggers.entities ?? []; const triggerEntities = config.triggers.entities ?? [];
const diffs = getHassDifferences(this._hass, oldHass, triggerEntities, { const diffs = getHassDifferences(this._hass, oldHass, triggerEntities, {
@@ -1307,7 +1363,7 @@ class FrigateCard extends LitElement {
*/ */
protected _cardActionHandler(ev: CustomEvent<ActionType>): void { protected _cardActionHandler(ev: CustomEvent<ActionType>): void {
const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail); const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail);
if (!frigateCardAction) { if (!this._view || !frigateCardAction) {
return; return;
} }
const action = frigateCardAction.frigate_card_action; const action = frigateCardAction.frigate_card_action;
@@ -1325,14 +1381,12 @@ class FrigateCard extends LitElement {
case 'snapshot': case 'snapshot':
case 'snapshots': case 'snapshots':
case 'timeline': case 'timeline':
if (this._view) {
this._changeView({ this._changeView({
view: new View({ view: new View({
view: action, view: action,
camera: this._view.camera, camera: this._view.camera,
}), }),
}); });
}
break; break;
case 'download': case 'download':
this._downloadViewerMedia(); this._downloadViewerMedia();
@@ -1355,17 +1409,32 @@ class FrigateCard extends LitElement {
this._refMenu.value?.toggleMenu(); this._refMenu.value?.toggleMenu();
break; break;
case 'camera_select': case 'camera_select':
const cameraID = frigateCardAction.camera; const selectCameraID = frigateCardAction.camera;
if (this._cameraManager?.hasCameraID(cameraID) && this._view) { if (
this._view &&
this._cameraManager?.getStore().hasVisibleCameraID(selectCameraID)
) {
const viewOnCameraSelect = this._getConfig().view.camera_select; const viewOnCameraSelect = this._getConfig().view.camera_select;
const targetView = const targetView =
viewOnCameraSelect === 'current' ? this._view.view : viewOnCameraSelect; viewOnCameraSelect === 'current' ? this._view.view : viewOnCameraSelect;
const actualView = this.isViewSupportedByCamera(cameraID, targetView) const actualView = this.isViewSupportedByCamera(selectCameraID, targetView)
? targetView ? targetView
: FRIGATE_CARD_VIEW_DEFAULT; : FRIGATE_CARD_VIEW_DEFAULT;
this._changeView({ view: new View({ view: actualView, camera: cameraID }) }); this._changeView({
view: new View({ view: actualView, camera: selectCameraID }),
});
} }
break; break;
case 'live_substream_select':
const overrides: Map<string, string> =
this._view.context?.live?.overrides ?? new Map();
overrides.set(this._view.camera, frigateCardAction.camera);
this._changeView({
view: this._view.clone().mergeInContext({
live: { overrides: overrides },
}),
});
break;
case 'media_player': case 'media_player':
this._mediaPlayerAction( this._mediaPlayerAction(
frigateCardAction.media_player, frigateCardAction.media_player,
+43 -20
View File
@@ -56,6 +56,18 @@ import { dispatchMessageEvent, dispatchErrorMessageEvent } from '../message.js';
import { HassEntity } from 'home-assistant-js-websocket'; import { HassEntity } from 'home-assistant-js-websocket';
import { CameraEndpoints } from '../../camera-manager/types.js'; import { CameraEndpoints } from '../../camera-manager/types.js';
interface LiveViewContext {
// A cameraID override (used for dependencies/substreams to force a different
// camera to be live rather than the camera selected in the view).
overrides?: Map<string, string>;
}
declare module 'view' {
interface ViewContext {
live?: LiveViewContext;
}
}
/** /**
* Get the state object or dispatch an error. Used in `ha` and `image` live * Get the state object or dispatch an error. Used in `ha` and `image` live
* providers. * providers.
@@ -339,7 +351,7 @@ export class FrigateCardLiveCarousel extends LitElement {
} }
protected _getSelectedCameraIndex(): number { protected _getSelectedCameraIndex(): number {
const cameraIDs = this.cameraManager?.getCameraIDs(); const cameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
if (!cameraIDs || !this.view) { if (!cameraIDs || !this.view) {
return 0; return 0;
} }
@@ -362,7 +374,7 @@ export class FrigateCardLiveCarousel extends LitElement {
* @returns A list of EmblaOptionsTypes. * @returns A list of EmblaOptionsTypes.
*/ */
protected _getPlugins(): EmblaCarouselPlugins { protected _getPlugins(): EmblaCarouselPlugins {
const cameras = this.cameraManager?.getCameraIDs(); const cameras = this.cameraManager?.getStore().getVisibleCameraIDs();
return [ return [
// Only enable wheel plugin if there is more than one camera. // Only enable wheel plugin if there is more than one camera.
...(cameras && cameras.size > 1 ...(cameras && cameras.size > 1
@@ -420,18 +432,27 @@ export class FrigateCardLiveCarousel extends LitElement {
* name to slide number. * name to slide number.
*/ */
protected _getSlides(): [TemplateResult[], Record<string, number>] { protected _getSlides(): [TemplateResult[], Record<string, number>] {
const cameras = this.cameraManager?.getCameras(); const visibleCameras = this.cameraManager?.getStore().getVisibleCameras();
if (!cameras) { if (!visibleCameras) {
return [[], {}]; return [[], {}];
} }
const slides: TemplateResult[] = []; const slides: TemplateResult[] = [];
const cameraToSlide: Record<string, number> = {}; const cameraToSlide: Record<string, number> = {};
for (const [camera, cameraConfig] of cameras) { for (const [cameraID, cameraConfig] of visibleCameras) {
const slide = this._renderLive(camera, cameraConfig, slides.length); const liveCameraID =
this.view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
const liveCameraConfig =
cameraID === liveCameraID
? cameraConfig
: this.cameraManager?.getStore().getCameraConfig(liveCameraID);
const slide = liveCameraConfig
? this._renderLive(liveCameraID, liveCameraConfig, slides.length)
: null;
if (slide) { if (slide) {
cameraToSlide[camera] = slides.length; cameraToSlide[cameraID] = slides.length;
slides.push(slide); slides.push(slide);
} }
} }
@@ -442,7 +463,7 @@ export class FrigateCardLiveCarousel extends LitElement {
* Handle the user selecting a new slide in the carousel. * Handle the user selecting a new slide in the carousel.
*/ */
protected _setViewHandler(ev: CustomEvent<CarouselSelect>): void { protected _setViewHandler(ev: CustomEvent<CarouselSelect>): void {
const cameras = this.cameraManager?.getCameras(); const cameras = this.cameraManager?.getStore().getVisibleCameras();
if (cameras && ev.detail.index !== this._getSelectedCameraIndex()) { if (cameras && ev.detail.index !== this._getSelectedCameraIndex()) {
this._setViewCameraID(Array.from(cameras.keys())[ev.detail.index]); this._setViewCameraID(Array.from(cameras.keys())[ev.detail.index]);
} }
@@ -515,7 +536,7 @@ export class FrigateCardLiveCarousel extends LitElement {
<frigate-card-live-provider <frigate-card-live-provider
?disabled=${this.liveConfig.lazy_load} ?disabled=${this.liveConfig.lazy_load}
.cameraConfig=${cameraConfig} .cameraConfig=${cameraConfig}
.cameraEndpoints=${guard([this.cameraManager], () => .cameraEndpoints=${guard([this.cameraManager, cameraID], () =>
this.cameraManager?.getCameraEndpoints(cameraID), this.cameraManager?.getCameraEndpoints(cameraID),
)} )}
.label=${cameraMetadata?.title ?? ''} .label=${cameraMetadata?.title ?? ''}
@@ -535,7 +556,7 @@ export class FrigateCardLiveCarousel extends LitElement {
} }
protected _getCameraIDsOfNeighbors(): [string | null, string | null] { protected _getCameraIDsOfNeighbors(): [string | null, string | null] {
const cameras = this.cameraManager?.getCameras(); const cameras = this.cameraManager?.getStore().getVisibleCameras();
if (!cameras || !this.view || !this.hass) { if (!cameras || !this.view || !this.hass) {
return [null, null]; return [null, null];
} }
@@ -557,15 +578,13 @@ export class FrigateCardLiveCarousel extends LitElement {
* @returns A template to display to the user. * @returns A template to display to the user.
*/ */
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
if (!this.liveConfig || !this.view || !this.hass || !this.cameraManager) {
return;
}
const [slides, cameraToSlide] = this._getSlides(); const [slides, cameraToSlide] = this._getSlides();
this._cameraToSlide = cameraToSlide; this._cameraToSlide = cameraToSlide;
if ( if (!slides.length) {
!slides.length ||
!this.liveConfig ||
!this.view ||
!this.hass ||
!this.cameraManager
) {
return; return;
} }
@@ -577,15 +596,19 @@ export class FrigateCardLiveCarousel extends LitElement {
const [prevID, nextID] = this._getCameraIDsOfNeighbors(); const [prevID, nextID] = this._getCameraIDsOfNeighbors();
const overrideCameraID = (cameraID: string): string => {
return this.view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
};
const cameraMetadataPrevious = prevID const cameraMetadataPrevious = prevID
? this.cameraManager.getCameraMetadata(this.hass, prevID) ? this.cameraManager.getCameraMetadata(this.hass, overrideCameraID(prevID))
: null; : null;
const cameraMetadataCurrent = this.cameraManager.getCameraMetadata( const cameraMetadataCurrent = this.cameraManager.getCameraMetadata(
this.hass, this.hass,
this.view.camera, overrideCameraID(this.view.camera),
); );
const cameraMetadataNext = nextID const cameraMetadataNext = nextID
? this.cameraManager.getCameraMetadata(this.hass, nextID) ? this.cameraManager.getCameraMetadata(this.hass, overrideCameraID(nextID))
: null; : null;
// Notes on the below: // Notes on the below:
+7 -10
View File
@@ -31,10 +31,7 @@ import { MediaQueriesClassifier } from '../view/media-queries-classifier';
import { View } from '../view/view'; import { View } from '../view/view';
import { CameraManager } from '../camera-manager/manager'; import { CameraManager } from '../camera-manager/manager';
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import { import { DataQuery, MediaMetadata, QueryType } from '../camera-manager/types';
MediaMetadata,
QueryType,
} from '../camera-manager/types';
import format from 'date-fns/format'; import format from 'date-fns/format';
import endOfMonth from 'date-fns/endOfMonth'; import endOfMonth from 'date-fns/endOfMonth';
import isEqual from 'lodash-es/isEqual'; import isEqual from 'lodash-es/isEqual';
@@ -168,7 +165,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
_ev: CustomEvent<{ value: unknown }>, _ev: CustomEvent<{ value: unknown }>,
): Promise<void> { ): Promise<void> {
const cameras = this.cameraManager?.getCameras(); const cameras = this.cameraManager?.getStore().getVisibleCameras();
if (!this.hass || !cameras || !this.cameraManager || !this.view) { if (!this.hass || !cameras || !this.cameraManager || !this.view) {
return; return;
} }
@@ -269,7 +266,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
protected willUpdate(changedProps: PropertyValues): void { protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('cameraManager')) { if (changedProps.has('cameraManager')) {
const cameras = this.cameraManager?.getCameras(); const cameras = this.cameraManager?.getStore().getVisibleCameras();
if (cameras) { if (cameras) {
this._cameraOptions = Array.from(cameras.keys()).map((cameraID) => ({ this._cameraOptions = Array.from(cameras.keys()).map((cameraID) => ({
value: cameraID, value: cameraID,
@@ -321,7 +318,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
protected _getDefaultsFromView(): MediaFilterCoreDefaults | null { protected _getDefaultsFromView(): MediaFilterCoreDefaults | null {
const queries = this.view?.query?.getQueries(); const queries = this.view?.query?.getQueries();
const cameras = this.cameraManager?.getCameras(); const cameras = this.cameraManager?.getStore().getVisibleCameras();
if (!this.view || !queries || !cameras) { if (!this.view || !queries || !cameras) {
return null; return null;
} }
@@ -333,12 +330,12 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
let favorite: MediaFilterCoreFavoriteSelection | undefined; let favorite: MediaFilterCoreFavoriteSelection | undefined;
const cameraIDSets = uniqWith( const cameraIDSets = uniqWith(
queries.map((query) => query.cameraIDs), queries.map((query: DataQuery) => query.cameraIDs),
isEqual, isEqual,
); );
// Special note: If all cameras are selected, this is the same as no // Special note: If all visible cameras are selected, this is the same as no
// selector at all. // selector at all.
if (cameraIDSets.length === 1 && queries[0].cameraIDs.size !== cameras.size) { if (cameraIDSets.length === 1 && isEqual(queries[0].cameraIDs, cameras)) {
cameraIDs = [...queries[0].cameraIDs]; cameraIDs = [...queries[0].cameraIDs];
} }
+8 -5
View File
@@ -22,6 +22,7 @@ import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
import './surround-basic.js'; import './surround-basic.js';
import { changeViewToRecentEventsForCameraAndDependents } from '../utils/media-to-view'; import { changeViewToRecentEventsForCameraAndDependents } from '../utils/media-to-view';
import { getAllDependentCameras } from '../utils/camera.js'; import { getAllDependentCameras } from '../utils/camera.js';
import type { DataQuery } from '../camera-manager/types';
interface ThumbnailViewContext { interface ThumbnailViewContext {
// Whether or not to fetch thumbnails. // Whether or not to fetch thumbnails.
@@ -120,7 +121,7 @@ export class FrigateCardSurround extends LitElement {
// user is scrubbing video). // user is scrubbing video).
if ( if (
changedProperties.has('view') && changedProperties.has('view') &&
View.isMediaChange(changedProperties.get('view'), this.view) View.isMajorMediaChange(changedProperties.get('view'), this.view)
) { ) {
this._cameraIDsForTimeline = this._getCameraIDsForTimeline() ?? undefined; this._cameraIDsForTimeline = this._getCameraIDsForTimeline() ?? undefined;
} }
@@ -138,18 +139,20 @@ export class FrigateCardSurround extends LitElement {
} }
protected _getCameraIDsForTimeline(): Set<string> | null { protected _getCameraIDsForTimeline(): Set<string> | null {
const cameras = this.cameraManager?.getCameras(); if (!this.view) {
if (!this.view || !cameras) {
return null; return null;
} }
if (this.view?.is('live')) { if (this.view?.is('live')) {
return getAllDependentCameras(cameras, this.view.camera); return getAllDependentCameras(
this.cameraManager,
this.view.camera,
);
} }
if (this.view.isViewerView()) { if (this.view.isViewerView()) {
return new Set( return new Set(
this.view.query this.view.query
?.getQueries() ?.getQueries()
?.map((query) => [...query.cameraIDs]) ?.map((query: DataQuery) => [...query.cameraIDs])
.flat(), .flat(),
); );
} }
+5 -15
View File
@@ -238,7 +238,7 @@ export class FrigateCardTimelineCore extends LitElement {
const item = request.detail.item; const item = request.detail.item;
const media = this._timelineSource?.dataset.get(item)?.media; const media = this._timelineSource?.dataset.get(item)?.media;
const cameraConfig = media const cameraConfig = media
? this.cameraManager?.getCameraConfig(media.getCameraID()) ?? undefined ? this.cameraManager?.getStore().getCameraConfigForMedia(media) ?? undefined
: undefined; : undefined;
request.detail.hass = this.hass; request.detail.hass = this.hass;
@@ -291,15 +291,9 @@ export class FrigateCardTimelineCore extends LitElement {
* @returns A set of camera ids (may be empty). * @returns A set of camera ids (may be empty).
*/ */
protected _getTimelineCameraIDs(): Set<string> | null { protected _getTimelineCameraIDs(): Set<string> | null {
return this.cameraIDs ?? this._getAllCameraIDs(); return (
} this.cameraIDs ?? this.cameraManager?.getStore().getVisibleCameraIDs() ?? null
);
/**
* Get all the keys of all cameras.
* @returns A set of camera ids (may be empty).
*/
protected _getAllCameraIDs(): Set<string> | null {
return this.cameraManager?.getCameraIDs() ?? null;
} }
/** /**
@@ -1042,11 +1036,7 @@ export class FrigateCardTimelineCore extends LitElement {
changedProps.has('cameraIDs') changedProps.has('cameraIDs')
) { ) {
const cameraIDs = this._getTimelineCameraIDs(); const cameraIDs = this._getTimelineCameraIDs();
if ( if (cameraIDs && this.cameraManager && this.timelineConfig) {
cameraIDs &&
this.cameraManager &&
this.timelineConfig
) {
this._timelineSource = new TimelineDataSource( this._timelineSource = new TimelineDataSource(
this.cameraManager, this.cameraManager,
cameraIDs, cameraIDs,
+1
View File
@@ -13,6 +13,7 @@ export const CONF_CAMERAS_ARRAY_FRIGATE_LABEL =
export const CONF_CAMERAS_ARRAY_FRIGATE_URL = `${CONF_CAMERAS}.#.frigate.url` as const; export const CONF_CAMERAS_ARRAY_FRIGATE_URL = `${CONF_CAMERAS}.#.frigate.url` as const;
export const CONF_CAMERAS_ARRAY_FRIGATE_ZONE = `${CONF_CAMERAS}.#.frigate.zone` as const; export const CONF_CAMERAS_ARRAY_FRIGATE_ZONE = `${CONF_CAMERAS}.#.frigate.zone` as const;
export const CONF_CAMERAS_ARRAY_GO2RTC_MODES = `${CONF_CAMERAS}.#.go2rtc.modes` as const; export const CONF_CAMERAS_ARRAY_GO2RTC_MODES = `${CONF_CAMERAS}.#.go2rtc.modes` as const;
export const CONF_CAMERAS_ARRAY_HIDE = `${CONF_CAMERAS}.#.hide` as const;
export const CONF_CAMERAS_ARRAY_ID = `${CONF_CAMERAS}.#.id` as const; export const CONF_CAMERAS_ARRAY_ID = `${CONF_CAMERAS}.#.id` as const;
export const CONF_CAMERAS_ARRAY_TITLE = `${CONF_CAMERAS}.#.title` as const; export const CONF_CAMERAS_ARRAY_TITLE = `${CONF_CAMERAS}.#.title` as const;
export const CONF_CAMERAS_ARRAY_ICON = `${CONF_CAMERAS}.#.icon` as const; export const CONF_CAMERAS_ARRAY_ICON = `${CONF_CAMERAS}.#.icon` as const;
+9
View File
@@ -130,6 +130,7 @@ import {
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE, CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
MEDIA_CHUNK_SIZE_MAX, MEDIA_CHUNK_SIZE_MAX,
CONF_CAMERAS_ARRAY_GO2RTC_MODES, CONF_CAMERAS_ARRAY_GO2RTC_MODES,
CONF_CAMERAS_ARRAY_HIDE,
} from './const.js'; } from './const.js';
import { localize } from './localize/localize.js'; import { localize } from './localize/localize.js';
import frigate_card_editor_style from './scss/editor.scss'; import frigate_card_editor_style from './scss/editor.scss';
@@ -1341,6 +1342,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderStringInput( ${this._renderStringInput(
getArrayConfigPath(CONF_CAMERAS_ARRAY_ID, cameraIndex), getArrayConfigPath(CONF_CAMERAS_ARRAY_ID, cameraIndex),
)} )}
${this._renderSwitch(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_HIDE,
cameraIndex,
),
this._defaults.cameras.hide,
)}
${this._putInSubmenu( ${this._putInSubmenu(
MENU_CAMERAS_FRIGATE, MENU_CAMERAS_FRIGATE,
cameraIndex, cameraIndex,
@@ -1604,6 +1612,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
})} })}
${this._renderMenuButton('frigate') /* */} ${this._renderMenuButton('frigate') /* */}
${this._renderMenuButton('cameras') /* */} ${this._renderMenuButton('cameras') /* */}
${this._renderMenuButton('substreams') /* */}
${this._renderMenuButton('live') /* */} ${this._renderMenuButton('live') /* */}
${this._renderMenuButton('clips') /* */} ${this._renderMenuButton('clips') /* */}
${this._renderMenuButton('snapshots')} ${this._renderMenuButton('snapshots')}
+4 -1
View File
@@ -33,6 +33,7 @@
"mjpeg": "Motion JPEG (MJPEG)" "mjpeg": "Motion JPEG (MJPEG)"
} }
}, },
"hide": "Hide camera from UI",
"icon": "Icon for this camera (Autodetected from entity)", "icon": "Icon for this camera (Autodetected from entity)",
"id": "Unique id for this camera in this card", "id": "Unique id for this camera in this card",
"live_provider": "Live view provider for this camera", "live_provider": "Live view provider for this camera",
@@ -239,6 +240,7 @@
"media_player": "Send to media player", "media_player": "Send to media player",
"priority": "Priority", "priority": "Priority",
"snapshots": "Snapshots", "snapshots": "Snapshots",
"substreams": "Substream(s)",
"timeline": "Timeline", "timeline": "Timeline",
"recordings": "Recordings" "recordings": "Recordings"
}, },
@@ -368,10 +370,11 @@
"live_camera_unavailable": "Camera unavailable", "live_camera_unavailable": "Camera unavailable",
"no_camera_engine": "Could not determine suitable engine for camera", "no_camera_engine": "Could not determine suitable engine for camera",
"no_camera_entity": "Could not find camera entity", "no_camera_entity": "Could not find camera entity",
"no_camera_entity_for_triggers": "A camera entity is required in order to autodetect triggers",
"no_camera_id": "Could not determine camera id for the following camera, may need to set 'id' parameter manually", "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_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",
"no_live_camera": "The camera_entity parameter must be set and valid for this live provider", "no_live_camera": "The camera_entity parameter must be set and valid for this live provider",
"no_visible_cameras": "No visible cameras found, you must configure at least one non-hidden camera",
"reconnecting": "Reconnecting", "reconnecting": "Reconnecting",
"timeline_no_cameras": "No Frigate cameras to show in timeline", "timeline_no_cameras": "No Frigate cameras to show in timeline",
"troubleshooting": "Check troubleshooting", "troubleshooting": "Check troubleshooting",
+4 -1
View File
@@ -23,6 +23,7 @@
"url": "Frigate URL del server", "url": "Frigate URL del server",
"zone": "Frigate zona" "zone": "Frigate zona"
}, },
"hide": "",
"icon": "Icona per questa telecamera (Autoidentificato dall'entità)", "icon": "Icona per questa telecamera (Autoidentificato dall'entità)",
"id": "ID univoco per questa telecamera in questa carta", "id": "ID univoco per questa telecamera in questa carta",
"live_provider": "Provider di visualizzazione dal vivo per questa telecamera", "live_provider": "Provider di visualizzazione dal vivo per questa telecamera",
@@ -223,6 +224,7 @@
"live": "Abitare", "live": "Abitare",
"media_player": "Invia a Media Player", "media_player": "Invia a Media Player",
"priority": "Priorità", "priority": "Priorità",
"substreams": "",
"snapshots": "Istantanee", "snapshots": "Istantanee",
"timeline": "Timeline" "timeline": "Timeline"
}, },
@@ -347,10 +349,11 @@
"live_camera_unavailable": "Telecamera non disponibile", "live_camera_unavailable": "Telecamera non disponibile",
"no_camera_engine": "", "no_camera_engine": "",
"no_camera_entity": "", "no_camera_entity": "",
"no_camera_entity_for_triggers": "",
"no_camera_id": "Impossibile determinare l'ID della telecamera , potrebbe essere necessario impostare manualmente il parametro 'ID'", "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_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",
"no_live_camera": "Il parametro fotocamera_enty deve essere impostato e valido per questo provider live", "no_live_camera": "Il parametro fotocamera_enty deve essere impostato e valido per questo provider live",
"no_visible_cameras": "",
"reconnecting": "Riconnessione", "reconnecting": "Riconnessione",
"timeline_no_cameras": "Nessuna telecamera damostrare in Frigate nella timeline", "timeline_no_cameras": "Nessuna telecamera damostrare in Frigate nella timeline",
"troubleshooting": "Controllare la risoluzione dei problemi", "troubleshooting": "Controllare la risoluzione dei problemi",
+4 -1
View File
@@ -23,6 +23,7 @@
"url": "URL do servidor Frigate", "url": "URL do servidor Frigate",
"zone": "Zona do Frigate" "zone": "Zona do Frigate"
}, },
"hide": "",
"icon": "Ícone para esta câmera (detectado automaticamente pela entidade)", "icon": "Ícone para esta câmera (detectado automaticamente pela entidade)",
"id": "ID exclusivo para esta câmera nesse cartão", "id": "ID exclusivo para esta câmera nesse cartão",
"live_provider": "Provedor de visualização ao vivo para esta câmera", "live_provider": "Provedor de visualização ao vivo para esta câmera",
@@ -223,6 +224,7 @@
"live": "Ao vivo", "live": "Ao vivo",
"media_player": "Enviar para o reprodutor de mídia", "media_player": "Enviar para o reprodutor de mídia",
"priority": "Prioridade", "priority": "Prioridade",
"substreams": "",
"snapshots": "Instantâneos", "snapshots": "Instantâneos",
"timeline": "Linha do tempo" "timeline": "Linha do tempo"
}, },
@@ -347,10 +349,11 @@
"live_camera_unavailable": "", "live_camera_unavailable": "",
"no_camera_engine": "", "no_camera_engine": "",
"no_camera_entity": "", "no_camera_entity": "",
"no_camera_entity_for_triggers": "",
"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_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_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",
"no_live_camera": "O parâmetro camera_entity deve ser definido e válido para este provedor ativo", "no_live_camera": "O parâmetro camera_entity deve ser definido e válido para este provedor ativo",
"no_visible_cameras": "",
"reconnecting": "Reconectando", "reconnecting": "Reconectando",
"timeline_no_cameras": "Nenhuma câmera do Frigate para mostrar na linha do tempo", "timeline_no_cameras": "Nenhuma câmera do Frigate para mostrar na linha do tempo",
"troubleshooting": "Verifique a solução de problemas", "troubleshooting": "Verifique a solução de problemas",
+17 -4
View File
@@ -199,7 +199,7 @@ const noActionSchema = schemaForType<
}), }),
); );
const frigateCardCustomactionsBaseSchema = customActionSchema.extend({ const frigateCardCustomActionsBaseSchema = customActionSchema.extend({
action: z action: z
.literal('custom:frigate-card-action') .literal('custom:frigate-card-action')
// Syntactic sugar to avoid 'fire-dom-event' as part of an external API. // Syntactic sugar to avoid 'fire-dom-event' as part of an external API.
@@ -227,18 +227,24 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
const FRIGATE_CARD_ACTIONS = [ const FRIGATE_CARD_ACTIONS = [
...FRIGATE_CARD_GENERAL_ACTIONS, ...FRIGATE_CARD_GENERAL_ACTIONS,
'camera_select', 'camera_select',
'live_substream_select',
'media_player', 'media_player',
] as const; ] as const;
export type FrigateCardAction = (typeof FRIGATE_CARD_ACTIONS)[number]; export type FrigateCardAction = (typeof FRIGATE_CARD_ACTIONS)[number];
const frigateCardGeneralActionSchema = frigateCardCustomactionsBaseSchema.extend({ const frigateCardGeneralActionSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.enum(FRIGATE_CARD_GENERAL_ACTIONS), frigate_card_action: z.enum(FRIGATE_CARD_GENERAL_ACTIONS),
}); });
const frigateCardCameraSelectActionSchema = frigateCardCustomactionsBaseSchema.extend({ const frigateCardCameraSelectActionSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('camera_select'), frigate_card_action: z.literal('camera_select'),
camera: z.string(), camera: z.string(),
}); });
const frigateCarMediaPlayerActionSchema = frigateCardCustomactionsBaseSchema.extend({ const frigateCardLiveDependencySelectActionSchema =
frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('live_substream_select'),
camera: z.string(),
});
const frigateCarMediaPlayerActionSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('media_player'), frigate_card_action: z.literal('media_player'),
media_player: z.string(), media_player: z.string(),
media_player_action: z.enum(['play', 'stop']), media_player_action: z.enum(['play', 'stop']),
@@ -247,6 +253,7 @@ const frigateCarMediaPlayerActionSchema = frigateCardCustomactionsBaseSchema.ext
export const frigateCardCustomActionSchema = z.union([ export const frigateCardCustomActionSchema = z.union([
frigateCardGeneralActionSchema, frigateCardGeneralActionSchema,
frigateCardCameraSelectActionSchema, frigateCardCameraSelectActionSchema,
frigateCardLiveDependencySelectActionSchema,
frigateCarMediaPlayerActionSchema, frigateCarMediaPlayerActionSchema,
]); ]);
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>; export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
@@ -398,6 +405,7 @@ const cameraConfigDefault = {
all_cameras: false, all_cameras: false,
cameras: [], cameras: [],
}, },
hide: false,
triggers: { triggers: {
motion: false, motion: false,
occupancy: true, occupancy: true,
@@ -418,6 +426,9 @@ const cameraConfigSchema = z
icon: z.string().optional(), icon: z.string().optional(),
title: z.string().optional(), title: z.string().optional(),
// Used to hide the camera (e.g. when used only as a dependency).
hide: z.boolean().optional(),
// Optional identifier to separate different camera configurations used in // Optional identifier to separate different camera configurations used in
// this card. // this card.
id: z.string().optional(), id: z.string().optional(),
@@ -944,6 +955,7 @@ const menuConfigDefault = {
buttons: { buttons: {
frigate: visibleButtonDefault, frigate: visibleButtonDefault,
cameras: visibleButtonDefault, cameras: visibleButtonDefault,
substreams: visibleButtonDefault,
live: visibleButtonDefault, live: visibleButtonDefault,
clips: visibleButtonDefault, clips: visibleButtonDefault,
snapshots: visibleButtonDefault, snapshots: visibleButtonDefault,
@@ -976,6 +988,7 @@ const menuConfigSchema = z
.object({ .object({
frigate: visibleButtonSchema.default(menuConfigDefault.buttons.frigate), frigate: visibleButtonSchema.default(menuConfigDefault.buttons.frigate),
cameras: visibleButtonSchema.default(menuConfigDefault.buttons.cameras), cameras: visibleButtonSchema.default(menuConfigDefault.buttons.cameras),
substreams: visibleButtonSchema.default(menuConfigDefault.buttons.substreams),
live: visibleButtonSchema.default(menuConfigDefault.buttons.live), live: visibleButtonSchema.default(menuConfigDefault.buttons.live),
clips: visibleButtonSchema.default(menuConfigDefault.buttons.clips), clips: visibleButtonSchema.default(menuConfigDefault.buttons.clips),
snapshots: visibleButtonSchema.default(menuConfigDefault.buttons.snapshots), snapshots: visibleButtonSchema.default(menuConfigDefault.buttons.snapshots),
+3 -3
View File
@@ -2,7 +2,7 @@ import {
ActionConfig, ActionConfig,
handleActionConfig, handleActionConfig,
hasAction, hasAction,
HomeAssistant HomeAssistant,
} from 'custom-card-helpers'; } from 'custom-card-helpers';
import { import {
Actions, Actions,
@@ -10,7 +10,7 @@ import {
ActionType, ActionType,
FrigateCardAction, FrigateCardAction,
FrigateCardCustomAction, FrigateCardCustomAction,
frigateCardCustomActionSchema frigateCardCustomActionSchema,
} from '../types.js'; } from '../types.js';
/** /**
@@ -43,7 +43,7 @@ export function createFrigateCardCustomAction(
media_player_action?: 'play' | 'stop'; media_player_action?: 'play' | 'stop';
}, },
): FrigateCardCustomAction | null { ): FrigateCardCustomAction | null {
if (action === 'camera_select') { if (action === 'camera_select' || action === 'live_substream_select') {
if (!args?.camera) { if (!args?.camera) {
return null; return null;
} }
+10 -4
View File
@@ -1,3 +1,4 @@
import { CameraManager } from '../camera-manager/manager.js';
import { CameraConfig, RawFrigateCardConfig } from '../types.js'; import { CameraConfig, RawFrigateCardConfig } from '../types.js';
/** /**
@@ -25,14 +26,19 @@ export function getCameraID(
/** /**
* Get all cameras that depend on a given camera. * Get all cameras that depend on a given camera.
* @param cameras Cameras map. * @param cameraManager The camera manager.
* @param cameraID ID of the target camera. * @param cameraID ID of the target camera.
* @returns A set of query parameters. * @returns A set of dependent cameraIDs or null.
*/ */
export const getAllDependentCameras = ( export const getAllDependentCameras = (
cameras: Map<string, CameraConfig>, cameraManager?: CameraManager,
cameraID?: string, cameraID?: string,
): Set<string> => { ): Set<string> | null => {
if (!cameraManager || !cameraID) {
return null;
}
const cameras = cameraManager.getStore().getCameras();
const cameraIDs: Set<string> = new Set(); const cameraIDs: Set<string> = new Set();
const getDependentCameras = (cameraID: string): void => { const getDependentCameras = (cameraID: string): void => {
const cameraConfig = cameras.get(cameraID); const cameraConfig = cameras.get(cameraID);
+1 -1
View File
@@ -53,7 +53,7 @@ export class EntityRegistryManager {
public async getExtendedEntity( public async getExtendedEntity(
hass: HomeAssistant, hass: HomeAssistant,
entityID: string, entityID: string,
): Promise<ExtendedEntity | null> { ): Promise<ExtendedEntity> {
const cachedValue = this._extendedCache.get(entityID); const cachedValue = this._extendedCache.get(entityID);
if (cachedValue) { if (cachedValue) {
return cachedValue; return cachedValue;
+5 -7
View File
@@ -27,11 +27,11 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
targetView?: FrigateCardView; targetView?: FrigateCardView;
}, },
): Promise<void> => { ): Promise<void> => {
const cameras = cameraManager.getCameras(); const cameraIDs = getAllDependentCameras(cameraManager, view.camera);
if (!cameras) { if (!cameraIDs) {
return; return;
} }
const cameraIDs = new Set(getAllDependentCameras(cameras, view.camera));
const queries = createQueriesForEventsView(cameraManager, cardWideConfig, cameraIDs, { const queries = createQueriesForEventsView(cameraManager, cardWideConfig, cameraIDs, {
mediaType: options?.mediaType, mediaType: options?.mediaType,
}); });
@@ -83,18 +83,16 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
targetView?: 'recording' | 'recordings'; targetView?: 'recording' | 'recordings';
}, },
): Promise<void> => { ): Promise<void> => {
const cameras = cameraManager.getCameras(); const cameraIDs = getAllDependentCameras(cameraManager, view.camera);
if (!cameras) { if (!cameraIDs) {
return; return;
} }
const cameraIDs = new Set(getAllDependentCameras(cameras, view.camera));
const queries = createQueriesForRecordingsView( const queries = createQueriesForRecordingsView(
cameraManager, cameraManager,
cardWideConfig, cardWideConfig,
cameraIDs, cameraIDs,
); );
if (!queries) { if (!queries) {
return; return;
} }
+6 -1
View File
@@ -39,12 +39,17 @@ export class View {
* @param curr The current view. * @param curr The current view.
* @returns True if the view change is a real media change. * @returns True if the view change is a real media change.
*/ */
public static isMediaChange(prev?: View, curr?: View): boolean { public static isMajorMediaChange(prev?: View, curr?: View): boolean {
return ( return (
!prev || !prev ||
!curr || !curr ||
prev.view !== curr.view || prev.view !== curr.view ||
prev.camera !== curr.camera || prev.camera !== curr.camera ||
// When in live mode, take overrides into account in deciding if this is a
// major media change.
(curr.view === 'live' &&
prev.context?.live?.overrides?.get(prev.camera) !==
curr.context?.live?.overrides?.get(curr.camera)) ||
// When in the live view, the queryResults contain the events that // When in the live view, the queryResults contain the events that
// happened in the past -- not reflective of the actual live media viewer // happened in the past -- not reflective of the actual live media viewer
// the user is seeing. // the user is seeing.