Refactor camera initialization into the camera engines.

This commit is contained in:
Dermot Duffy
2023-02-12 16:10:22 -08:00
parent 587f483f40
commit 06b4cc914c
30 changed files with 1249 additions and 842 deletions
+150 -351
View File
@@ -11,7 +11,6 @@ import { customElement, property, state } from 'lit/decorators.js';
import { classMap } from 'lit/directives/class-map.js';
import { createRef, ref, Ref } from 'lit/directives/ref.js';
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
import { until } from 'lit/directives/until.js';
import throttle from 'lodash-es/throttle';
import screenfull from 'screenfull';
import { z } from 'zod';
@@ -33,19 +32,13 @@ import './components/message.js';
import { renderMessage, renderProgressIndicator } from './components/message.js';
import './components/thumbnail-carousel.js';
import { isConfigUpgradeable } from './config-mgmt.js';
import {
CAMERA_BIRDSEYE,
MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA,
REPO_URL,
} from './const.js';
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA, REPO_URL } from './const.js';
import { getLanguage, loadLanguages, localize } from './localize/localize.js';
import cardStyle from './scss/card.scss';
import {
Actions,
ActionType,
CameraConfig,
EntityList,
ExtendedEntity,
ExtendedHomeAssistant,
FrigateCardConfig,
frigateCardConfigSchema,
@@ -58,6 +51,8 @@ import {
Message,
RawFrigateCardConfig,
CardWideConfig,
FrigateCardError,
FRIGATE_CARD_VIEW_DEFAULT,
} from './types.js';
import {
convertActionToFrigateCardCustomAction,
@@ -67,7 +62,6 @@ import {
getActionConfigGivenAction,
} from './utils/action.js';
import { contentsChanged, errorToConsole } from './utils/basic.js';
import { getCameraID } from './utils/camera.js';
import {
getEntityIcon,
getEntityTitle,
@@ -79,12 +73,6 @@ import {
sideLoadHomeAssistantElements,
} from './utils/ha';
import { DeviceList, getAllDevices } from './utils/ha/device-registry.js';
import {
ExtendedEntityCache,
getAllEntities,
getExtendedEntities,
getExtendedEntity,
} from './utils/ha/entity-registry.js';
import { ResolvedMediaCache } from './utils/ha/resolved-media.js';
import { supportsFeature } from './utils/ha/update.js';
import { isValidMediaLoadedInfo } from './utils/media-info.js';
@@ -95,6 +83,10 @@ import { CameraManager } from './camera-manager/manager.js';
import { setLowPerformanceProfile, setPerformanceCSSStyles } from './performance.js';
import { CameraManagerEngineFactory } from './camera-manager/engine-factory.js';
import { log } from './utils/debug.js';
import { EntityRegistryManager } from './utils/ha/entity-registry/index.js';
import { EntityCache } from './utils/ha/entity-registry/cache.js';
import { Entity, ExtendedEntity } from './utils/ha/entity-registry/types.js';
import { getAllDependentCameras } from './utils/camera.js';
/** A note on media callbacks:
*
@@ -196,9 +188,6 @@ class FrigateCard extends LitElement {
// Array of dynamic menu buttons to be added to menu.
protected _dynamicMenuButtons: MenuButton[] = [];
@state()
protected _cameras?: Map<string, CameraConfig>;
// Error/info message to render.
protected _message: Message | null = null;
@@ -207,6 +196,8 @@ class FrigateCard extends LitElement {
protected _cameraManager?: CameraManager;
protected _entityRegistryManager: EntityRegistryManager;
// The mouse handler may be called continually, throttle it to at most once
// per second for performance reasons.
protected _boundMouseHandler = throttle(this._mouseHandler.bind(this), 1 * 1000);
@@ -219,6 +210,14 @@ class FrigateCard extends LitElement {
protected _conditionManager: CardConditionManager | null = null;
constructor() {
super();
this._entityRegistryManager = new EntityRegistryManager(
new EntityCache<Entity>(),
new EntityCache<ExtendedEntity>(),
);
}
/**
* Set the Home Assistant object.
*/
@@ -383,6 +382,17 @@ class FrigateCard extends LitElement {
protected _getMenuButtons(): MenuButton[] {
const buttons: MenuButton[] = [];
const cameras = this._cameraManager?.getCameras();
const selectedCameraID = this._view?.camera;
const selectedCameraConfig = this._getSelectedCameraConfig();
const allSelectedCameraIDs =
cameras && selectedCameraID
? getAllDependentCameras(cameras, selectedCameraID)
: null;
const cameraCapabilities = allSelectedCameraIDs
? this._cameraManager?.getAggregateCameraCapabilities(allSelectedCameraIDs)
: null;
buttons.push({
// Use a magic icon value that the menu will use to render the custom
// Frigate icon.
@@ -398,8 +408,8 @@ class FrigateCard extends LitElement {
) as FrigateCardCustomAction,
});
if (this._cameras && this._cameras.size > 1) {
const menuItems = Array.from(this._cameras, ([cameraID, config]) => {
if (cameras) {
const menuItems = Array.from(cameras, ([cameraID, config]) => {
const action = createFrigateCardCustomAction('camera_select', {
camera: cameraID,
});
@@ -436,16 +446,7 @@ class FrigateCard extends LitElement {
tap_action: createFrigateCardCustomAction('live') as FrigateCardCustomAction,
});
const cameraConfig = this._getSelectedCameraConfig();
// Don't show `clips` button if there's no `camera_name` (e.g. non-Frigate
// cameras), or is birdseye (unless there are dependent cameras).
if (
cameraConfig?.frigate.camera_name &&
(cameraConfig?.frigate.camera_name !== CAMERA_BIRDSEYE ||
cameraConfig.dependencies.cameras.length ||
cameraConfig.dependencies.all_cameras)
) {
if (cameraCapabilities?.supportsClips) {
buttons.push({
icon: 'mdi:filmstrip',
...this._getConfig().menu.buttons.clips,
@@ -457,14 +458,7 @@ class FrigateCard extends LitElement {
});
}
// Don't show `snapshots` button if there's no `camera_name` (e.g. non-Frigate
// cameras), or is birdseye (unless there are dependent cameras).
if (
cameraConfig?.frigate.camera_name &&
(cameraConfig?.frigate.camera_name !== CAMERA_BIRDSEYE ||
cameraConfig?.dependencies.cameras.length ||
cameraConfig?.dependencies.all_cameras)
) {
if (cameraCapabilities?.supportsSnapshots) {
buttons.push({
icon: 'mdi:camera',
...this._getConfig().menu.buttons.snapshots,
@@ -480,14 +474,7 @@ class FrigateCard extends LitElement {
});
}
// Don't show `recordings` button if there's no `camera_name` (e.g. non-Frigate
// cameras), or is birdseye (unless there are dependent cameras).
if (
cameraConfig?.frigate.camera_name &&
(cameraConfig?.frigate.camera_name !== CAMERA_BIRDSEYE ||
cameraConfig?.dependencies.cameras.length ||
cameraConfig?.dependencies.all_cameras)
) {
if (cameraCapabilities?.supportsRecordings) {
buttons.push({
icon: 'mdi:album',
...this._getConfig().menu.buttons.recordings,
@@ -514,13 +501,7 @@ class FrigateCard extends LitElement {
// Don't show the timeline button unless there's at least one non-birdseye
// camera with a Frigate camera name.
if (
this._cameras &&
[...this._cameras.values()].some(
(config) =>
config.frigate.camera_name && config.frigate.camera_name !== CAMERA_BIRDSEYE,
)
) {
if (cameraCapabilities?.supportsTimeline) {
buttons.push({
icon: 'mdi:chart-gantt',
...this._getConfig().menu.buttons.timeline,
@@ -545,7 +526,7 @@ class FrigateCard extends LitElement {
});
}
if (cameraConfig?.frigate.url) {
if (this._getCameraURLFromContext()) {
buttons.push({
icon: 'mdi:web',
...this._getConfig().menu.buttons.frigate_ui,
@@ -590,7 +571,7 @@ class FrigateCard extends LitElement {
if (
mediaPlayers.length &&
(this._view?.isViewerView() ||
(this._view?.is('live') && cameraConfig?.camera_entity))
(this._view?.is('live') && selectedCameraConfig?.camera_entity))
) {
const mediaPlayerItems = mediaPlayers.map((playerEntityID) => {
const title = getEntityTitle(this._hass, playerEntityID) || playerEntityID;
@@ -660,237 +641,15 @@ class FrigateCard extends LitElement {
}
}
/**
* Get the motion sensor entity for a given camera.
* @param cache The ExtendedEntityCache of entity registry information.
* @param cameraConfig The camera config in question.
* @returns The entity id of the motion sensor or null.
*/
protected _getMotionSensor(
cache: ExtendedEntityCache,
cameraConfig: CameraConfig,
): string | null {
if (cameraConfig.frigate.camera_name) {
return (
cache.getMatch(
(ent) =>
!!ent.unique_id?.match(
new RegExp(
`:motion_sensor:${
cameraConfig.frigate.zone || cameraConfig.frigate.camera_name
}`,
),
),
)?.entity_id ?? null
);
}
return null;
}
/**
* Get the occupancy sensor entity for a given camera.
* @param cache The ExtendedEntityCache of entity registry information.
* @param cameraConfig The camera config in question.
* @returns The entity id of the occupancy sensor or null.
*/
protected _getOccupancySensor(
cache: ExtendedEntityCache,
cameraConfig: CameraConfig,
): string | null {
if (cameraConfig.frigate.camera_name) {
return (
cache.getMatch(
(ent) =>
!!ent.unique_id?.match(
new RegExp(
`:occupancy_sensor:${
cameraConfig.frigate.zone || cameraConfig.frigate.camera_name
}_${cameraConfig.frigate.label || 'all'}`,
),
),
)?.entity_id ?? null
);
}
return null;
}
/**
* Fully load the configured cameras.
*/
protected async _loadCameras(): Promise<void> {
if (!this._hass) {
return;
}
const hasAutoTriggers = (config: CameraConfig): boolean => {
return config.triggers.motion || config.triggers.occupancy;
};
const hasAnyTriggers = (config: CameraConfig): boolean => {
return hasAutoTriggers(config) || !!config.triggers.entities.length;
};
const hasCameraName = (config: CameraConfig): boolean => {
return !!config.frigate?.camera_name;
};
// Loading cameras may require a number of calls to Home Assistant.
//
// - getAllEntities: Required if any camera has auto-triggers (motion or
// occupancy sensors).
// - getExtendedEntity: Per camera entity to autodetect the Frigate camera
// name or to compute auto-triggers (motion or occupancy sensors).
// - getExtendedEntities: Per binary sensor associated with a Frigate config
// entry, to compute auto-triggers (motion or occupancy sensors).
//
// For loading performance these are only called when absolutely needed.
let entityList: EntityList | undefined;
const cache = new ExtendedEntityCache();
const loadedCameras: CameraConfig[] = [];
const addCameraConfig = async (config: CameraConfig, index: number) => {
if (!this._hass) {
return;
}
let entity: ExtendedEntity | null = null;
if (config.camera_entity && (hasAnyTriggers(config) || !hasCameraName(config))) {
try {
entity = await getExtendedEntity(this._hass, config.camera_entity, cache);
} catch (e) {
// Silently ignore errors here, as non-Frigate camera entities may not
// necessarily have a registry entry and otherwise this would cause
// log spam for those cases.
}
}
if (entity && !hasCameraName(config)) {
const resolvedName = this._getFrigateCameraNameFromEntity(entity);
if (resolvedName) {
config.frigate.camera_name = resolvedName;
}
}
if (entity && entityList && hasAnyTriggers(config)) {
if (hasAutoTriggers(config)) {
// Try to find the correct entities for the motion & occupancy sensors.
// We know they are binary_sensors, and that they'll have the same
// config entry ID as the camera. Searching via unique_id ensures this
// search still works if the user renames the entity_id.
const binarySensorEntities = entityList.filter(
(ent) =>
ent.config_entry_id === entity?.config_entry_id &&
!ent.disabled_by &&
ent.entity_id.startsWith('binary_sensor.'),
);
try {
await getExtendedEntities(
this._hass,
binarySensorEntities.map((ent) => ent.entity_id),
cache,
);
} catch (e) {
errorToConsole(e as Error);
}
if (config.triggers.motion) {
const motionEntity = this._getMotionSensor(cache, config);
if (motionEntity) {
config.triggers.entities.push(motionEntity);
}
}
if (config.triggers.occupancy) {
const occupancyEntity = this._getOccupancySensor(cache, config);
if (occupancyEntity) {
config.triggers.entities.push(occupancyEntity);
}
}
}
config.triggers.entities = [...new Set(config.triggers.entities)];
}
loadedCameras[index] = config;
};
let errorFree = true;
const cameras: Map<string, CameraConfig> = new Map();
const configCameras = this._getConfig().cameras;
if (configCameras && Array.isArray(configCameras)) {
if (configCameras.some((config) => hasAutoTriggers(config))) {
try {
entityList = await getAllEntities(this._hass);
} catch (e) {
errorToConsole(e as Error);
}
}
// Load all cameras in parallel, but remember the order they were provided
// (they must be added to the cameraMap in this same order).
await Promise.all(
configCameras.map((configCamera, index) => addCameraConfig(configCamera, index)),
);
loadedCameras.forEach((loadedCamera: CameraConfig) => {
const id = getCameraID(loadedCamera);
if (!id) {
this._setMessageAndUpdate({
message: localize('error.no_camera_id'),
type: 'error',
context: loadedCamera,
});
errorFree = false;
} else if (cameras.has(id)) {
this._setMessageAndUpdate({
message: localize('error.duplicate_camera_id'),
type: 'error',
context: loadedCamera,
});
errorFree = false;
} else {
cameras.set(id, loadedCamera);
}
});
}
if (!cameras.size) {
return this._setMessageAndUpdate({
message: localize('error.no_cameras'),
type: 'error',
});
errorFree = false;
}
if (errorFree) {
this._cameras = cameras;
}
}
/**
* Get the camera configuration for the selected camera.
* @returns The CameraConfig object or null if not found.
*/
protected _getSelectedCameraConfig(): CameraConfig | null {
if (!this._cameras || !this._cameras.size || !this._view?.camera) {
if (!this._view || !this._cameraManager) {
return null;
}
return this._cameras.get(this._view.camera) || null;
}
/**
* Get the Frigate camera name from an entity.
* @returns The Frigate camera name or null if unavailable.
*/
protected _getFrigateCameraNameFromEntity(entity: ExtendedEntity): string | null {
if (entity.unique_id && entity.platform === 'frigate') {
const match = entity.unique_id.match(/:camera:(?<camera>[^:]+)$/);
if (match && match.groups) {
return match.groups['camera'];
}
}
return null;
return this._cameraManager.getCameraConfig(this._view.camera);
}
/**
@@ -997,7 +756,7 @@ class FrigateCard extends LitElement {
};
this._overriddenConfig = undefined;
this._cameras = undefined;
this._cameraManager = undefined;
this._view = undefined;
this._message = null;
@@ -1039,24 +798,27 @@ class FrigateCard extends LitElement {
if (!args?.view) {
// Load the default view.
let camera;
if (this._cameras?.size) {
if (this._view?.camera && this._getConfig().view.update_cycle_camera) {
const keys = Array.from(this._cameras.keys());
const currentIndex = keys.indexOf(this._view.camera);
const targetIndex = currentIndex + 1 >= keys.length ? 0 : currentIndex + 1;
camera = keys[targetIndex];
} else {
// Reset to the default camera.
camera = this._cameras.keys().next().value;
let cameraID: string | null = null;
if (this._cameraManager) {
const cameras = this._cameraManager.getCameras();
if (cameras) {
if (this._view?.camera && this._getConfig().view.update_cycle_camera) {
const keys = Array.from(cameras.keys());
const currentIndex = keys.indexOf(this._view.camera);
const targetIndex = currentIndex + 1 >= keys.length ? 0 : currentIndex + 1;
cameraID = keys[targetIndex];
} else {
// Reset to the default camera.
cameraID = cameras.keys().next().value;
}
}
}
if (camera) {
if (cameraID) {
changeView(
new View({
view: this._getConfig().view.default,
camera: camera,
camera: cameraID,
}),
);
@@ -1103,22 +865,53 @@ class FrigateCard extends LitElement {
});
}
protected async _initializeCameras(): Promise<void> {
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() : ''}
<div ${ref(this._refMain)} class="${classMap(mainClasses)}">
${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) : ''
}
</div>
@@ -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` <frigate-card-gallery
.hass=${this._hass}
.view=${this._view}
.cameras=${this._cameras}
.galleryConfig=${this._getConfig().event_gallery}
.cameraManager=${this._cameraManager}
.cardWideConfig=${this._cardWideConfig}
@@ -2048,7 +1850,6 @@ class FrigateCard extends LitElement {
? html` <frigate-card-viewer
.hass=${this._hass}
.view=${this._view}
.cameras=${this._cameras}
.viewerConfig=${this._getConfig().media_viewer}
.resolvedMediaCache=${this._resolvedMediaCache}
.cameraManager=${this._cameraManager}
@@ -2060,7 +1861,6 @@ class FrigateCard extends LitElement {
? html` <frigate-card-timeline
.hass=${this._hass}
.view=${this._view}
.cameras=${this._cameras}
.timelineConfig=${this._getConfig().timeline}
.cameraManager=${this._cameraManager}
>
@@ -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)}"