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,
EventQuery,
EventQueryResultsMap,
MediaMetadata,
PartialEventQuery,
PartialRecordingQuery,
PartialRecordingSegmentsQuery,
@@ -19,7 +19,6 @@ import {
EventQuery,
EventQueryResults,
EventQueryResultsMap,
MediaMetadata,
PartialEventQuery,
PartialRecordingQuery,
PartialRecordingSegmentsQuery,
+22 -28
View File
@@ -1,5 +1,9 @@
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 {
CameraManagerCameraCapabilities,
@@ -46,7 +50,7 @@ import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
import { getCameraID } from '../utils/camera.js';
import { localize } from '../localize/localize.js';
import { CameraInitializationError } from './error.js';
import { CameraManagerStore } from './store.js';
import { CameraManagerReadOnlyConfigStore, CameraManagerStore } from './store.js';
import cloneDeep from 'lodash-es/cloneDeep';
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
@@ -126,11 +130,15 @@ export class CameraManager {
const output: Map<CameraConfig, CameraManagerEngine> = new Map();
const engines: Map<Engine, CameraManagerEngine> = new Map();
for (const cameraConfig of camerasConfig) {
const engineType = await this._engineFactory.getEngineForCamera(
hass,
cameraConfig,
const getEngineTypes = async (configs: CameraConfig[]) => {
return await allPromises(configs, (config) =>
this._engineFactory.getEngineForCamera(hass, config),
);
};
const engineTypes = await getEngineTypes(camerasConfig);
for (const [index, cameraConfig] of camerasConfig.entries()) {
const engineType = engineTypes[index];
const engine = engineType
? engines.get(engineType) ?? this._engineFactory.createEngine(engineType)
: null;
@@ -218,8 +226,8 @@ export class CameraManager {
this._store.addCamera(id, result.initializedConfig, result.engine);
});
if (!this._store.getCameraCount()) {
throw new CameraInitializationError(localize('error.no_cameras'));
if (!this._store.getVisibleCameraCount()) {
throw new CameraInitializationError(localize('error.no_visible_cameras'));
}
}
@@ -227,22 +235,8 @@ export class CameraManager {
return this._store.getCameraCount() > 0;
}
public getCameras(): Map<string, CameraConfig> | null {
return this._store.getCameras();
}
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 getStore(): CameraManagerReadOnlyConfigStore {
return this._store;
}
public generateDefaultEventQueries(
@@ -287,7 +281,7 @@ export class CameraManager {
const results = await this._handleQuery(hass, query);
for (const [query, result] of results?.entries() ?? []) {
for (const result of results?.values() ?? []) {
if (result.metadata.what) {
result.metadata.what.forEach(what.add, what);
}
@@ -325,19 +319,19 @@ export class CameraManager {
let queries: DataQuery[] | null = null;
if (QueryClassifier.isEventQuery(partialQuery)) {
queries = engine.generateDefaultEventQuery(
this._store.getCameras(),
this._store.getVisibleCameras(),
cameraIDs,
partialQuery,
);
} else if (QueryClassifier.isRecordingQuery(partialQuery)) {
queries = engine.generateDefaultRecordingQuery(
this._store.getCameras(),
this._store.getVisibleCameras(),
cameraIDs,
partialQuery,
);
} else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
queries = engine.generateDefaultRecordingSegmentsQuery(
this._store.getCameras(),
this._store.getVisibleCameras(),
cameraIDs,
partialQuery,
);
+43 -10
View File
@@ -5,8 +5,26 @@ import { CameraConfigs, Engine } from './types';
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
export class CameraManagerStore {
protected _configs: Map<string, CameraConfig> = new Map();
export interface CameraManagerReadOnlyConfigStore {
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 _enginesByType: Map<Engine, CameraManagerEngine> = new Map();
@@ -15,29 +33,44 @@ export class CameraManagerStore {
cameraConfig: CameraConfig,
engine: CameraManagerEngine,
): 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._enginesByType.set(engine.getEngineType(), engine);
}
public getCameraCount(): number {
return this._configs.size;
public getCameraConfig(cameraID: string): CameraConfig | null {
return this._allConfigs.get(cameraID) ?? null;
}
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 {
return this._configs.get(cameraID) ?? null;
public getCameraCount(): number {
return this._allConfigs.size;
}
public getVisibleCameraCount(): number {
return this._visibleConfigs.size;
}
public getCameras(): CameraConfigs {
return this._configs;
return this._allConfigs;
}
public getVisibleCameras(): CameraConfigs {
return this._visibleConfigs;
}
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 {
+87 -18
View File
@@ -384,13 +384,14 @@ class FrigateCard extends LitElement {
protected _getMenuButtons(): MenuButton[] {
const buttons: MenuButton[] = [];
const cameras = this._cameraManager?.getCameras();
const visibleCameras = this._cameraManager?.getStore().getVisibleCameras();
const selectedCameraID = this._view?.camera;
const selectedCameraConfig = this._getSelectedCameraConfig();
const allSelectedCameraIDs =
cameras && selectedCameraID
? getAllDependentCameras(cameras, selectedCameraID)
: null;
const allSelectedCameraIDs = getAllDependentCameras(
this._cameraManager,
selectedCameraID,
);
const cameraCapabilities = allSelectedCameraIDs
? this._cameraManager?.getAggregateCameraCapabilities(allSelectedCameraIDs)
: null;
@@ -410,8 +411,8 @@ class FrigateCard extends LitElement {
) as FrigateCardCustomAction,
});
if (cameras) {
const menuItems = Array.from(cameras, ([cameraID, config]) => {
if (visibleCameras) {
const menuItems = Array.from(visibleCameras, ([cameraID, config]) => {
const action = createFrigateCardCustomAction('camera_select', {
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({
icon: 'mdi:cctv',
...this._getConfig().menu.buttons.live,
@@ -634,7 +690,7 @@ class FrigateCard extends LitElement {
if (!this._view || !this._cameraManager) {
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 {
log(this._cardWideConfig, `Frigate Card view change: `, args?.view ?? '[default]');
const changeView = (view: View): void => {
if (View.isMediaChange(this._view, view)) {
if (View.isMajorMediaChange(this._view, view)) {
this._currentMediaLoadedInfo = null;
}
if (this._view?.view !== view.view) {
@@ -785,7 +841,7 @@ class FrigateCard extends LitElement {
// Load the default view.
let cameraID: string | null = null;
if (this._cameraManager) {
const cameras = this._cameraManager.getCameras();
const cameras = this._cameraManager.getStore().getVisibleCameras();
if (cameras) {
if (this._view?.camera && this._getConfig().view.update_cycle_camera) {
const keys = Array.from(cameras.keys());
@@ -1007,7 +1063,7 @@ class FrigateCard extends LitElement {
let changedCamera = false;
let triggerChanges = false;
const cameras = this._cameraManager?.getCameras();
const cameras = this._cameraManager?.getStore().getVisibleCameras();
for (const [cameraID, config] of cameras?.entries() ?? []) {
const triggerEntities = config.triggers.entities ?? [];
const diffs = getHassDifferences(this._hass, oldHass, triggerEntities, {
@@ -1307,7 +1363,7 @@ class FrigateCard extends LitElement {
*/
protected _cardActionHandler(ev: CustomEvent<ActionType>): void {
const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail);
if (!frigateCardAction) {
if (!this._view || !frigateCardAction) {
return;
}
const action = frigateCardAction.frigate_card_action;
@@ -1325,14 +1381,12 @@ class FrigateCard extends LitElement {
case 'snapshot':
case 'snapshots':
case 'timeline':
if (this._view) {
this._changeView({
view: new View({
view: action,
camera: this._view.camera,
}),
});
}
break;
case 'download':
this._downloadViewerMedia();
@@ -1355,17 +1409,32 @@ class FrigateCard extends LitElement {
this._refMenu.value?.toggleMenu();
break;
case 'camera_select':
const cameraID = frigateCardAction.camera;
if (this._cameraManager?.hasCameraID(cameraID) && this._view) {
const selectCameraID = frigateCardAction.camera;
if (
this._view &&
this._cameraManager?.getStore().hasVisibleCameraID(selectCameraID)
) {
const viewOnCameraSelect = this._getConfig().view.camera_select;
const targetView =
viewOnCameraSelect === 'current' ? this._view.view : viewOnCameraSelect;
const actualView = this.isViewSupportedByCamera(cameraID, targetView)
const actualView = this.isViewSupportedByCamera(selectCameraID, targetView)
? targetView
: FRIGATE_CARD_VIEW_DEFAULT;
this._changeView({ view: new View({ view: actualView, camera: cameraID }) });
this._changeView({
view: new View({ view: actualView, camera: selectCameraID }),
});
}
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':
this._mediaPlayerAction(
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 { 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
* providers.
@@ -339,7 +351,7 @@ export class FrigateCardLiveCarousel extends LitElement {
}
protected _getSelectedCameraIndex(): number {
const cameraIDs = this.cameraManager?.getCameraIDs();
const cameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
if (!cameraIDs || !this.view) {
return 0;
}
@@ -362,7 +374,7 @@ export class FrigateCardLiveCarousel extends LitElement {
* @returns A list of EmblaOptionsTypes.
*/
protected _getPlugins(): EmblaCarouselPlugins {
const cameras = this.cameraManager?.getCameraIDs();
const cameras = this.cameraManager?.getStore().getVisibleCameraIDs();
return [
// Only enable wheel plugin if there is more than one camera.
...(cameras && cameras.size > 1
@@ -420,18 +432,27 @@ export class FrigateCardLiveCarousel extends LitElement {
* name to slide number.
*/
protected _getSlides(): [TemplateResult[], Record<string, number>] {
const cameras = this.cameraManager?.getCameras();
if (!cameras) {
const visibleCameras = this.cameraManager?.getStore().getVisibleCameras();
if (!visibleCameras) {
return [[], {}];
}
const slides: TemplateResult[] = [];
const cameraToSlide: Record<string, number> = {};
for (const [camera, cameraConfig] of cameras) {
const slide = this._renderLive(camera, cameraConfig, slides.length);
for (const [cameraID, cameraConfig] of visibleCameras) {
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) {
cameraToSlide[camera] = slides.length;
cameraToSlide[cameraID] = slides.length;
slides.push(slide);
}
}
@@ -442,7 +463,7 @@ export class FrigateCardLiveCarousel extends LitElement {
* Handle the user selecting a new slide in the carousel.
*/
protected _setViewHandler(ev: CustomEvent<CarouselSelect>): void {
const cameras = this.cameraManager?.getCameras();
const cameras = this.cameraManager?.getStore().getVisibleCameras();
if (cameras && ev.detail.index !== this._getSelectedCameraIndex()) {
this._setViewCameraID(Array.from(cameras.keys())[ev.detail.index]);
}
@@ -515,7 +536,7 @@ export class FrigateCardLiveCarousel extends LitElement {
<frigate-card-live-provider
?disabled=${this.liveConfig.lazy_load}
.cameraConfig=${cameraConfig}
.cameraEndpoints=${guard([this.cameraManager], () =>
.cameraEndpoints=${guard([this.cameraManager, cameraID], () =>
this.cameraManager?.getCameraEndpoints(cameraID),
)}
.label=${cameraMetadata?.title ?? ''}
@@ -535,7 +556,7 @@ export class FrigateCardLiveCarousel extends LitElement {
}
protected _getCameraIDsOfNeighbors(): [string | null, string | null] {
const cameras = this.cameraManager?.getCameras();
const cameras = this.cameraManager?.getStore().getVisibleCameras();
if (!cameras || !this.view || !this.hass) {
return [null, null];
}
@@ -557,15 +578,13 @@ export class FrigateCardLiveCarousel extends LitElement {
* @returns A template to display to the user.
*/
protected render(): TemplateResult | void {
if (!this.liveConfig || !this.view || !this.hass || !this.cameraManager) {
return;
}
const [slides, cameraToSlide] = this._getSlides();
this._cameraToSlide = cameraToSlide;
if (
!slides.length ||
!this.liveConfig ||
!this.view ||
!this.hass ||
!this.cameraManager
) {
if (!slides.length) {
return;
}
@@ -577,15 +596,19 @@ export class FrigateCardLiveCarousel extends LitElement {
const [prevID, nextID] = this._getCameraIDsOfNeighbors();
const overrideCameraID = (cameraID: string): string => {
return this.view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
};
const cameraMetadataPrevious = prevID
? this.cameraManager.getCameraMetadata(this.hass, prevID)
? this.cameraManager.getCameraMetadata(this.hass, overrideCameraID(prevID))
: null;
const cameraMetadataCurrent = this.cameraManager.getCameraMetadata(
this.hass,
this.view.camera,
overrideCameraID(this.view.camera),
);
const cameraMetadataNext = nextID
? this.cameraManager.getCameraMetadata(this.hass, nextID)
? this.cameraManager.getCameraMetadata(this.hass, overrideCameraID(nextID))
: null;
// 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 { CameraManager } from '../camera-manager/manager';
import { HomeAssistant } from 'custom-card-helpers';
import {
MediaMetadata,
QueryType,
} from '../camera-manager/types';
import { DataQuery, MediaMetadata, QueryType } from '../camera-manager/types';
import format from 'date-fns/format';
import endOfMonth from 'date-fns/endOfMonth';
import isEqual from 'lodash-es/isEqual';
@@ -168,7 +165,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
_ev: CustomEvent<{ value: unknown }>,
): Promise<void> {
const cameras = this.cameraManager?.getCameras();
const cameras = this.cameraManager?.getStore().getVisibleCameras();
if (!this.hass || !cameras || !this.cameraManager || !this.view) {
return;
}
@@ -269,7 +266,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
protected willUpdate(changedProps: PropertyValues): void {
if (changedProps.has('cameraManager')) {
const cameras = this.cameraManager?.getCameras();
const cameras = this.cameraManager?.getStore().getVisibleCameras();
if (cameras) {
this._cameraOptions = Array.from(cameras.keys()).map((cameraID) => ({
value: cameraID,
@@ -321,7 +318,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
protected _getDefaultsFromView(): MediaFilterCoreDefaults | null {
const queries = this.view?.query?.getQueries();
const cameras = this.cameraManager?.getCameras();
const cameras = this.cameraManager?.getStore().getVisibleCameras();
if (!this.view || !queries || !cameras) {
return null;
}
@@ -333,12 +330,12 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
let favorite: MediaFilterCoreFavoriteSelection | undefined;
const cameraIDSets = uniqWith(
queries.map((query) => query.cameraIDs),
queries.map((query: DataQuery) => query.cameraIDs),
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.
if (cameraIDSets.length === 1 && queries[0].cameraIDs.size !== cameras.size) {
if (cameraIDSets.length === 1 && isEqual(queries[0].cameraIDs, cameras)) {
cameraIDs = [...queries[0].cameraIDs];
}
+8 -5
View File
@@ -22,6 +22,7 @@ import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
import './surround-basic.js';
import { changeViewToRecentEventsForCameraAndDependents } from '../utils/media-to-view';
import { getAllDependentCameras } from '../utils/camera.js';
import type { DataQuery } from '../camera-manager/types';
interface ThumbnailViewContext {
// Whether or not to fetch thumbnails.
@@ -120,7 +121,7 @@ export class FrigateCardSurround extends LitElement {
// user is scrubbing video).
if (
changedProperties.has('view') &&
View.isMediaChange(changedProperties.get('view'), this.view)
View.isMajorMediaChange(changedProperties.get('view'), this.view)
) {
this._cameraIDsForTimeline = this._getCameraIDsForTimeline() ?? undefined;
}
@@ -138,18 +139,20 @@ export class FrigateCardSurround extends LitElement {
}
protected _getCameraIDsForTimeline(): Set<string> | null {
const cameras = this.cameraManager?.getCameras();
if (!this.view || !cameras) {
if (!this.view) {
return null;
}
if (this.view?.is('live')) {
return getAllDependentCameras(cameras, this.view.camera);
return getAllDependentCameras(
this.cameraManager,
this.view.camera,
);
}
if (this.view.isViewerView()) {
return new Set(
this.view.query
?.getQueries()
?.map((query) => [...query.cameraIDs])
?.map((query: DataQuery) => [...query.cameraIDs])
.flat(),
);
}
+5 -15
View File
@@ -238,7 +238,7 @@ export class FrigateCardTimelineCore extends LitElement {
const item = request.detail.item;
const media = this._timelineSource?.dataset.get(item)?.media;
const cameraConfig = media
? this.cameraManager?.getCameraConfig(media.getCameraID()) ?? undefined
? this.cameraManager?.getStore().getCameraConfigForMedia(media) ?? undefined
: undefined;
request.detail.hass = this.hass;
@@ -291,15 +291,9 @@ export class FrigateCardTimelineCore extends LitElement {
* @returns A set of camera ids (may be empty).
*/
protected _getTimelineCameraIDs(): Set<string> | null {
return this.cameraIDs ?? this._getAllCameraIDs();
}
/**
* 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;
return (
this.cameraIDs ?? this.cameraManager?.getStore().getVisibleCameraIDs() ?? null
);
}
/**
@@ -1042,11 +1036,7 @@ export class FrigateCardTimelineCore extends LitElement {
changedProps.has('cameraIDs')
) {
const cameraIDs = this._getTimelineCameraIDs();
if (
cameraIDs &&
this.cameraManager &&
this.timelineConfig
) {
if (cameraIDs && this.cameraManager && this.timelineConfig) {
this._timelineSource = new TimelineDataSource(
this.cameraManager,
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_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_HIDE = `${CONF_CAMERAS}.#.hide` 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_ICON = `${CONF_CAMERAS}.#.icon` as const;
+9
View File
@@ -130,6 +130,7 @@ import {
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
MEDIA_CHUNK_SIZE_MAX,
CONF_CAMERAS_ARRAY_GO2RTC_MODES,
CONF_CAMERAS_ARRAY_HIDE,
} from './const.js';
import { localize } from './localize/localize.js';
import frigate_card_editor_style from './scss/editor.scss';
@@ -1341,6 +1342,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
${this._renderStringInput(
getArrayConfigPath(CONF_CAMERAS_ARRAY_ID, cameraIndex),
)}
${this._renderSwitch(
getArrayConfigPath(
CONF_CAMERAS_ARRAY_HIDE,
cameraIndex,
),
this._defaults.cameras.hide,
)}
${this._putInSubmenu(
MENU_CAMERAS_FRIGATE,
cameraIndex,
@@ -1604,6 +1612,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
})}
${this._renderMenuButton('frigate') /* */}
${this._renderMenuButton('cameras') /* */}
${this._renderMenuButton('substreams') /* */}
${this._renderMenuButton('live') /* */}
${this._renderMenuButton('clips') /* */}
${this._renderMenuButton('snapshots')}
+4 -1
View File
@@ -33,6 +33,7 @@
"mjpeg": "Motion JPEG (MJPEG)"
}
},
"hide": "Hide camera from UI",
"icon": "Icon for this camera (Autodetected from entity)",
"id": "Unique id for this camera in this card",
"live_provider": "Live view provider for this camera",
@@ -239,6 +240,7 @@
"media_player": "Send to media player",
"priority": "Priority",
"snapshots": "Snapshots",
"substreams": "Substream(s)",
"timeline": "Timeline",
"recordings": "Recordings"
},
@@ -368,10 +370,11 @@
"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_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_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_visible_cameras": "No visible cameras found, you must configure at least one non-hidden camera",
"reconnecting": "Reconnecting",
"timeline_no_cameras": "No Frigate cameras to show in timeline",
"troubleshooting": "Check troubleshooting",
+4 -1
View File
@@ -23,6 +23,7 @@
"url": "Frigate URL del server",
"zone": "Frigate zona"
},
"hide": "",
"icon": "Icona per questa telecamera (Autoidentificato dall'entità)",
"id": "ID univoco per questa telecamera in questa carta",
"live_provider": "Provider di visualizzazione dal vivo per questa telecamera",
@@ -223,6 +224,7 @@
"live": "Abitare",
"media_player": "Invia a Media Player",
"priority": "Priorità",
"substreams": "",
"snapshots": "Istantanee",
"timeline": "Timeline"
},
@@ -347,10 +349,11 @@
"live_camera_unavailable": "Telecamera non disponibile",
"no_camera_engine": "",
"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_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_visible_cameras": "",
"reconnecting": "Riconnessione",
"timeline_no_cameras": "Nessuna telecamera damostrare in Frigate nella timeline",
"troubleshooting": "Controllare la risoluzione dei problemi",
+4 -1
View File
@@ -23,6 +23,7 @@
"url": "URL do servidor Frigate",
"zone": "Zona do Frigate"
},
"hide": "",
"icon": "Ícone para esta câmera (detectado automaticamente pela entidade)",
"id": "ID exclusivo para esta câmera nesse cartão",
"live_provider": "Provedor de visualização ao vivo para esta câmera",
@@ -223,6 +224,7 @@
"live": "Ao vivo",
"media_player": "Enviar para o reprodutor de mídia",
"priority": "Prioridade",
"substreams": "",
"snapshots": "Instantâneos",
"timeline": "Linha do tempo"
},
@@ -347,10 +349,11 @@
"live_camera_unavailable": "",
"no_camera_engine": "",
"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_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_visible_cameras": "",
"reconnecting": "Reconectando",
"timeline_no_cameras": "Nenhuma câmera do Frigate para mostrar na linha do tempo",
"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
.literal('custom:frigate-card-action')
// 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 = [
...FRIGATE_CARD_GENERAL_ACTIONS,
'camera_select',
'live_substream_select',
'media_player',
] as const;
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),
});
const frigateCardCameraSelectActionSchema = frigateCardCustomactionsBaseSchema.extend({
const frigateCardCameraSelectActionSchema = frigateCardCustomActionsBaseSchema.extend({
frigate_card_action: z.literal('camera_select'),
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'),
media_player: z.string(),
media_player_action: z.enum(['play', 'stop']),
@@ -247,6 +253,7 @@ const frigateCarMediaPlayerActionSchema = frigateCardCustomactionsBaseSchema.ext
export const frigateCardCustomActionSchema = z.union([
frigateCardGeneralActionSchema,
frigateCardCameraSelectActionSchema,
frigateCardLiveDependencySelectActionSchema,
frigateCarMediaPlayerActionSchema,
]);
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
@@ -398,6 +405,7 @@ const cameraConfigDefault = {
all_cameras: false,
cameras: [],
},
hide: false,
triggers: {
motion: false,
occupancy: true,
@@ -418,6 +426,9 @@ const cameraConfigSchema = z
icon: 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
// this card.
id: z.string().optional(),
@@ -944,6 +955,7 @@ const menuConfigDefault = {
buttons: {
frigate: visibleButtonDefault,
cameras: visibleButtonDefault,
substreams: visibleButtonDefault,
live: visibleButtonDefault,
clips: visibleButtonDefault,
snapshots: visibleButtonDefault,
@@ -976,6 +988,7 @@ const menuConfigSchema = z
.object({
frigate: visibleButtonSchema.default(menuConfigDefault.buttons.frigate),
cameras: visibleButtonSchema.default(menuConfigDefault.buttons.cameras),
substreams: visibleButtonSchema.default(menuConfigDefault.buttons.substreams),
live: visibleButtonSchema.default(menuConfigDefault.buttons.live),
clips: visibleButtonSchema.default(menuConfigDefault.buttons.clips),
snapshots: visibleButtonSchema.default(menuConfigDefault.buttons.snapshots),
+3 -3
View File
@@ -2,7 +2,7 @@ import {
ActionConfig,
handleActionConfig,
hasAction,
HomeAssistant
HomeAssistant,
} from 'custom-card-helpers';
import {
Actions,
@@ -10,7 +10,7 @@ import {
ActionType,
FrigateCardAction,
FrigateCardCustomAction,
frigateCardCustomActionSchema
frigateCardCustomActionSchema,
} from '../types.js';
/**
@@ -43,7 +43,7 @@ export function createFrigateCardCustomAction(
media_player_action?: 'play' | 'stop';
},
): FrigateCardCustomAction | null {
if (action === 'camera_select') {
if (action === 'camera_select' || action === 'live_substream_select') {
if (!args?.camera) {
return null;
}
+10 -4
View File
@@ -1,3 +1,4 @@
import { CameraManager } from '../camera-manager/manager.js';
import { CameraConfig, RawFrigateCardConfig } from '../types.js';
/**
@@ -25,14 +26,19 @@ export function getCameraID(
/**
* 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.
* @returns A set of query parameters.
* @returns A set of dependent cameraIDs or null.
*/
export const getAllDependentCameras = (
cameras: Map<string, CameraConfig>,
cameraManager?: CameraManager,
cameraID?: string,
): Set<string> => {
): Set<string> | null => {
if (!cameraManager || !cameraID) {
return null;
}
const cameras = cameraManager.getStore().getCameras();
const cameraIDs: Set<string> = new Set();
const getDependentCameras = (cameraID: string): void => {
const cameraConfig = cameras.get(cameraID);
+1 -1
View File
@@ -53,7 +53,7 @@ export class EntityRegistryManager {
public async getExtendedEntity(
hass: HomeAssistant,
entityID: string,
): Promise<ExtendedEntity | null> {
): Promise<ExtendedEntity> {
const cachedValue = this._extendedCache.get(entityID);
if (cachedValue) {
return cachedValue;
+5 -7
View File
@@ -27,11 +27,11 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
targetView?: FrigateCardView;
},
): Promise<void> => {
const cameras = cameraManager.getCameras();
if (!cameras) {
const cameraIDs = getAllDependentCameras(cameraManager, view.camera);
if (!cameraIDs) {
return;
}
const cameraIDs = new Set(getAllDependentCameras(cameras, view.camera));
const queries = createQueriesForEventsView(cameraManager, cardWideConfig, cameraIDs, {
mediaType: options?.mediaType,
});
@@ -83,18 +83,16 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
targetView?: 'recording' | 'recordings';
},
): Promise<void> => {
const cameras = cameraManager.getCameras();
if (!cameras) {
const cameraIDs = getAllDependentCameras(cameraManager, view.camera);
if (!cameraIDs) {
return;
}
const cameraIDs = new Set(getAllDependentCameras(cameras, view.camera));
const queries = createQueriesForRecordingsView(
cameraManager,
cardWideConfig,
cameraIDs,
);
if (!queries) {
return;
}
+6 -1
View File
@@ -39,12 +39,17 @@ export class View {
* @param curr The current view.
* @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 (
!prev ||
!curr ||
prev.view !== curr.view ||
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
// happened in the past -- not reflective of the actual live media viewer
// the user is seeing.