Merge pull request #936 from dermotduffy/refactor-initialization
Refactor camera initialization logic into the camera engines
This commit is contained in:
@@ -104,6 +104,7 @@ See the [fully expanded cameras configuration example](#config-expanded-cameras)
|
|||||||
| `title` | Autodetected from `camera_entity` if that is specified. | :heavy_multiplication_x: | A friendly name for this camera to use in the card. |
|
| `title` | Autodetected from `camera_entity` if that is specified. | :heavy_multiplication_x: | A friendly name for this camera to use in the card. |
|
||||||
| `icon` | Autodetected from `camera_entity` if that is specified. | :heavy_multiplication_x: | The icon to use for this camera in the camera menu and in the next & previous controls when using the `icon` style. |
|
| `icon` | Autodetected from `camera_entity` if that is specified. | :heavy_multiplication_x: | The icon to use for this camera in the camera menu and in the next & previous controls when using the `icon` style. |
|
||||||
| `id` | `camera_entity`, `webrtc_card.entity` or `frigate.camera_name` if set (in that preference order). | :heavy_multiplication_x: | An optional identifier to use throughout the card configuration to refer unambiguously to this camera. See [camera IDs](#camera-ids). |
|
| `id` | `camera_entity`, `webrtc_card.entity` or `frigate.camera_name` if set (in that preference order). | :heavy_multiplication_x: | An optional identifier to use throughout the card configuration to refer unambiguously to this camera. See [camera IDs](#camera-ids). |
|
||||||
|
| `engine` | `auto` | :heavy_multiplication_x: | Which camera engine to use for this camera. If `auto` the card will attempt to choose the correct engine from the specified options. See [engines](#engines) below for valid options.|
|
||||||
| `frigate` | | :heavy_multiplication_x: | Options for a Frigate camera. See [Frigate configuration](#camera-frigate-configuration) below. |
|
| `frigate` | | :heavy_multiplication_x: | Options for a Frigate camera. See [Frigate configuration](#camera-frigate-configuration) below. |
|
||||||
| `dependencies` | | :heavy_multiplication_x: | Other cameras that this camera should depend upon. See [camera dependencies](#camera-dependencies-configuration) below. |
|
| `dependencies` | | :heavy_multiplication_x: | Other cameras that this camera should depend upon. See [camera dependencies](#camera-dependencies-configuration) below. |
|
||||||
| `triggers` | | :heavy_multiplication_x: | Define what should cause this camera to update/trigger. See [camera triggers](#camera-trigger-configuration) below. |
|
| `triggers` | | :heavy_multiplication_x: | Define what should cause this camera to update/trigger. See [camera triggers](#camera-trigger-configuration) below. |
|
||||||
@@ -122,6 +123,15 @@ See the [fully expanded cameras configuration example](#config-expanded-cameras)
|
|||||||
|`frigate-jsmpeg`|Better|Low|Poor|Builtin|Stream the JSMPEG stream from Frigate (proxied via the Frigate integration). See [note below on the required integration version](#jsmpeg-troubleshooting) for this live provider to function. This is the only live provider that can view the Frigate `birdseye` view.|
|
|`frigate-jsmpeg`|Better|Low|Poor|Builtin|Stream the JSMPEG stream from Frigate (proxied via the Frigate integration). See [note below on the required integration version](#jsmpeg-troubleshooting) for this live provider to function. This is the only live provider that can view the Frigate `birdseye` view.|
|
||||||
|`webrtc-card`|Best|High|Better|Separate installation required|Embed's [AlexxIT's WebRTC Card](https://github.com/AlexxIT/WebRTC) to stream live feed, requires manual extra setup, see [below](#webrtc). Not to be confused with native Home Assistant WebRTC (use `ha` provider above).|
|
|`webrtc-card`|Best|High|Better|Separate installation required|Embed's [AlexxIT's WebRTC Card](https://github.com/AlexxIT/WebRTC) to stream live feed, requires manual extra setup, see [below](#webrtc). Not to be confused with native Home Assistant WebRTC (use `ha` provider above).|
|
||||||
|
|
||||||
|
<a name="engines"></a>
|
||||||
|
|
||||||
|
#### Available Camera Engines
|
||||||
|
|
||||||
|
|Engine|Live|Supports clips|Supports Snapshots|Supports Recordings|Supports Timeline|Favorite events|Favorite recordings|
|
||||||
|
| - | - | - | - | - | - | - | - |
|
||||||
|
|`frigate`| :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :white_check_mark: | :heavy_multiplication_x: |
|
||||||
|
|`generic`| :white_check_mark: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: | :heavy_multiplication_x: |
|
||||||
|
|
||||||
<a name="camera-frigate-configuration"></a>
|
<a name="camera-frigate-configuration"></a>
|
||||||
|
|
||||||
#### Camera Frigate configuration
|
#### Camera Frigate configuration
|
||||||
@@ -1304,6 +1314,7 @@ Reference: [Camera Options](#camera-options).
|
|||||||
cameras:
|
cameras:
|
||||||
- camera_entity: camera.front_Door
|
- camera_entity: camera.front_Door
|
||||||
live_provider: ha
|
live_provider: ha
|
||||||
|
engine: auto
|
||||||
frigate:
|
frigate:
|
||||||
url: http://my.frigate.local
|
url: http://my.frigate.local
|
||||||
client_id: frigate
|
client_id: frigate
|
||||||
@@ -1322,6 +1333,7 @@ cameras:
|
|||||||
- binary_sensor.front_door_sensor
|
- binary_sensor.front_door_sensor
|
||||||
- camera_entity: camera.entrance
|
- camera_entity: camera.entrance
|
||||||
live_provider: webrtc-card
|
live_provider: webrtc-card
|
||||||
|
engine: auto
|
||||||
frigate:
|
frigate:
|
||||||
url: http://my-other.frigate.local
|
url: http://my-other.frigate.local
|
||||||
client_id: frigate-other
|
client_id: frigate-other
|
||||||
|
|||||||
@@ -1,27 +1,30 @@
|
|||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { CameraConfig, CardWideConfig } from '../types';
|
import { CameraConfig, CardWideConfig } from '../types';
|
||||||
import { ViewMedia } from '../view/media';
|
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||||
import { RecordingSegmentsCache, RequestCache } from './cache';
|
import { RecordingSegmentsCache, RequestCache } from './cache';
|
||||||
import { CameraManagerEngine } from './engine';
|
import { CameraManagerEngine } from './engine';
|
||||||
import { FrigateCameraManagerEngine } from './frigate/engine-frigate';
|
import { FrigateCameraManagerEngine } from './frigate/engine-frigate';
|
||||||
|
import { GenericCameraManagerEngine } from './generic/engine-generic';
|
||||||
import { Engine } from './types';
|
import { Engine } from './types';
|
||||||
|
|
||||||
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
|
|
||||||
|
|
||||||
export class CameraManagerEngineFactory {
|
export class CameraManagerEngineFactory {
|
||||||
protected _engines: Map<Engine, CameraManagerEngine> = new Map();
|
protected _entityRegistryManager: EntityRegistryManager;
|
||||||
protected _cardWideConfig: CardWideConfig;
|
protected _cardWideConfig: CardWideConfig;
|
||||||
|
|
||||||
constructor(cardWideConfig: CardWideConfig) {
|
constructor(
|
||||||
|
entityRegistryManager: EntityRegistryManager,
|
||||||
|
cardWideConfig: CardWideConfig,
|
||||||
|
) {
|
||||||
|
this._entityRegistryManager = entityRegistryManager;
|
||||||
this._cardWideConfig = cardWideConfig;
|
this._cardWideConfig = cardWideConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEngine(engine: Engine): CameraManagerEngine | null {
|
public async createEngine(engine: Engine): Promise<CameraManagerEngine | null> {
|
||||||
const cachedEngine = this._engines.get(engine);
|
|
||||||
if (cachedEngine) {
|
|
||||||
return cachedEngine;
|
|
||||||
}
|
|
||||||
let cameraManagerEngine: CameraManagerEngine | null = null;
|
let cameraManagerEngine: CameraManagerEngine | null = null;
|
||||||
switch (engine) {
|
switch (engine) {
|
||||||
|
case Engine.Generic:
|
||||||
|
cameraManagerEngine = new GenericCameraManagerEngine();
|
||||||
|
break;
|
||||||
case Engine.Frigate:
|
case Engine.Frigate:
|
||||||
cameraManagerEngine = new FrigateCameraManagerEngine(
|
cameraManagerEngine = new FrigateCameraManagerEngine(
|
||||||
this._cardWideConfig,
|
this._cardWideConfig,
|
||||||
@@ -30,64 +33,39 @@ export class CameraManagerEngineFactory {
|
|||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (cameraManagerEngine) {
|
|
||||||
this._engines.set(engine, cameraManagerEngine);
|
|
||||||
}
|
|
||||||
return cameraManagerEngine;
|
return cameraManagerEngine;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEngineForCamera(cameraConfig?: CameraConfig): CameraManagerEngine | null {
|
public async getEngineForCamera(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
): Promise<Engine | null> {
|
||||||
if (!cameraConfig) {
|
if (!cameraConfig) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let engine: Engine | null = null;
|
let engine: Engine | null = null;
|
||||||
if (cameraConfig.frigate.camera_name) {
|
if (cameraConfig.engine === 'frigate') {
|
||||||
|
engine = Engine.Frigate;
|
||||||
|
} else if (cameraConfig.engine === 'auto') {
|
||||||
|
const cameraEntity = cameraConfig.camera_entity;
|
||||||
|
|
||||||
|
if (cameraEntity) {
|
||||||
|
const entity = await this._entityRegistryManager.getEntity(hass, cameraEntity);
|
||||||
|
switch (entity?.platform) {
|
||||||
|
case 'frigate':
|
||||||
|
engine = Engine.Frigate;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
engine = Engine.Generic;
|
||||||
|
}
|
||||||
|
} else if (cameraConfig.frigate.camera_name) {
|
||||||
|
// Frigate technically does not need an entity, if the camera name is
|
||||||
|
// manually set the camera is assumed to be Frigate.
|
||||||
engine = Engine.Frigate;
|
engine = Engine.Frigate;
|
||||||
}
|
}
|
||||||
return engine ? this.getEngine(engine) : null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEnginesForCameraIDs(
|
return engine;
|
||||||
cameras: Map<string, CameraConfig>,
|
|
||||||
cameraIDs: Set<string>,
|
|
||||||
): CameraManagerEngineCameraIDMap | null {
|
|
||||||
const output: CameraManagerEngineCameraIDMap = new Map();
|
|
||||||
|
|
||||||
for (const cameraID of cameraIDs) {
|
|
||||||
const cameraConfig = cameras.get(cameraID);
|
|
||||||
if (!cameraConfig) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const engine = this.getEngineForCamera(cameraConfig);
|
|
||||||
if (!engine) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (!output.has(engine)) {
|
|
||||||
output.set(engine, new Set());
|
|
||||||
}
|
|
||||||
output.get(engine)?.add(cameraID);
|
|
||||||
}
|
|
||||||
return output.size ? output : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getEngineForMedia(
|
|
||||||
cameras: Map<string, CameraConfig>,
|
|
||||||
media: ViewMedia,
|
|
||||||
): CameraManagerEngine | null {
|
|
||||||
const cameraID = media.getCameraID();
|
|
||||||
if (!cameraID) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const engines = this.getEnginesForCameraIDs(cameras, new Set([cameraID]));
|
|
||||||
return engines ? ([...engines.keys()][0] ?? null) : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
public getAllEngines(
|
|
||||||
cameras: Map<string, CameraConfig>,
|
|
||||||
): CameraManagerEngine[] | null {
|
|
||||||
const engines = this.getEnginesForCameraIDs(cameras, new Set(cameras.keys()));
|
|
||||||
return engines ? [...engines.keys()] : null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { CameraConfig } from '../types';
|
import { CameraConfig } from '../types';
|
||||||
|
import { EntityRegistryManager } from '../utils/ha/entity-registry';
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
import {
|
import {
|
||||||
DataQuery,
|
DataQuery,
|
||||||
@@ -17,57 +18,68 @@ import {
|
|||||||
CameraManagerCameraCapabilities,
|
CameraManagerCameraCapabilities,
|
||||||
CameraManagerMediaCapabilities,
|
CameraManagerMediaCapabilities,
|
||||||
CameraManagerCameraMetadata,
|
CameraManagerCameraMetadata,
|
||||||
|
CameraURLContext,
|
||||||
|
CameraConfigs,
|
||||||
|
Engine,
|
||||||
} from './types';
|
} from './types';
|
||||||
|
|
||||||
export const CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT = 10000;
|
export const CAMERA_MANAGER_ENGINE_EVENT_LIMIT_DEFAULT = 10000;
|
||||||
|
|
||||||
export interface CameraManagerEngine {
|
export interface CameraManagerEngine {
|
||||||
|
getEngineType(): Engine;
|
||||||
|
|
||||||
|
initializeCamera(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityRegistryManager: EntityRegistryManager,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
): Promise<CameraConfig>;
|
||||||
|
|
||||||
generateDefaultEventQuery(
|
generateDefaultEventQuery(
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
query: PartialEventQuery,
|
query: PartialEventQuery,
|
||||||
): EventQuery[] | null;
|
): EventQuery[] | null;
|
||||||
|
|
||||||
generateDefaultRecordingQuery(
|
generateDefaultRecordingQuery(
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
query: PartialRecordingQuery,
|
query: PartialRecordingQuery,
|
||||||
): RecordingQuery[] | null;
|
): RecordingQuery[] | null;
|
||||||
|
|
||||||
generateDefaultRecordingSegmentsQuery(
|
generateDefaultRecordingSegmentsQuery(
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
query: PartialRecordingSegmentsQuery,
|
query: PartialRecordingSegmentsQuery,
|
||||||
): RecordingSegmentsQuery[] | null;
|
): RecordingSegmentsQuery[] | null;
|
||||||
|
|
||||||
getEvents(
|
getEvents(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
query: EventQuery,
|
query: EventQuery,
|
||||||
): Promise<EventQueryResultsMap | null>;
|
): Promise<EventQueryResultsMap | null>;
|
||||||
|
|
||||||
getRecordings(
|
getRecordings(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
query: RecordingQuery,
|
query: RecordingQuery,
|
||||||
): Promise<RecordingQueryResultsMap | null>;
|
): Promise<RecordingQueryResultsMap | null>;
|
||||||
|
|
||||||
getRecordingSegments(
|
getRecordingSegments(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
query: RecordingSegmentsQuery,
|
query: RecordingSegmentsQuery,
|
||||||
): Promise<RecordingSegmentsQueryResultsMap | null>;
|
): Promise<RecordingSegmentsQueryResultsMap | null>;
|
||||||
|
|
||||||
generateMediaFromEvents(
|
generateMediaFromEvents(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
query: EventQuery,
|
query: EventQuery,
|
||||||
results: QueryReturnType<EventQuery>,
|
results: QueryReturnType<EventQuery>,
|
||||||
): ViewMedia[] | null;
|
): ViewMedia[] | null;
|
||||||
|
|
||||||
generateMediaFromRecordings(
|
generateMediaFromRecordings(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
query: RecordingQuery,
|
query: RecordingQuery,
|
||||||
results: QueryReturnType<RecordingQuery>,
|
results: QueryReturnType<RecordingQuery>,
|
||||||
): ViewMedia[] | null;
|
): ViewMedia[] | null;
|
||||||
@@ -85,14 +97,14 @@ export interface CameraManagerEngine {
|
|||||||
|
|
||||||
getMediaSeekTime(
|
getMediaSeekTime(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
media: ViewMedia,
|
media: ViewMedia,
|
||||||
target: Date,
|
target: Date,
|
||||||
): Promise<number | null>;
|
): Promise<number | null>;
|
||||||
|
|
||||||
getMediaMetadata(
|
getMediaMetadata(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
): Promise<MediaMetadata | null>;
|
): Promise<MediaMetadata | null>;
|
||||||
|
|
||||||
getCameraMetadata(
|
getCameraMetadata(
|
||||||
@@ -105,4 +117,9 @@ export interface CameraManagerEngine {
|
|||||||
): CameraManagerCameraCapabilities | null;
|
): CameraManagerCameraCapabilities | null;
|
||||||
|
|
||||||
getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null;
|
getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null;
|
||||||
|
|
||||||
|
getCameraURL(
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
context?: CameraURLContext,
|
||||||
|
): string | null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { FrigateCardError } from '../types.js';
|
||||||
|
|
||||||
|
export class CameraInitializationError extends FrigateCardError {}
|
||||||
@@ -37,6 +37,8 @@ import {
|
|||||||
RecordingSegment,
|
RecordingSegment,
|
||||||
RecordingSegmentsQuery,
|
RecordingSegmentsQuery,
|
||||||
RecordingSegmentsQueryResultsMap,
|
RecordingSegmentsQueryResultsMap,
|
||||||
|
CameraURLContext,
|
||||||
|
CameraConfigs,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { FrigateRecording } from './types';
|
import { FrigateRecording } from './types';
|
||||||
import {
|
import {
|
||||||
@@ -62,7 +64,14 @@ import { FrigateViewMediaClassifier } from './media-classifier';
|
|||||||
import { ViewMediaClassifier } from '../../view/media-classifier';
|
import { ViewMediaClassifier } from '../../view/media-classifier';
|
||||||
import { FrigateViewMediaFactory } from './media';
|
import { FrigateViewMediaFactory } from './media';
|
||||||
import { log } from '../../utils/debug';
|
import { log } from '../../utils/debug';
|
||||||
import { getEntityIcon, getEntityTitle } from '../../utils/ha';
|
import { getEntityTitle } from '../../utils/ha';
|
||||||
|
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
||||||
|
import { ExtendedEntity } from '../../utils/ha/entity-registry/types';
|
||||||
|
import { CameraInitializationError } from '../error';
|
||||||
|
import { localize } from '../../localize/localize';
|
||||||
|
import uniq from 'lodash-es/uniq';
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
import { GenericCameraManagerEngine } from '../generic/engine-generic';
|
||||||
|
|
||||||
const EVENT_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
const EVENT_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
||||||
const RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
const RECORDING_SUMMARY_REQUEST_CACHE_MAX_AGE_SECONDS = 60;
|
||||||
@@ -92,7 +101,10 @@ class FrigateQueryResultsClassifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
export class FrigateCameraManagerEngine
|
||||||
|
extends GenericCameraManagerEngine
|
||||||
|
implements CameraManagerEngine
|
||||||
|
{
|
||||||
protected _recordingSegmentsCache: RecordingSegmentsCache;
|
protected _recordingSegmentsCache: RecordingSegmentsCache;
|
||||||
protected _requestCache: RequestCache;
|
protected _requestCache: RequestCache;
|
||||||
protected _cardWideConfig: CardWideConfig;
|
protected _cardWideConfig: CardWideConfig;
|
||||||
@@ -109,11 +121,162 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
recordingSegmentsCache: RecordingSegmentsCache,
|
recordingSegmentsCache: RecordingSegmentsCache,
|
||||||
requestCache: RequestCache,
|
requestCache: RequestCache,
|
||||||
) {
|
) {
|
||||||
|
super();
|
||||||
this._cardWideConfig = cardWideConfig;
|
this._cardWideConfig = cardWideConfig;
|
||||||
this._recordingSegmentsCache = recordingSegmentsCache;
|
this._recordingSegmentsCache = recordingSegmentsCache;
|
||||||
this._requestCache = requestCache;
|
this._requestCache = requestCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getEngineType(): Engine {
|
||||||
|
return Engine.Frigate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initializeCamera(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityRegistryManager: EntityRegistryManager,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
): Promise<CameraConfig> {
|
||||||
|
const hasCameraName = !!cameraConfig.frigate?.camera_name;
|
||||||
|
const hasAutoTriggers =
|
||||||
|
cameraConfig.triggers.motion || cameraConfig.triggers.occupancy;
|
||||||
|
|
||||||
|
let entity: ExtendedEntity | null = null;
|
||||||
|
|
||||||
|
// Extended entity information is required if the Frigate camera name is
|
||||||
|
// missing, or if the entity requires automatic resolution of
|
||||||
|
// motion/occupancy sensors.
|
||||||
|
if (cameraConfig.camera_entity && (!hasCameraName || hasAutoTriggers)) {
|
||||||
|
try {
|
||||||
|
entity = await entityRegistryManager.getExtendedEntity(
|
||||||
|
hass,
|
||||||
|
cameraConfig.camera_entity,
|
||||||
|
);
|
||||||
|
} catch (e) {
|
||||||
|
throw new CameraInitializationError(
|
||||||
|
localize('error.no_camera_entity'),
|
||||||
|
cameraConfig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entity && !hasCameraName) {
|
||||||
|
const resolvedName = this._getFrigateCameraNameFromEntity(entity);
|
||||||
|
if (resolvedName) {
|
||||||
|
cameraConfig.frigate.camera_name = resolvedName;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasAutoTriggers) {
|
||||||
|
// 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 = await entityRegistryManager.getMatchingEntities(
|
||||||
|
hass,
|
||||||
|
(ent) =>
|
||||||
|
ent.config_entry_id === entity?.config_entry_id &&
|
||||||
|
!ent.disabled_by &&
|
||||||
|
ent.entity_id.startsWith('binary_sensor.'),
|
||||||
|
);
|
||||||
|
|
||||||
|
const extendedEntities = await entityRegistryManager.getExtendedEntities(
|
||||||
|
hass,
|
||||||
|
binarySensorEntities.map((entity) => entity.entity_id),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (cameraConfig.triggers.motion) {
|
||||||
|
const motionEntity = this._getMotionSensor(cameraConfig, [
|
||||||
|
...extendedEntities.values(),
|
||||||
|
]);
|
||||||
|
if (motionEntity) {
|
||||||
|
cameraConfig.triggers.entities.push(motionEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cameraConfig.triggers.occupancy) {
|
||||||
|
const occupancyEntity = this._getOccupancySensor(cameraConfig, [
|
||||||
|
...extendedEntities.values(),
|
||||||
|
]);
|
||||||
|
if (occupancyEntity) {
|
||||||
|
cameraConfig.triggers.entities.push(occupancyEntity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// De-duplicate triggering entities.
|
||||||
|
cameraConfig.triggers.entities = uniq(cameraConfig.triggers.entities);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cameraConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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(
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
extendedEntities: ExtendedEntity[],
|
||||||
|
): string | null {
|
||||||
|
if (cameraConfig.frigate.camera_name) {
|
||||||
|
return (
|
||||||
|
extendedEntities.find(
|
||||||
|
(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(
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
extendedEntities: ExtendedEntity[],
|
||||||
|
): string | null {
|
||||||
|
if (cameraConfig.frigate.camera_name) {
|
||||||
|
return (
|
||||||
|
extendedEntities.find(
|
||||||
|
(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;
|
||||||
|
}
|
||||||
|
|
||||||
public getMediaDownloadPath(
|
public getMediaDownloadPath(
|
||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
media: ViewMedia,
|
media: ViewMedia,
|
||||||
@@ -137,7 +300,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultEventQuery(
|
public generateDefaultEventQuery(
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
query?: PartialEventQuery,
|
query?: PartialEventQuery,
|
||||||
): EventQuery[] | null {
|
): EventQuery[] | null {
|
||||||
@@ -182,7 +345,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultRecordingQuery(
|
public generateDefaultRecordingQuery(
|
||||||
_cameras: Map<string, CameraConfig>,
|
_cameras: CameraConfigs,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
query?: PartialRecordingQuery,
|
query?: PartialRecordingQuery,
|
||||||
): RecordingQuery[] {
|
): RecordingQuery[] {
|
||||||
@@ -196,7 +359,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public generateDefaultRecordingSegmentsQuery(
|
public generateDefaultRecordingSegmentsQuery(
|
||||||
_cameras: Map<string, CameraConfig>,
|
_cameras: CameraConfigs,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
query: PartialRecordingSegmentsQuery,
|
query: PartialRecordingSegmentsQuery,
|
||||||
): RecordingSegmentsQuery[] | null {
|
): RecordingSegmentsQuery[] | null {
|
||||||
@@ -229,7 +392,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _buildInstanceToCameraIDMapFromQuery(
|
protected _buildInstanceToCameraIDMapFromQuery(
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
): Map<string, Set<string>> {
|
): Map<string, Set<string>> {
|
||||||
const output: Map<string, Set<string>> = new Map();
|
const output: Map<string, Set<string>> = new Map();
|
||||||
@@ -247,7 +410,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _getFrigateCameraNamesForCameraIDs(
|
protected _getFrigateCameraNamesForCameraIDs(
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
cameraIDs: Set<string>,
|
cameraIDs: Set<string>,
|
||||||
): Set<string> {
|
): Set<string> {
|
||||||
const output = new Set<string>();
|
const output = new Set<string>();
|
||||||
@@ -262,7 +425,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public async getEvents(
|
public async getEvents(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
query: EventQuery,
|
query: EventQuery,
|
||||||
): Promise<EventQueryResultsMap | null> {
|
): Promise<EventQueryResultsMap | null> {
|
||||||
const output: EventQueryResultsMap = new Map();
|
const output: EventQueryResultsMap = new Map();
|
||||||
@@ -326,7 +489,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public async getRecordings(
|
public async getRecordings(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
query: RecordingQuery,
|
query: RecordingQuery,
|
||||||
): Promise<RecordingQueryResultsMap | null> {
|
): Promise<RecordingQueryResultsMap | null> {
|
||||||
const output: RecordingQueryResultsMap = new Map();
|
const output: RecordingQueryResultsMap = new Map();
|
||||||
@@ -408,7 +571,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public async getRecordingSegments(
|
public async getRecordingSegments(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
query: RecordingSegmentsQuery,
|
query: RecordingSegmentsQuery,
|
||||||
): Promise<RecordingSegmentsQueryResultsMap | null> {
|
): Promise<RecordingSegmentsQueryResultsMap | null> {
|
||||||
const output: RecordingSegmentsQueryResultsMap = new Map();
|
const output: RecordingSegmentsQueryResultsMap = new Map();
|
||||||
@@ -474,7 +637,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _getCameraIDMatch(
|
protected _getCameraIDMatch(
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
query: DataQuery,
|
query: DataQuery,
|
||||||
instanceID: string,
|
instanceID: string,
|
||||||
cameraName: string,
|
cameraName: string,
|
||||||
@@ -499,7 +662,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public generateMediaFromEvents(
|
public generateMediaFromEvents(
|
||||||
_hass: HomeAssistant,
|
_hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
query: EventQuery,
|
query: EventQuery,
|
||||||
results: QueryReturnType<EventQuery>,
|
results: QueryReturnType<EventQuery>,
|
||||||
): ViewMedia[] | null {
|
): ViewMedia[] | null {
|
||||||
@@ -552,7 +715,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public generateMediaFromRecordings(
|
public generateMediaFromRecordings(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
_query: RecordingQuery,
|
_query: RecordingQuery,
|
||||||
results: QueryReturnType<RecordingQuery>,
|
results: QueryReturnType<RecordingQuery>,
|
||||||
): ViewMedia[] | null {
|
): ViewMedia[] | null {
|
||||||
@@ -590,7 +753,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public async getMediaSeekTime(
|
public async getMediaSeekTime(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
media: ViewMedia,
|
media: ViewMedia,
|
||||||
target: Date,
|
target: Date,
|
||||||
): Promise<number | null> {
|
): Promise<number | null> {
|
||||||
@@ -623,7 +786,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _getQueryableCameraConfig(
|
protected _getQueryableCameraConfig(
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
cameraID: string,
|
cameraID: string,
|
||||||
): CameraConfig | null {
|
): CameraConfig | null {
|
||||||
const cameraConfig = cameras.get(cameraID);
|
const cameraConfig = cameras.get(cameraID);
|
||||||
@@ -635,7 +798,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
|
|
||||||
public async getMediaMetadata(
|
public async getMediaMetadata(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
): Promise<MediaMetadata | null> {
|
): Promise<MediaMetadata | null> {
|
||||||
const what: Set<string> = new Set();
|
const what: Set<string> = new Set();
|
||||||
const where: Set<string> = new Set();
|
const where: Set<string> = new Set();
|
||||||
@@ -715,7 +878,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
*/
|
*/
|
||||||
protected async _garbageCollectSegments(
|
protected async _garbageCollectSegments(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameras: Map<string, CameraConfig>,
|
cameras: CameraConfigs,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const cameraIDs = this._recordingSegmentsCache.getCameraIDs();
|
const cameraIDs = this._recordingSegmentsCache.getCameraIDs();
|
||||||
const recordingQuery: RecordingQuery = {
|
const recordingQuery: RecordingQuery = {
|
||||||
@@ -812,6 +975,9 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
return {
|
return {
|
||||||
canFavoriteEvents: !isBirdseye,
|
canFavoriteEvents: !isBirdseye,
|
||||||
canFavoriteRecordings: !isBirdseye,
|
canFavoriteRecordings: !isBirdseye,
|
||||||
|
supportsClips: !isBirdseye,
|
||||||
|
supportsSnapshots: !isBirdseye,
|
||||||
|
supportsRecordings: !isBirdseye,
|
||||||
supportsTimeline: !isBirdseye,
|
supportsTimeline: !isBirdseye,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -826,6 +992,7 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameraConfig: CameraConfig,
|
cameraConfig: CameraConfig,
|
||||||
): CameraManagerCameraMetadata {
|
): CameraManagerCameraMetadata {
|
||||||
|
const metadata = super.getCameraMetadata(hass, cameraConfig);
|
||||||
return {
|
return {
|
||||||
title:
|
title:
|
||||||
cameraConfig.title ??
|
cameraConfig.title ??
|
||||||
@@ -834,10 +1001,51 @@ export class FrigateCameraManagerEngine implements CameraManagerEngine {
|
|||||||
prettifyTitle(cameraConfig.frigate?.camera_name) ??
|
prettifyTitle(cameraConfig.frigate?.camera_name) ??
|
||||||
cameraConfig.id ??
|
cameraConfig.id ??
|
||||||
'',
|
'',
|
||||||
icon:
|
icon: metadata.icon,
|
||||||
cameraConfig?.icon ??
|
|
||||||
getEntityIcon(hass, cameraConfig.camera_entity) ??
|
|
||||||
'mdi:video',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCameraURL(
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
context?: CameraURLContext,
|
||||||
|
): string | null {
|
||||||
|
if (!cameraConfig.frigate.url) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!cameraConfig.frigate.camera_name) {
|
||||||
|
return cameraConfig.frigate.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventsURL =
|
||||||
|
`${cameraConfig.frigate.url}/events?camera=` + cameraConfig.frigate.camera_name;
|
||||||
|
const recordingsURL =
|
||||||
|
`${cameraConfig.frigate.url}/recording/` + cameraConfig.frigate.camera_name;
|
||||||
|
|
||||||
|
// If media is available, use it since it may result in a more precisely
|
||||||
|
// correct URL.
|
||||||
|
switch (context?.media?.getMediaType()) {
|
||||||
|
case 'clip':
|
||||||
|
case 'snapshot':
|
||||||
|
return eventsURL;
|
||||||
|
case 'recording':
|
||||||
|
const startTime = context.media.getStartTime();
|
||||||
|
if (startTime) {
|
||||||
|
return recordingsURL + format(startTime, 'yyyy-MM-dd/HH');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, fall back to just using the view if we have that.
|
||||||
|
switch (context?.view) {
|
||||||
|
case 'clip':
|
||||||
|
case 'clips':
|
||||||
|
case 'snapshots':
|
||||||
|
case 'snapshot':
|
||||||
|
return eventsURL;
|
||||||
|
case 'recording':
|
||||||
|
case 'recordings':
|
||||||
|
return recordingsURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${cameraConfig.frigate.url}/cameras/${cameraConfig.frigate.camera_name}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,186 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unused-vars */
|
||||||
|
|
||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { CameraConfig } from '../../types';
|
||||||
|
import { ViewMedia } from '../../view/media';
|
||||||
|
import {
|
||||||
|
CameraManagerCameraMetadata,
|
||||||
|
CameraManagerMediaCapabilities,
|
||||||
|
DataQuery,
|
||||||
|
EventQuery,
|
||||||
|
EventQueryResultsMap,
|
||||||
|
MediaMetadata,
|
||||||
|
PartialEventQuery,
|
||||||
|
PartialRecordingQuery,
|
||||||
|
PartialRecordingSegmentsQuery,
|
||||||
|
RecordingQueryResultsMap,
|
||||||
|
RecordingSegmentsQuery,
|
||||||
|
RecordingSegmentsQueryResultsMap,
|
||||||
|
CameraURLContext,
|
||||||
|
CameraConfigs,
|
||||||
|
RecordingQuery,
|
||||||
|
QueryReturnType,
|
||||||
|
CameraManagerCameraCapabilities,
|
||||||
|
Engine,
|
||||||
|
} from '../types';
|
||||||
|
import { getEntityIcon, getEntityTitle } from '../../utils/ha';
|
||||||
|
import { EntityRegistryManager } from '../../utils/ha/entity-registry';
|
||||||
|
import { CameraManagerEngine } from '../engine';
|
||||||
|
|
||||||
|
export class GenericCameraManagerEngine implements CameraManagerEngine {
|
||||||
|
public getEngineType(): Engine {
|
||||||
|
return Engine.Generic;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initializeCamera(
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
_entityRegistryManager: EntityRegistryManager,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
): Promise<CameraConfig> {
|
||||||
|
return cameraConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateDefaultEventQuery(
|
||||||
|
_cameras: CameraConfigs,
|
||||||
|
_cameraIDs: Set<string>,
|
||||||
|
_query: PartialEventQuery,
|
||||||
|
): EventQuery[] | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateDefaultRecordingQuery(
|
||||||
|
_cameras: CameraConfigs,
|
||||||
|
_cameraIDs: Set<string>,
|
||||||
|
_query: PartialRecordingQuery,
|
||||||
|
): RecordingQuery[] | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateDefaultRecordingSegmentsQuery(
|
||||||
|
_cameras: CameraConfigs,
|
||||||
|
_cameraIDs: Set<string>,
|
||||||
|
_query: PartialRecordingSegmentsQuery,
|
||||||
|
): RecordingSegmentsQuery[] | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getEvents(
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
_cameras: CameraConfigs,
|
||||||
|
_query: EventQuery,
|
||||||
|
): Promise<EventQueryResultsMap | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRecordings(
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
_cameras: CameraConfigs,
|
||||||
|
_query: RecordingQuery,
|
||||||
|
): Promise<RecordingQueryResultsMap | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getRecordingSegments(
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
_cameras: CameraConfigs,
|
||||||
|
_query: RecordingSegmentsQuery,
|
||||||
|
): Promise<RecordingSegmentsQueryResultsMap | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateMediaFromEvents(
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
_cameras: CameraConfigs,
|
||||||
|
_query: EventQuery,
|
||||||
|
_results: QueryReturnType<EventQuery>,
|
||||||
|
): ViewMedia[] | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateMediaFromRecordings(
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
_cameras: CameraConfigs,
|
||||||
|
_query: RecordingQuery,
|
||||||
|
_results: QueryReturnType<RecordingQuery>,
|
||||||
|
): ViewMedia[] | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMediaDownloadPath(
|
||||||
|
_cameraConfig: CameraConfig,
|
||||||
|
_media: ViewMedia,
|
||||||
|
): string | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async favoriteMedia(
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
_cameraConfig: CameraConfig,
|
||||||
|
_media: ViewMedia,
|
||||||
|
_favorite: boolean,
|
||||||
|
): Promise<void> {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getQueryResultMaxAge(_query: DataQuery): number | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getMediaSeekTime(
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
_cameras: CameraConfigs,
|
||||||
|
_media: ViewMedia,
|
||||||
|
_target: Date,
|
||||||
|
): Promise<number | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getMediaMetadata(
|
||||||
|
_hass: HomeAssistant,
|
||||||
|
_cameras: CameraConfigs,
|
||||||
|
): Promise<MediaMetadata | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCameraMetadata(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
): CameraManagerCameraMetadata {
|
||||||
|
return {
|
||||||
|
title:
|
||||||
|
cameraConfig.title ??
|
||||||
|
getEntityTitle(hass, cameraConfig.camera_entity) ??
|
||||||
|
getEntityTitle(hass, cameraConfig.webrtc_card?.entity) ??
|
||||||
|
cameraConfig.id ??
|
||||||
|
'',
|
||||||
|
icon:
|
||||||
|
cameraConfig?.icon ??
|
||||||
|
getEntityIcon(hass, cameraConfig.camera_entity) ??
|
||||||
|
'mdi:video',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCameraCapabilities(
|
||||||
|
_cameraConfig: CameraConfig,
|
||||||
|
): CameraManagerCameraCapabilities | null {
|
||||||
|
return {
|
||||||
|
canFavoriteEvents: false,
|
||||||
|
canFavoriteRecordings:false,
|
||||||
|
supportsClips: false,
|
||||||
|
supportsRecordings: false,
|
||||||
|
supportsSnapshots: false,
|
||||||
|
supportsTimeline: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public getMediaCapabilities(_media: ViewMedia): CameraManagerMediaCapabilities | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCameraURL(
|
||||||
|
_cameraConfig: CameraConfig,
|
||||||
|
_context?: CameraURLContext,
|
||||||
|
): string | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
+194
-64
@@ -1,11 +1,12 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { CameraConfig, 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,
|
||||||
CameraManagerCameraMetadata,
|
CameraManagerCameraMetadata,
|
||||||
CameraManagerCapabilities,
|
CameraManagerCapabilities,
|
||||||
CameraManagerMediaCapabilities,
|
CameraManagerMediaCapabilities,
|
||||||
|
CameraURLContext,
|
||||||
DataQuery,
|
DataQuery,
|
||||||
EventQuery,
|
EventQuery,
|
||||||
EventQueryResults,
|
EventQueryResults,
|
||||||
@@ -37,6 +38,12 @@ import { CameraManagerEngine } from './engine.js';
|
|||||||
import sum from 'lodash-es/sum';
|
import sum from 'lodash-es/sum';
|
||||||
import add from 'date-fns/add';
|
import add from 'date-fns/add';
|
||||||
import { log } from '../utils/debug.js';
|
import { log } from '../utils/debug.js';
|
||||||
|
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 { cloneDeep } from 'lodash-es';
|
||||||
|
|
||||||
class QueryClassifier {
|
class QueryClassifier {
|
||||||
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
|
public static isEventQuery(query: DataQuery | PartialDataQuery): query is EventQuery {
|
||||||
@@ -77,19 +84,132 @@ export interface ExtendedMediaQueryResult<T extends MediaQuery> {
|
|||||||
results: ViewMedia[];
|
results: ViewMedia[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface InitializedCamera {
|
||||||
|
inputConfig: CameraConfig;
|
||||||
|
initializedConfig: CameraConfig;
|
||||||
|
engine: CameraManagerEngine;
|
||||||
|
}
|
||||||
|
|
||||||
export class CameraManager {
|
export class CameraManager {
|
||||||
protected _engineFactory: CameraManagerEngineFactory;
|
protected _engineFactory: CameraManagerEngineFactory;
|
||||||
protected _cameras: Map<string, CameraConfig>;
|
|
||||||
protected _cardWideConfig?: CardWideConfig;
|
protected _cardWideConfig?: CardWideConfig;
|
||||||
|
protected _store: CameraManagerStore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
engineFactory: CameraManagerEngineFactory,
|
engineFactory: CameraManagerEngineFactory,
|
||||||
cameras: Map<string, CameraConfig>,
|
|
||||||
cardWideConfig?: CardWideConfig,
|
cardWideConfig?: CardWideConfig,
|
||||||
) {
|
) {
|
||||||
this._engineFactory = engineFactory;
|
this._engineFactory = engineFactory;
|
||||||
this._cameras = cameras;
|
|
||||||
this._cardWideConfig = cardWideConfig;
|
this._cardWideConfig = cardWideConfig;
|
||||||
|
this._store = new CameraManagerStore();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async _initializeCamera(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityRegistryManager: EntityRegistryManager,
|
||||||
|
inputCameraConfig: CameraConfig,
|
||||||
|
): Promise<InitializedCamera> {
|
||||||
|
const engineType = await this._engineFactory.getEngineForCamera(
|
||||||
|
hass,
|
||||||
|
inputCameraConfig,
|
||||||
|
);
|
||||||
|
const engine = engineType
|
||||||
|
? this._store.getEngineOfType(engineType) ??
|
||||||
|
(await this._engineFactory.createEngine(engineType))
|
||||||
|
: null;
|
||||||
|
if (!engine) {
|
||||||
|
throw new CameraInitializationError(
|
||||||
|
localize('error.no_camera_engine'),
|
||||||
|
inputCameraConfig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const initializedConfig = await engine.initializeCamera(
|
||||||
|
hass,
|
||||||
|
entityRegistryManager,
|
||||||
|
// Camera initialization may modify the configuration. Keep the original
|
||||||
|
// for display in error messages to avoid user confusion.
|
||||||
|
cloneDeep(inputCameraConfig),
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
inputConfig: inputCameraConfig,
|
||||||
|
initializedConfig: initializedConfig,
|
||||||
|
engine: engine,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async initializeCameras(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityRegistryManager: EntityRegistryManager,
|
||||||
|
camerasConfig: CamerasConfig,
|
||||||
|
): Promise<void> {
|
||||||
|
const hasAutoTriggers = (config: CameraConfig): boolean => {
|
||||||
|
return config.triggers.motion || config.triggers.occupancy;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (
|
||||||
|
// If any camera requires automatic trigger detection ...
|
||||||
|
camerasConfig.some((config) => hasAutoTriggers(config))
|
||||||
|
) {
|
||||||
|
// ... then we need to populate the entity cache by fetching all entities
|
||||||
|
// from Home Assistant.
|
||||||
|
await entityRegistryManager.fetchEntityList(hass);
|
||||||
|
}
|
||||||
|
|
||||||
|
const results = await allPromises(
|
||||||
|
camerasConfig,
|
||||||
|
async (cameraConfig) =>
|
||||||
|
await this._initializeCamera(hass, entityRegistryManager, cameraConfig),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Do the additions based off the result-order, to ensure the map order is
|
||||||
|
// preserved.
|
||||||
|
results.forEach((result) => {
|
||||||
|
const id = getCameraID(result.initializedConfig);
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
throw new CameraInitializationError(
|
||||||
|
localize('error.no_camera_id'),
|
||||||
|
result.inputConfig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this._store.hasCameraID(id)) {
|
||||||
|
throw new CameraInitializationError(
|
||||||
|
localize('error.duplicate_camera_id'),
|
||||||
|
result.inputConfig,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._store.addCamera(id, result.initializedConfig, result.engine);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!this._store.getCameraCount()) {
|
||||||
|
throw new CameraInitializationError(localize('error.no_cameras'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public isInitialized(): boolean {
|
||||||
|
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 generateDefaultEventQueries(
|
public generateDefaultEventQueries(
|
||||||
@@ -127,13 +247,13 @@ export class CameraManager {
|
|||||||
const where: Set<string> = new Set();
|
const where: Set<string> = new Set();
|
||||||
const days: Set<string> = new Set();
|
const days: Set<string> = new Set();
|
||||||
|
|
||||||
const engines = this._engineFactory.getAllEngines(this._cameras);
|
const engines = this._store.getAllEngines();
|
||||||
if (!engines) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const processMetadata = async (engine: CameraManagerEngine): Promise<void> => {
|
const processMetadata = async (engine: CameraManagerEngine): Promise<void> => {
|
||||||
const engineMetadata = await engine.getMediaMetadata(hass, this._cameras);
|
const engineMetadata = await engine.getMediaMetadata(
|
||||||
|
hass,
|
||||||
|
this._store.getCameras(),
|
||||||
|
);
|
||||||
if (engineMetadata) {
|
if (engineMetadata) {
|
||||||
if (engineMetadata.what) {
|
if (engineMetadata.what) {
|
||||||
engineMetadata.what.forEach(what.add, what);
|
engineMetadata.what.forEach(what.add, what);
|
||||||
@@ -147,7 +267,7 @@ export class CameraManager {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
await allPromises(engines, (engine) => processMetadata(engine));
|
await allPromises(engines, processMetadata);
|
||||||
|
|
||||||
if (!what.size && !where.size && !days.size) {
|
if (!what.size && !where.size && !days.size) {
|
||||||
return null;
|
return null;
|
||||||
@@ -165,11 +285,7 @@ export class CameraManager {
|
|||||||
): PartialQueryConcreteType<PQT>[] | null {
|
): PartialQueryConcreteType<PQT>[] | null {
|
||||||
const concreteQueries: PartialQueryConcreteType<PQT>[] = [];
|
const concreteQueries: PartialQueryConcreteType<PQT>[] = [];
|
||||||
const _cameraIDs = setify(cameraIDs);
|
const _cameraIDs = setify(cameraIDs);
|
||||||
|
const engines = this._store.getEnginesForCameraIDs(_cameraIDs);
|
||||||
const engines = this._engineFactory.getEnginesForCameraIDs(
|
|
||||||
this._cameras,
|
|
||||||
_cameraIDs,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!engines) {
|
if (!engines) {
|
||||||
return null;
|
return null;
|
||||||
@@ -179,19 +295,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._cameras,
|
this._store.getCameras(),
|
||||||
cameraIDs,
|
cameraIDs,
|
||||||
partialQuery,
|
partialQuery,
|
||||||
);
|
);
|
||||||
} else if (QueryClassifier.isRecordingQuery(partialQuery)) {
|
} else if (QueryClassifier.isRecordingQuery(partialQuery)) {
|
||||||
queries = engine.generateDefaultRecordingQuery(
|
queries = engine.generateDefaultRecordingQuery(
|
||||||
this._cameras,
|
this._store.getCameras(),
|
||||||
cameraIDs,
|
cameraIDs,
|
||||||
partialQuery,
|
partialQuery,
|
||||||
);
|
);
|
||||||
} else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
|
} else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
|
||||||
queries = engine.generateDefaultRecordingSegmentsQuery(
|
queries = engine.generateDefaultRecordingSegmentsQuery(
|
||||||
this._cameras,
|
this._store.getCameras(),
|
||||||
cameraIDs,
|
cameraIDs,
|
||||||
partialQuery,
|
partialQuery,
|
||||||
);
|
);
|
||||||
@@ -303,10 +419,9 @@ export class CameraManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getMediaDownloadPath(media: ViewMedia): string | null {
|
public getMediaDownloadPath(media: ViewMedia): string | null {
|
||||||
const cameraConfig = this._cameras.get(media.getCameraID());
|
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
||||||
const engine = cameraConfig
|
const engine = this._store.getEngineForMedia(media);
|
||||||
? this._engineFactory.getEngineForCamera(cameraConfig)
|
|
||||||
: null;
|
|
||||||
if (!cameraConfig || !engine) {
|
if (!cameraConfig || !engine) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -314,7 +429,7 @@ export class CameraManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null {
|
public getMediaCapabilities(media: ViewMedia): CameraManagerMediaCapabilities | null {
|
||||||
const engine = this._engineFactory.getEngineForMedia(this._cameras, media);
|
const engine = this._store.getEngineForMedia(media);
|
||||||
if (!engine) {
|
if (!engine) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -326,12 +441,13 @@ export class CameraManager {
|
|||||||
media: ViewMedia,
|
media: ViewMedia,
|
||||||
favorite: boolean,
|
favorite: boolean,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const cameraConfig = this._cameras.get(media.getCameraID());
|
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
||||||
if (!cameraConfig) {
|
const engine = this._store.getEngineForMedia(media);
|
||||||
|
|
||||||
|
if (!cameraConfig || !engine) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const engine = this._engineFactory.getEngineForCamera(cameraConfig);
|
|
||||||
if (engine) {
|
|
||||||
const queryStartTime = new Date();
|
const queryStartTime = new Date();
|
||||||
await engine.favoriteMedia(hass, cameraConfig, media, favorite);
|
await engine.favoriteMedia(hass, cameraConfig, media, favorite);
|
||||||
|
|
||||||
@@ -346,7 +462,6 @@ export class CameraManager {
|
|||||||
')',
|
')',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public areMediaQueriesResultsFresh<T extends MediaQuery>(
|
public areMediaQueriesResultsFresh<T extends MediaQuery>(
|
||||||
queries: T[],
|
queries: T[],
|
||||||
@@ -355,10 +470,7 @@ export class CameraManager {
|
|||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
|
||||||
for (const query of queries) {
|
for (const query of queries) {
|
||||||
const engines = this._engineFactory.getEnginesForCameraIDs(
|
const engines = this._store.getEnginesForCameraIDs(query.cameraIDs);
|
||||||
this._cameras,
|
|
||||||
query.cameraIDs,
|
|
||||||
);
|
|
||||||
for (const [engine, cameraIDs] of engines ?? []) {
|
for (const [engine, cameraIDs] of engines ?? []) {
|
||||||
const maxAgeSeconds = engine.getQueryResultMaxAge({
|
const maxAgeSeconds = engine.getQueryResultMaxAge({
|
||||||
...query,
|
...query,
|
||||||
@@ -382,9 +494,11 @@ export class CameraManager {
|
|||||||
): Promise<number | null> {
|
): Promise<number | null> {
|
||||||
const startTime = media.getStartTime();
|
const startTime = media.getStartTime();
|
||||||
const endTime = media.getEndTime();
|
const endTime = media.getEndTime();
|
||||||
const cameraConfig = this._cameras.get(media.getCameraID());
|
const cameraConfig = this._store.getCameraConfigForMedia(media);
|
||||||
|
const engine = this._store.getEngineForMedia(media);
|
||||||
if (
|
if (
|
||||||
!cameraConfig ||
|
!cameraConfig ||
|
||||||
|
!engine ||
|
||||||
!startTime ||
|
!startTime ||
|
||||||
!endTime ||
|
!endTime ||
|
||||||
target < startTime ||
|
target < startTime ||
|
||||||
@@ -393,8 +507,7 @@ export class CameraManager {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const engine = this._engineFactory.getEngineForCamera(cameraConfig);
|
return await engine.getMediaSeekTime(hass, this._store.getCameras(), media, target);
|
||||||
return (await engine?.getMediaSeekTime(hass, this._cameras, media, target)) ?? null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async _handleQuery<QT extends DataQuery>(
|
protected async _handleQuery<QT extends DataQuery>(
|
||||||
@@ -415,19 +528,21 @@ export class CameraManager {
|
|||||||
|
|
||||||
let engineResult: Map<QT, QueryReturnType<QT>> | null = null;
|
let engineResult: Map<QT, QueryReturnType<QT>> | null = null;
|
||||||
if (QueryClassifier.isEventQuery(query)) {
|
if (QueryClassifier.isEventQuery(query)) {
|
||||||
engineResult = (await engine.getEvents(hass, this._cameras, query)) as Map<
|
engineResult = (await engine.getEvents(
|
||||||
QT,
|
hass,
|
||||||
QueryReturnType<QT>
|
this._store.getCameras(),
|
||||||
> | null;
|
query,
|
||||||
|
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||||
} else if (QueryClassifier.isRecordingQuery(query)) {
|
} else if (QueryClassifier.isRecordingQuery(query)) {
|
||||||
engineResult = (await engine.getRecordings(hass, this._cameras, query)) as Map<
|
engineResult = (await engine.getRecordings(
|
||||||
QT,
|
hass,
|
||||||
QueryReturnType<QT>
|
this._store.getCameras(),
|
||||||
> | null;
|
query,
|
||||||
|
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||||
} else if (QueryClassifier.isRecordingSegmentsQuery(query)) {
|
} else if (QueryClassifier.isRecordingSegmentsQuery(query)) {
|
||||||
engineResult = (await engine.getRecordingSegments(
|
engineResult = (await engine.getRecordingSegments(
|
||||||
hass,
|
hass,
|
||||||
this._cameras,
|
this._store.getCameras(),
|
||||||
query,
|
query,
|
||||||
)) as Map<QT, QueryReturnType<QT>> | null;
|
)) as Map<QT, QueryReturnType<QT>> | null;
|
||||||
}
|
}
|
||||||
@@ -436,10 +551,7 @@ export class CameraManager {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const processQuery = async (query: QT): Promise<void> => {
|
const processQuery = async (query: QT): Promise<void> => {
|
||||||
const engines = this._engineFactory.getEnginesForCameraIDs(
|
const engines = this._store.getEnginesForCameraIDs(query.cameraIDs);
|
||||||
this._cameras,
|
|
||||||
query.cameraIDs,
|
|
||||||
);
|
|
||||||
if (!engines) {
|
if (!engines) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -481,7 +593,7 @@ export class CameraManager {
|
|||||||
): ViewMedia[] {
|
): ViewMedia[] {
|
||||||
const mediaArray: ViewMedia[] = [];
|
const mediaArray: ViewMedia[] = [];
|
||||||
for (const [query, result] of results.entries()) {
|
for (const [query, result] of results.entries()) {
|
||||||
const engine = this._engineFactory.getEngine(result.engine);
|
const engine = this._store.getEngineOfType(result.engine);
|
||||||
|
|
||||||
if (engine) {
|
if (engine) {
|
||||||
let media: ViewMedia[] | null = null;
|
let media: ViewMedia[] | null = null;
|
||||||
@@ -489,12 +601,22 @@ export class CameraManager {
|
|||||||
QueryClassifier.isEventQuery(query) &&
|
QueryClassifier.isEventQuery(query) &&
|
||||||
QueryResultClassifier.isEventQueryResult(result)
|
QueryResultClassifier.isEventQueryResult(result)
|
||||||
) {
|
) {
|
||||||
media = engine.generateMediaFromEvents(hass, this._cameras, query, result);
|
media = engine.generateMediaFromEvents(
|
||||||
|
hass,
|
||||||
|
this._store.getCameras(),
|
||||||
|
query,
|
||||||
|
result,
|
||||||
|
);
|
||||||
} else if (
|
} else if (
|
||||||
QueryClassifier.isRecordingQuery(query) &&
|
QueryClassifier.isRecordingQuery(query) &&
|
||||||
QueryResultClassifier.isRecordingQuery(result)
|
QueryResultClassifier.isRecordingQuery(result)
|
||||||
) {
|
) {
|
||||||
media = engine.generateMediaFromRecordings(hass, this._cameras, query, result);
|
media = engine.generateMediaFromRecordings(
|
||||||
|
hass,
|
||||||
|
this._store.getCameras(),
|
||||||
|
query,
|
||||||
|
result,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (media) {
|
if (media) {
|
||||||
mediaArray.push(...media);
|
mediaArray.push(...media);
|
||||||
@@ -517,13 +639,22 @@ export class CameraManager {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public getCameraURL(cameraID: string, context?: CameraURLContext): string | null {
|
||||||
|
const cameraConfig = this._store.getCameraConfig(cameraID);
|
||||||
|
const engine = this._store.getEngineForCameraID(cameraID);
|
||||||
|
if (!cameraConfig || !engine) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return engine.getCameraURL(cameraConfig, context);
|
||||||
|
}
|
||||||
|
|
||||||
public getCameraMetadata(
|
public getCameraMetadata(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameraID: string,
|
cameraID: string,
|
||||||
): CameraManagerCameraMetadata | null {
|
): CameraManagerCameraMetadata | null {
|
||||||
const cameraConfig = this._cameras.get(cameraID);
|
const cameraConfig = this._store.getCameraConfig(cameraID);
|
||||||
const engine = this._engineFactory.getEngineForCamera(cameraConfig);
|
const engine = this._store.getEngineForCameraID(cameraID);
|
||||||
if (!engine || !cameraConfig) {
|
if (!cameraConfig || !engine) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return engine.getCameraMetadata(hass, cameraConfig);
|
return engine.getCameraMetadata(hass, cameraConfig);
|
||||||
@@ -532,23 +663,18 @@ export class CameraManager {
|
|||||||
public getCameraCapabilities(
|
public getCameraCapabilities(
|
||||||
cameraID: string,
|
cameraID: string,
|
||||||
): CameraManagerCameraCapabilities | null {
|
): CameraManagerCameraCapabilities | null {
|
||||||
const cameraConfig = this._cameras.get(cameraID);
|
const cameraConfig = this._store.getCameraConfig(cameraID);
|
||||||
if (!cameraConfig) {
|
const engine = this._store.getEngineForCameraID(cameraID);
|
||||||
|
if (!cameraConfig || !engine) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const engine = this._engineFactory.getEngineForCamera(cameraConfig);
|
|
||||||
if (!engine) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return engine.getCameraCapabilities(cameraConfig);
|
return engine.getCameraCapabilities(cameraConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAggregateCameraCapabilities(
|
public getAggregateCameraCapabilities(
|
||||||
cameraIDs?: Set<string>,
|
cameraIDs?: Set<string>,
|
||||||
): CameraManagerCapabilities {
|
): CameraManagerCapabilities | null {
|
||||||
const perCameraCapabilities = [...(cameraIDs ?? this._cameras.keys())].map(
|
const perCameraCapabilities = [...(cameraIDs ?? this._store.getCameraIDs())].map(
|
||||||
(cameraID) => this.getCameraCapabilities(cameraID),
|
(cameraID) => this.getCameraCapabilities(cameraID),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -557,6 +683,10 @@ export class CameraManager {
|
|||||||
canFavoriteRecordings: perCameraCapabilities.some(
|
canFavoriteRecordings: perCameraCapabilities.some(
|
||||||
(cap) => cap?.canFavoriteRecordings,
|
(cap) => cap?.canFavoriteRecordings,
|
||||||
),
|
),
|
||||||
|
|
||||||
|
supportsClips: perCameraCapabilities.some((cap) => cap?.supportsClips),
|
||||||
|
supportsRecordings: perCameraCapabilities.some((cap) => cap?.supportsRecordings),
|
||||||
|
supportsSnapshots: perCameraCapabilities.some((cap) => cap?.supportsSnapshots),
|
||||||
supportsTimeline: perCameraCapabilities.some((cap) => cap?.supportsTimeline),
|
supportsTimeline: perCameraCapabilities.some((cap) => cap?.supportsTimeline),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import uniq from 'lodash-es/uniq';
|
||||||
|
import { CameraConfig } from '../types';
|
||||||
|
import { ViewMedia } from '../view/media';
|
||||||
|
import { CameraManagerEngine } from './engine';
|
||||||
|
import { CameraConfigs, Engine } from './types';
|
||||||
|
|
||||||
|
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
|
||||||
|
|
||||||
|
export class CameraManagerStore {
|
||||||
|
protected _configs: Map<string, CameraConfig> = new Map();
|
||||||
|
protected _engines: Map<string, CameraManagerEngine> = new Map();
|
||||||
|
protected _enginesByType: Map<Engine, CameraManagerEngine> = new Map();
|
||||||
|
|
||||||
|
public addCamera(
|
||||||
|
cameraID: string,
|
||||||
|
cameraConfig: CameraConfig,
|
||||||
|
engine: CameraManagerEngine,
|
||||||
|
): void {
|
||||||
|
this._configs.set(cameraID, cameraConfig);
|
||||||
|
this._engines.set(cameraID, engine);
|
||||||
|
this._enginesByType.set(engine.getEngineType(), engine);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCameraCount(): number {
|
||||||
|
return this._configs.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
public hasCameraID(cameraID: string): boolean {
|
||||||
|
return this._configs.has(cameraID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCameraConfig(cameraID: string): CameraConfig | null {
|
||||||
|
return this._configs.get(cameraID) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCameras(): CameraConfigs {
|
||||||
|
return this._configs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCameraIDs(): Set<string> {
|
||||||
|
return new Set(this._configs.keys());
|
||||||
|
}
|
||||||
|
|
||||||
|
public getCameraConfigForMedia(media: ViewMedia): CameraConfig | null {
|
||||||
|
const cameraID = media.getCameraID();
|
||||||
|
if (!cameraID) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.getCameraConfig(cameraID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEngineOfType(engine: Engine): CameraManagerEngine | null {
|
||||||
|
return this._enginesByType.get(engine) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEngineForCameraID(cameraID: string): CameraManagerEngine | null {
|
||||||
|
return this._engines.get(cameraID) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEnginesForCameraIDs(
|
||||||
|
cameraIDs: Set<string>,
|
||||||
|
): CameraManagerEngineCameraIDMap | null {
|
||||||
|
const output: CameraManagerEngineCameraIDMap = new Map();
|
||||||
|
|
||||||
|
for (const cameraID of cameraIDs) {
|
||||||
|
const engine = this.getEngineForCameraID(cameraID);
|
||||||
|
if (!engine) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!output.has(engine)) {
|
||||||
|
output.set(engine, new Set());
|
||||||
|
}
|
||||||
|
output.get(engine)?.add(cameraID);
|
||||||
|
}
|
||||||
|
return output.size ? output : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getEngineForMedia(media: ViewMedia): CameraManagerEngine | null {
|
||||||
|
const cameraID = media.getCameraID();
|
||||||
|
if (!cameraID) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return this.getEngineForCameraID(cameraID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public getAllEngines(): CameraManagerEngine[] {
|
||||||
|
return uniq([...this._engines.values()]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { CameraConfig, FrigateCardView } from '../types';
|
||||||
|
import { ViewMedia } from '../view/media';
|
||||||
import { FrigateEvent, FrigateRecording } from './frigate/types';
|
import { FrigateEvent, FrigateRecording } from './frigate/types';
|
||||||
|
|
||||||
// ====
|
// ====
|
||||||
@@ -18,6 +20,7 @@ export enum QueryResultsType {
|
|||||||
|
|
||||||
export enum Engine {
|
export enum Engine {
|
||||||
Frigate = 'frigate',
|
Frigate = 'frigate',
|
||||||
|
Generic = 'generic',
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataQuery {
|
export interface DataQuery {
|
||||||
@@ -85,6 +88,10 @@ export interface MediaMetadata {
|
|||||||
interface BaseCapabilities {
|
interface BaseCapabilities {
|
||||||
canFavoriteEvents: boolean;
|
canFavoriteEvents: boolean;
|
||||||
canFavoriteRecordings: boolean;
|
canFavoriteRecordings: boolean;
|
||||||
|
|
||||||
|
supportsClips: boolean;
|
||||||
|
supportsRecordings: boolean;
|
||||||
|
supportsSnapshots: boolean;
|
||||||
supportsTimeline: boolean;
|
supportsTimeline: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +106,13 @@ export interface CameraManagerCameraMetadata {
|
|||||||
icon: string;
|
icon: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CameraURLContext {
|
||||||
|
media?: ViewMedia;
|
||||||
|
view?: FrigateCardView;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CameraConfigs = Map<string, CameraConfig>;
|
||||||
|
|
||||||
// ===========
|
// ===========
|
||||||
// Event Query
|
// Event Query
|
||||||
// ===========
|
// ===========
|
||||||
|
|||||||
+144
-345
@@ -11,7 +11,6 @@ import { customElement, property, state } from 'lit/decorators.js';
|
|||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||||
import { until } from 'lit/directives/until.js';
|
|
||||||
import throttle from 'lodash-es/throttle';
|
import throttle from 'lodash-es/throttle';
|
||||||
import screenfull from 'screenfull';
|
import screenfull from 'screenfull';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -33,19 +32,13 @@ import './components/message.js';
|
|||||||
import { renderMessage, renderProgressIndicator } from './components/message.js';
|
import { renderMessage, renderProgressIndicator } from './components/message.js';
|
||||||
import './components/thumbnail-carousel.js';
|
import './components/thumbnail-carousel.js';
|
||||||
import { isConfigUpgradeable } from './config-mgmt.js';
|
import { isConfigUpgradeable } from './config-mgmt.js';
|
||||||
import {
|
import { MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA, REPO_URL } from './const.js';
|
||||||
CAMERA_BIRDSEYE,
|
|
||||||
MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA,
|
|
||||||
REPO_URL,
|
|
||||||
} from './const.js';
|
|
||||||
import { getLanguage, loadLanguages, localize } from './localize/localize.js';
|
import { getLanguage, loadLanguages, localize } from './localize/localize.js';
|
||||||
import cardStyle from './scss/card.scss';
|
import cardStyle from './scss/card.scss';
|
||||||
import {
|
import {
|
||||||
Actions,
|
Actions,
|
||||||
ActionType,
|
ActionType,
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
EntityList,
|
|
||||||
ExtendedEntity,
|
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
FrigateCardConfig,
|
FrigateCardConfig,
|
||||||
frigateCardConfigSchema,
|
frigateCardConfigSchema,
|
||||||
@@ -58,6 +51,8 @@ import {
|
|||||||
Message,
|
Message,
|
||||||
RawFrigateCardConfig,
|
RawFrigateCardConfig,
|
||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
|
FrigateCardError,
|
||||||
|
FRIGATE_CARD_VIEW_DEFAULT,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import {
|
import {
|
||||||
convertActionToFrigateCardCustomAction,
|
convertActionToFrigateCardCustomAction,
|
||||||
@@ -67,7 +62,6 @@ import {
|
|||||||
getActionConfigGivenAction,
|
getActionConfigGivenAction,
|
||||||
} from './utils/action.js';
|
} from './utils/action.js';
|
||||||
import { contentsChanged, errorToConsole } from './utils/basic.js';
|
import { contentsChanged, errorToConsole } from './utils/basic.js';
|
||||||
import { getCameraID } from './utils/camera.js';
|
|
||||||
import {
|
import {
|
||||||
getEntityIcon,
|
getEntityIcon,
|
||||||
getEntityTitle,
|
getEntityTitle,
|
||||||
@@ -79,12 +73,6 @@ import {
|
|||||||
sideLoadHomeAssistantElements,
|
sideLoadHomeAssistantElements,
|
||||||
} from './utils/ha';
|
} from './utils/ha';
|
||||||
import { DeviceList, getAllDevices } from './utils/ha/device-registry.js';
|
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 { ResolvedMediaCache } from './utils/ha/resolved-media.js';
|
||||||
import { supportsFeature } from './utils/ha/update.js';
|
import { supportsFeature } from './utils/ha/update.js';
|
||||||
import { isValidMediaLoadedInfo } from './utils/media-info.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 { setLowPerformanceProfile, setPerformanceCSSStyles } from './performance.js';
|
||||||
import { CameraManagerEngineFactory } from './camera-manager/engine-factory.js';
|
import { CameraManagerEngineFactory } from './camera-manager/engine-factory.js';
|
||||||
import { log } from './utils/debug.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:
|
/** A note on media callbacks:
|
||||||
*
|
*
|
||||||
@@ -196,9 +188,6 @@ class FrigateCard extends LitElement {
|
|||||||
// Array of dynamic menu buttons to be added to menu.
|
// Array of dynamic menu buttons to be added to menu.
|
||||||
protected _dynamicMenuButtons: MenuButton[] = [];
|
protected _dynamicMenuButtons: MenuButton[] = [];
|
||||||
|
|
||||||
@state()
|
|
||||||
protected _cameras?: Map<string, CameraConfig>;
|
|
||||||
|
|
||||||
// Error/info message to render.
|
// Error/info message to render.
|
||||||
protected _message: Message | null = null;
|
protected _message: Message | null = null;
|
||||||
|
|
||||||
@@ -207,6 +196,8 @@ class FrigateCard extends LitElement {
|
|||||||
|
|
||||||
protected _cameraManager?: CameraManager;
|
protected _cameraManager?: CameraManager;
|
||||||
|
|
||||||
|
protected _entityRegistryManager: EntityRegistryManager;
|
||||||
|
|
||||||
// The mouse handler may be called continually, throttle it to at most once
|
// The mouse handler may be called continually, throttle it to at most once
|
||||||
// per second for performance reasons.
|
// per second for performance reasons.
|
||||||
protected _boundMouseHandler = throttle(this._mouseHandler.bind(this), 1 * 1000);
|
protected _boundMouseHandler = throttle(this._mouseHandler.bind(this), 1 * 1000);
|
||||||
@@ -219,6 +210,14 @@ class FrigateCard extends LitElement {
|
|||||||
|
|
||||||
protected _conditionManager: CardConditionManager | null = null;
|
protected _conditionManager: CardConditionManager | null = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
this._entityRegistryManager = new EntityRegistryManager(
|
||||||
|
new EntityCache<Entity>(),
|
||||||
|
new EntityCache<ExtendedEntity>(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the Home Assistant object.
|
* Set the Home Assistant object.
|
||||||
*/
|
*/
|
||||||
@@ -383,6 +382,17 @@ class FrigateCard extends LitElement {
|
|||||||
protected _getMenuButtons(): MenuButton[] {
|
protected _getMenuButtons(): MenuButton[] {
|
||||||
const buttons: 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({
|
buttons.push({
|
||||||
// Use a magic icon value that the menu will use to render the custom
|
// Use a magic icon value that the menu will use to render the custom
|
||||||
// Frigate icon.
|
// Frigate icon.
|
||||||
@@ -398,8 +408,8 @@ class FrigateCard extends LitElement {
|
|||||||
) as FrigateCardCustomAction,
|
) as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (this._cameras && this._cameras.size > 1) {
|
if (cameras) {
|
||||||
const menuItems = Array.from(this._cameras, ([cameraID, config]) => {
|
const menuItems = Array.from(cameras, ([cameraID, config]) => {
|
||||||
const action = createFrigateCardCustomAction('camera_select', {
|
const action = createFrigateCardCustomAction('camera_select', {
|
||||||
camera: cameraID,
|
camera: cameraID,
|
||||||
});
|
});
|
||||||
@@ -436,16 +446,7 @@ class FrigateCard extends LitElement {
|
|||||||
tap_action: createFrigateCardCustomAction('live') as FrigateCardCustomAction,
|
tap_action: createFrigateCardCustomAction('live') as FrigateCardCustomAction,
|
||||||
});
|
});
|
||||||
|
|
||||||
const cameraConfig = this._getSelectedCameraConfig();
|
if (cameraCapabilities?.supportsClips) {
|
||||||
|
|
||||||
// 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)
|
|
||||||
) {
|
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'mdi:filmstrip',
|
icon: 'mdi:filmstrip',
|
||||||
...this._getConfig().menu.buttons.clips,
|
...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
|
if (cameraCapabilities?.supportsSnapshots) {
|
||||||
// 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)
|
|
||||||
) {
|
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'mdi:camera',
|
icon: 'mdi:camera',
|
||||||
...this._getConfig().menu.buttons.snapshots,
|
...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
|
if (cameraCapabilities?.supportsRecordings) {
|
||||||
// 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)
|
|
||||||
) {
|
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'mdi:album',
|
icon: 'mdi:album',
|
||||||
...this._getConfig().menu.buttons.recordings,
|
...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
|
// Don't show the timeline button unless there's at least one non-birdseye
|
||||||
// camera with a Frigate camera name.
|
// camera with a Frigate camera name.
|
||||||
if (
|
if (cameraCapabilities?.supportsTimeline) {
|
||||||
this._cameras &&
|
|
||||||
[...this._cameras.values()].some(
|
|
||||||
(config) =>
|
|
||||||
config.frigate.camera_name && config.frigate.camera_name !== CAMERA_BIRDSEYE,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'mdi:chart-gantt',
|
icon: 'mdi:chart-gantt',
|
||||||
...this._getConfig().menu.buttons.timeline,
|
...this._getConfig().menu.buttons.timeline,
|
||||||
@@ -545,7 +526,7 @@ class FrigateCard extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cameraConfig?.frigate.url) {
|
if (this._getCameraURLFromContext()) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
icon: 'mdi:web',
|
icon: 'mdi:web',
|
||||||
...this._getConfig().menu.buttons.frigate_ui,
|
...this._getConfig().menu.buttons.frigate_ui,
|
||||||
@@ -590,7 +571,7 @@ class FrigateCard extends LitElement {
|
|||||||
if (
|
if (
|
||||||
mediaPlayers.length &&
|
mediaPlayers.length &&
|
||||||
(this._view?.isViewerView() ||
|
(this._view?.isViewerView() ||
|
||||||
(this._view?.is('live') && cameraConfig?.camera_entity))
|
(this._view?.is('live') && selectedCameraConfig?.camera_entity))
|
||||||
) {
|
) {
|
||||||
const mediaPlayerItems = mediaPlayers.map((playerEntityID) => {
|
const mediaPlayerItems = mediaPlayers.map((playerEntityID) => {
|
||||||
const title = getEntityTitle(this._hass, playerEntityID) || 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.
|
* Get the camera configuration for the selected camera.
|
||||||
* @returns The CameraConfig object or null if not found.
|
* @returns The CameraConfig object or null if not found.
|
||||||
*/
|
*/
|
||||||
protected _getSelectedCameraConfig(): CameraConfig | null {
|
protected _getSelectedCameraConfig(): CameraConfig | null {
|
||||||
if (!this._cameras || !this._cameras.size || !this._view?.camera) {
|
if (!this._view || !this._cameraManager) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return this._cameras.get(this._view.camera) || null;
|
return this._cameraManager.getCameraConfig(this._view.camera);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -997,7 +756,7 @@ class FrigateCard extends LitElement {
|
|||||||
};
|
};
|
||||||
|
|
||||||
this._overriddenConfig = undefined;
|
this._overriddenConfig = undefined;
|
||||||
this._cameras = undefined;
|
this._cameraManager = undefined;
|
||||||
this._view = undefined;
|
this._view = undefined;
|
||||||
this._message = null;
|
this._message = null;
|
||||||
|
|
||||||
@@ -1039,24 +798,27 @@ class FrigateCard extends LitElement {
|
|||||||
|
|
||||||
if (!args?.view) {
|
if (!args?.view) {
|
||||||
// Load the default view.
|
// Load the default view.
|
||||||
let camera;
|
let cameraID: string | null = null;
|
||||||
if (this._cameras?.size) {
|
if (this._cameraManager) {
|
||||||
|
const cameras = this._cameraManager.getCameras();
|
||||||
|
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(this._cameras.keys());
|
const keys = Array.from(cameras.keys());
|
||||||
const currentIndex = keys.indexOf(this._view.camera);
|
const currentIndex = keys.indexOf(this._view.camera);
|
||||||
const targetIndex = currentIndex + 1 >= keys.length ? 0 : currentIndex + 1;
|
const targetIndex = currentIndex + 1 >= keys.length ? 0 : currentIndex + 1;
|
||||||
camera = keys[targetIndex];
|
cameraID = keys[targetIndex];
|
||||||
} else {
|
} else {
|
||||||
// Reset to the default camera.
|
// Reset to the default camera.
|
||||||
camera = this._cameras.keys().next().value;
|
cameraID = cameras.keys().next().value;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (camera) {
|
if (cameraID) {
|
||||||
changeView(
|
changeView(
|
||||||
new View({
|
new View({
|
||||||
view: this._getConfig().view.default,
|
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.
|
* Called before each update.
|
||||||
*/
|
*/
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
if (
|
if (
|
||||||
this._cameras &&
|
|
||||||
this._cardWideConfig &&
|
this._cardWideConfig &&
|
||||||
(changedProps.has('_config') ||
|
(!this._cameraManager ||
|
||||||
changedProps.has('_cameras') ||
|
changedProps.has('_config') ||
|
||||||
changedProps.has('_cardWideConfig'))
|
changedProps.has('_cardWideConfig'))
|
||||||
) {
|
) {
|
||||||
this._cameraManager = new CameraManager(
|
this._cameraManager = new CameraManager(
|
||||||
new CameraManagerEngineFactory(this._cardWideConfig),
|
new CameraManagerEngineFactory(
|
||||||
this._cameras,
|
this._entityRegistryManager,
|
||||||
|
this._cardWideConfig,
|
||||||
|
),
|
||||||
this._cardWideConfig,
|
this._cardWideConfig,
|
||||||
);
|
);
|
||||||
|
this._initializeCameras().then(() => this.requestUpdate());
|
||||||
}
|
}
|
||||||
|
|
||||||
if (changedProps.has('_cardWideConfig')) {
|
if (changedProps.has('_cardWideConfig')) {
|
||||||
@@ -1162,7 +955,8 @@ class FrigateCard extends LitElement {
|
|||||||
let changedCamera = false;
|
let changedCamera = false;
|
||||||
let triggerChanges = 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 triggerEntities = config.triggers.entities ?? [];
|
||||||
const diffs = getHassDifferences(this._hass, oldHass, triggerEntities, {
|
const diffs = getHassDifferences(this._hass, oldHass, triggerEntities, {
|
||||||
stateOnly: true,
|
stateOnly: true,
|
||||||
@@ -1172,10 +966,10 @@ class FrigateCard extends LitElement {
|
|||||||
(entity) => !isTriggeredState(this._hass?.states[entity]),
|
(entity) => !isTriggeredState(this._hass?.states[entity]),
|
||||||
);
|
);
|
||||||
if (shouldTrigger) {
|
if (shouldTrigger) {
|
||||||
this._triggers.set(camera, now);
|
this._triggers.set(cameraID, now);
|
||||||
triggerChanges = true;
|
triggerChanges = true;
|
||||||
} else if (shouldUntrigger && this._triggers.has(camera)) {
|
} else if (shouldUntrigger && this._triggers.has(cameraID)) {
|
||||||
this._triggers.delete(camera);
|
this._triggers.delete(cameraID);
|
||||||
triggerChanges = true;
|
triggerChanges = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1419,7 +1213,7 @@ class FrigateCard extends LitElement {
|
|||||||
const cameraEntity = cameraConfig.camera_entity ?? null;
|
const cameraEntity = cameraConfig.camera_entity ?? null;
|
||||||
const media = this._view.queryResults?.getSelectedResult();
|
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_id = media.getContentID();
|
||||||
media_content_type = media.getContentType();
|
media_content_type = media.getContentType();
|
||||||
title = media.getTitle();
|
title = media.getTitle();
|
||||||
@@ -1491,9 +1285,9 @@ class FrigateCard extends LitElement {
|
|||||||
this._downloadViewerMedia();
|
this._downloadViewerMedia();
|
||||||
break;
|
break;
|
||||||
case 'frigate_ui':
|
case 'frigate_ui':
|
||||||
const frigate_url = this._getFrigateURLFromContext();
|
const url = this._getCameraURLFromContext();
|
||||||
if (frigate_url) {
|
if (url) {
|
||||||
window.open(frigate_url);
|
window.open(url);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'fullscreen':
|
case 'fullscreen':
|
||||||
@@ -1508,22 +1302,15 @@ class FrigateCard extends LitElement {
|
|||||||
this._refMenu.value?.toggleMenu();
|
this._refMenu.value?.toggleMenu();
|
||||||
break;
|
break;
|
||||||
case 'camera_select':
|
case 'camera_select':
|
||||||
const camera = frigateCardAction.camera;
|
const cameraID = frigateCardAction.camera;
|
||||||
if (this._cameras?.has(camera) && this._view) {
|
if (this._cameraManager?.hasCameraID(cameraID) && this._view) {
|
||||||
const targetView = View.selectBestViewForUserSpecified(
|
const viewOnCameraSelect = this._getConfig().view.camera_select;
|
||||||
this._getConfig().view.camera_select === 'current'
|
const targetView =
|
||||||
? this._view.view
|
viewOnCameraSelect === 'current' ? this._view.view : viewOnCameraSelect;
|
||||||
: (this._getConfig().view.camera_select as FrigateCardView),
|
const actualView = this.isViewSupportedByCamera(cameraID, targetView)
|
||||||
);
|
|
||||||
this._changeView({
|
|
||||||
view: new View({
|
|
||||||
view: this._cameras?.get(camera)?.frigate.camera_name
|
|
||||||
? targetView
|
? targetView
|
||||||
: // Fallback to supported views for non-Frigate cameras.
|
: FRIGATE_CARD_VIEW_DEFAULT;
|
||||||
View.selectBestViewForNonFrigateCameras(targetView),
|
this._changeView({ view: new View({ view: actualView, camera: cameraID }) });
|
||||||
camera: camera,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case 'media_player':
|
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.
|
* Generate diagnostics for issue reports.
|
||||||
*/
|
*/
|
||||||
@@ -1593,18 +1407,16 @@ class FrigateCard extends LitElement {
|
|||||||
* Get the Frigate UI URL from context.
|
* Get the Frigate UI URL from context.
|
||||||
* @returns The URL or null if unavailable.
|
* @returns The URL or null if unavailable.
|
||||||
*/
|
*/
|
||||||
protected _getFrigateURLFromContext(): string | null {
|
protected _getCameraURLFromContext(): string | null {
|
||||||
const cameraConfig = this._getSelectedCameraConfig();
|
const view = this._view;
|
||||||
if (!cameraConfig || !cameraConfig.frigate.url || !this._view) {
|
const selectedCameraID = view?.camera;
|
||||||
return null;
|
const media = view?.queryResults?.getSelectedResult() ?? null;
|
||||||
}
|
return this._hass && view && selectedCameraID
|
||||||
if (!cameraConfig.frigate.camera_name) {
|
? this._cameraManager?.getCameraURL(selectedCameraID, {
|
||||||
return cameraConfig.frigate.url;
|
...(media && { media: media }),
|
||||||
}
|
...(view && { view: view.view }),
|
||||||
if (this._view.isViewerView() || this._view.isGalleryView()) {
|
}) ?? null
|
||||||
return `${cameraConfig.frigate.url}/events?camera=${cameraConfig.frigate.camera_name}`;
|
: null;
|
||||||
}
|
|
||||||
return `${cameraConfig.frigate.url}/cameras/${cameraConfig.frigate.camera_name}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -1961,23 +1773,14 @@ class FrigateCard extends LitElement {
|
|||||||
>
|
>
|
||||||
${renderMenuAbove ? this._renderMenu() : ''}
|
${renderMenuAbove ? this._renderMenu() : ''}
|
||||||
<div ${ref(this._refMain)} class="${classMap(mainClasses)}">
|
<div ${ref(this._refMain)} class="${classMap(mainClasses)}">
|
||||||
${this._cameras === undefined && !this._message
|
${!this._cameraManager?.isInitialized() && !this._message
|
||||||
? until(
|
? renderProgressIndicator({ cardWideConfig: this._cardWideConfig })
|
||||||
(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 }),
|
|
||||||
)
|
|
||||||
: // Always want to call render even if there's a message, to
|
: // Always want to call render even if there's a message, to
|
||||||
// ensure live preload is always present (even if not displayed).
|
// ensure live preload is always present (even if not displayed).
|
||||||
this._render()}
|
this._render()}
|
||||||
${
|
${
|
||||||
// Keep message rendering to last to show messages that may have
|
// Keep message rendering to last to show messages that may have been
|
||||||
// been generated during the render.
|
// generated during the render.
|
||||||
this._message ? renderMessage(this._message) : ''
|
this._message ? renderMessage(this._message) : ''
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@@ -2011,7 +1814,7 @@ class FrigateCard extends LitElement {
|
|||||||
protected _render(): TemplateResult | void {
|
protected _render(): TemplateResult | void {
|
||||||
const cameraConfig = this._getSelectedCameraConfig();
|
const cameraConfig = this._getSelectedCameraConfig();
|
||||||
|
|
||||||
if (!this._hass || !this._view || !cameraConfig || !this._cameras) {
|
if (!this._hass || !this._view || !cameraConfig) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2037,7 +1840,6 @@ class FrigateCard extends LitElement {
|
|||||||
? html` <frigate-card-gallery
|
? html` <frigate-card-gallery
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.cameras=${this._cameras}
|
|
||||||
.galleryConfig=${this._getConfig().event_gallery}
|
.galleryConfig=${this._getConfig().event_gallery}
|
||||||
.cameraManager=${this._cameraManager}
|
.cameraManager=${this._cameraManager}
|
||||||
.cardWideConfig=${this._cardWideConfig}
|
.cardWideConfig=${this._cardWideConfig}
|
||||||
@@ -2048,7 +1850,6 @@ class FrigateCard extends LitElement {
|
|||||||
? html` <frigate-card-viewer
|
? html` <frigate-card-viewer
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.cameras=${this._cameras}
|
|
||||||
.viewerConfig=${this._getConfig().media_viewer}
|
.viewerConfig=${this._getConfig().media_viewer}
|
||||||
.resolvedMediaCache=${this._resolvedMediaCache}
|
.resolvedMediaCache=${this._resolvedMediaCache}
|
||||||
.cameraManager=${this._cameraManager}
|
.cameraManager=${this._cameraManager}
|
||||||
@@ -2060,7 +1861,6 @@ class FrigateCard extends LitElement {
|
|||||||
? html` <frigate-card-timeline
|
? html` <frigate-card-timeline
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.cameras=${this._cameras}
|
|
||||||
.timelineConfig=${this._getConfig().timeline}
|
.timelineConfig=${this._getConfig().timeline}
|
||||||
.cameraManager=${this._cameraManager}
|
.cameraManager=${this._cameraManager}
|
||||||
>
|
>
|
||||||
@@ -2082,7 +1882,6 @@ class FrigateCard extends LitElement {
|
|||||||
.liveConfig=${this._config.live}
|
.liveConfig=${this._config.live}
|
||||||
.conditionState=${this._conditionState}
|
.conditionState=${this._conditionState}
|
||||||
.liveOverrides=${getOverridesByKey(this._getConfig().overrides, 'live')}
|
.liveOverrides=${getOverridesByKey(this._getConfig().overrides, 'live')}
|
||||||
.cameras=${this._cameras}
|
|
||||||
.cameraManager=${this._cameraManager}
|
.cameraManager=${this._cameraManager}
|
||||||
.cardWideConfig=${this._cardWideConfig}
|
.cardWideConfig=${this._cardWideConfig}
|
||||||
class="${classMap(liveClasses)}"
|
class="${classMap(liveClasses)}"
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
import { customElement, property, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import galleryStyle from '../scss/gallery.scss';
|
import galleryStyle from '../scss/gallery.scss';
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
|
||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
@@ -24,7 +23,7 @@ import {
|
|||||||
} from '../utils/media-to-view.js';
|
} from '../utils/media-to-view.js';
|
||||||
import { CameraManager, ExtendedMediaQueryResult } from '../camera-manager/manager.js';
|
import { CameraManager, ExtendedMediaQueryResult } from '../camera-manager/manager.js';
|
||||||
import { View } from '../view/view.js';
|
import { View } from '../view/view.js';
|
||||||
import { renderProgressIndicator } from './message.js';
|
import { dispatchMessageEvent, renderProgressIndicator } from './message.js';
|
||||||
import './thumbnail.js';
|
import './thumbnail.js';
|
||||||
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
||||||
import { createRef, Ref } from 'lit/directives/ref.js';
|
import { createRef, Ref } from 'lit/directives/ref.js';
|
||||||
@@ -36,6 +35,7 @@ import { errorToConsole } from '../utils/basic';
|
|||||||
import './media-filter';
|
import './media-filter';
|
||||||
import "./surround-basic";
|
import "./surround-basic";
|
||||||
import { ViewMedia } from '../view/media';
|
import { ViewMedia } from '../view/media';
|
||||||
|
import { localize } from '../localize/localize';
|
||||||
|
|
||||||
const GALLERY_MEDIA_CHUNK_SIZE = 100;
|
const GALLERY_MEDIA_CHUNK_SIZE = 100;
|
||||||
|
|
||||||
@@ -55,9 +55,6 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public galleryConfig?: GalleryConfig;
|
public galleryConfig?: GalleryConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameras?: Map<string, CameraConfig>;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
@@ -72,7 +69,6 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
if (
|
if (
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
!this.view ||
|
!this.view ||
|
||||||
!this.cameras ||
|
|
||||||
!this.view.isGalleryView() ||
|
!this.view.isGalleryView() ||
|
||||||
!this.cameraManager
|
!this.cameraManager
|
||||||
) {
|
) {
|
||||||
@@ -85,7 +81,6 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
this,
|
this,
|
||||||
this.hass,
|
this.hass,
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.cameras,
|
|
||||||
this.view,
|
this.view,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -98,7 +93,6 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
this,
|
this,
|
||||||
this.hass,
|
this.hass,
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.cameras,
|
|
||||||
this.view,
|
this.view,
|
||||||
{
|
{
|
||||||
...(mediaType && { mediaType: mediaType }),
|
...(mediaType && { mediaType: mediaType }),
|
||||||
@@ -120,7 +114,6 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
${this.galleryConfig && this.galleryConfig.controls.filter.mode !== 'none'
|
${this.galleryConfig && this.galleryConfig.controls.filter.mode !== 'none'
|
||||||
? html` <frigate-card-media-filter
|
? html` <frigate-card-media-filter
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.cameras=${this.cameras}
|
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.mediaLimit=${GALLERY_MEDIA_CHUNK_SIZE}
|
.mediaLimit=${GALLERY_MEDIA_CHUNK_SIZE}
|
||||||
@@ -132,7 +125,6 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.galleryConfig=${this.galleryConfig}
|
.galleryConfig=${this.galleryConfig}
|
||||||
.cameras=${this.cameras}
|
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cardWideConfig=${this.cardWideConfig}
|
.cardWideConfig=${this.cardWideConfig}
|
||||||
>
|
>
|
||||||
@@ -166,9 +158,6 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public galleryConfig?: GalleryConfig;
|
public galleryConfig?: GalleryConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameras?: Map<string, CameraConfig>;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
@@ -330,12 +319,17 @@ export class FrigateCardGalleryCore extends LitElement {
|
|||||||
!this._media ||
|
!this._media ||
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
!this.view ||
|
!this.view ||
|
||||||
!this.view.isGalleryView() ||
|
!this.view.isGalleryView()
|
||||||
!this.cameras
|
|
||||||
) {
|
) {
|
||||||
return html``;
|
return html``;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ((this.view?.queryResults?.getResultsCount() ?? 0) === 0) {
|
||||||
|
return dispatchMessageEvent(this, localize('common.no_media'), 'info', {
|
||||||
|
icon: 'mdi:multimedia',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this._media.map(
|
${this._media.map(
|
||||||
(media, index) =>
|
(media, index) =>
|
||||||
|
|||||||
+20
-23
@@ -104,9 +104,6 @@ export class FrigateCardLive extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public view?: Readonly<View>;
|
public view?: Readonly<View>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameras?: Map<string, CameraConfig>;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public liveConfig?: LiveConfig;
|
public liveConfig?: LiveConfig;
|
||||||
|
|
||||||
@@ -200,7 +197,7 @@ export class FrigateCardLive extends LitElement {
|
|||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass || !this.liveConfig || !this.cameras || !this.view) {
|
if (!this.hass || !this.liveConfig || !this.cameraManager || !this.view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -229,7 +226,6 @@ export class FrigateCardLive extends LitElement {
|
|||||||
.fetchMedia=${config.controls.thumbnails.media}
|
.fetchMedia=${config.controls.thumbnails.media}
|
||||||
.thumbnailConfig=${config.controls.thumbnails}
|
.thumbnailConfig=${config.controls.thumbnails}
|
||||||
.timelineConfig=${config.controls.timeline}
|
.timelineConfig=${config.controls.timeline}
|
||||||
.cameras=${this.cameras}
|
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.inBackground=${this._inBackground}
|
.inBackground=${this._inBackground}
|
||||||
@frigate-card:message=${(ev: CustomEvent<Message>) => {
|
@frigate-card:message=${(ev: CustomEvent<Message>) => {
|
||||||
@@ -254,7 +250,6 @@ export class FrigateCardLive extends LitElement {
|
|||||||
<frigate-card-live-carousel
|
<frigate-card-live-carousel
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.cameras=${this.cameras}
|
|
||||||
.liveConfig=${this.liveConfig}
|
.liveConfig=${this.liveConfig}
|
||||||
.inBackground=${this._inBackground}
|
.inBackground=${this._inBackground}
|
||||||
.conditionState=${this.conditionState}
|
.conditionState=${this.conditionState}
|
||||||
@@ -286,9 +281,6 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public view?: Readonly<View>;
|
public view?: Readonly<View>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameras?: Map<string, CameraConfig>;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public liveConfig?: LiveConfig;
|
public liveConfig?: LiveConfig;
|
||||||
|
|
||||||
@@ -345,10 +337,11 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _getSelectedCameraIndex(): number {
|
protected _getSelectedCameraIndex(): number {
|
||||||
if (!this.cameras || !this.view) {
|
const cameraIDs = this.cameraManager?.getCameraIDs();
|
||||||
|
if (!cameraIDs || !this.view) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
return Math.max(0, Array.from(this.cameras.keys()).indexOf(this.view.camera));
|
return Math.max(0, Array.from(cameraIDs).indexOf(this.view.camera));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -367,9 +360,10 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
* @returns A list of EmblaOptionsTypes.
|
* @returns A list of EmblaOptionsTypes.
|
||||||
*/
|
*/
|
||||||
protected _getPlugins(): EmblaCarouselPlugins {
|
protected _getPlugins(): EmblaCarouselPlugins {
|
||||||
|
const cameras = this.cameraManager?.getCameraIDs();
|
||||||
return [
|
return [
|
||||||
// Only enable wheel plugin if there is more than one camera.
|
// Only enable wheel plugin if there is more than one camera.
|
||||||
...(this.cameras && this.cameras.size > 1
|
...(cameras && cameras.size > 1
|
||||||
? [
|
? [
|
||||||
WheelGesturesPlugin({
|
WheelGesturesPlugin({
|
||||||
// Whether the carousel is vertical or horizontal, interpret y-axis wheel
|
// Whether the carousel is vertical or horizontal, interpret y-axis wheel
|
||||||
@@ -424,14 +418,15 @@ 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>] {
|
||||||
if (!this.cameras) {
|
const cameras = this.cameraManager?.getCameras();
|
||||||
|
if (!cameras) {
|
||||||
return [[], {}];
|
return [[], {}];
|
||||||
}
|
}
|
||||||
|
|
||||||
const slides: TemplateResult[] = [];
|
const slides: TemplateResult[] = [];
|
||||||
const cameraToSlide: Record<string, number> = {};
|
const cameraToSlide: Record<string, number> = {};
|
||||||
|
|
||||||
for (const [camera, cameraConfig] of this.cameras) {
|
for (const [camera, cameraConfig] of cameras) {
|
||||||
const slide = this._renderLive(camera, cameraConfig, slides.length);
|
const slide = this._renderLive(camera, cameraConfig, slides.length);
|
||||||
if (slide) {
|
if (slide) {
|
||||||
cameraToSlide[camera] = slides.length;
|
cameraToSlide[camera] = slides.length;
|
||||||
@@ -445,8 +440,9 @@ 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 {
|
||||||
if (this.cameras && ev.detail.index !== this._getSelectedCameraIndex()) {
|
const cameras = this.cameraManager?.getCameras();
|
||||||
this._setViewCameraID(Array.from(this.cameras.keys())[ev.detail.index]);
|
if (cameras && ev.detail.index !== this._getSelectedCameraIndex()) {
|
||||||
|
this._setViewCameraID(Array.from(cameras.keys())[ev.detail.index]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -534,19 +530,20 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _getCameraIDsOfNeighbors(): [string | null, string | null] {
|
protected _getCameraIDsOfNeighbors(): [string | null, string | null] {
|
||||||
if (!this.cameras || !this.view || !this.hass) {
|
const cameras = this.cameraManager?.getCameras();
|
||||||
|
if (!cameras || !this.view || !this.hass) {
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
const keys = Array.from(this.cameras.keys());
|
const keys = Array.from(cameras.keys());
|
||||||
const currentIndex = keys.indexOf(this.view.camera);
|
const currentIndex = keys.indexOf(this.view.camera);
|
||||||
|
|
||||||
if (currentIndex < 0 || this.cameras.size <= 1) {
|
if (currentIndex < 0 || cameras.size <= 1) {
|
||||||
return [null, null];
|
return [null, null];
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
keys[currentIndex > 0 ? currentIndex - 1 : this.cameras.size - 1],
|
keys[currentIndex > 0 ? currentIndex - 1 : cameras.size - 1],
|
||||||
keys[currentIndex + 1 < this.cameras.size ? currentIndex + 1 : 0],
|
keys[currentIndex + 1 < cameras.size ? currentIndex + 1 : 0],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -602,11 +599,11 @@ export class FrigateCardLiveCarousel extends LitElement {
|
|||||||
<frigate-card-media-carousel
|
<frigate-card-media-carousel
|
||||||
${ref(this._refMediaCarousel)}
|
${ref(this._refMediaCarousel)}
|
||||||
.carouselOptions=${guard(
|
.carouselOptions=${guard(
|
||||||
[this.cameras, this.liveConfig],
|
[this.cameraManager, this.liveConfig],
|
||||||
this._getOptions.bind(this),
|
this._getOptions.bind(this),
|
||||||
)}
|
)}
|
||||||
.carouselPlugins=${guard(
|
.carouselPlugins=${guard(
|
||||||
[this.cameras, this.liveConfig],
|
[this.cameraManager, this.liveConfig],
|
||||||
this._getPlugins.bind(this),
|
this._getPlugins.bind(this),
|
||||||
) as EmblaCarouselPlugins}
|
) as EmblaCarouselPlugins}
|
||||||
.label="${cameraMetadataCurrent ? `${localize('common.live')}: ${cameraMetadataCurrent.title}` : ''}"
|
.label="${cameraMetadataCurrent ? `${localize('common.live')}: ${cameraMetadataCurrent.title}` : ''}"
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
|||||||
import { DateRange } from '../camera-manager/range';
|
import { DateRange } from '../camera-manager/range';
|
||||||
import { localize } from '../localize/localize';
|
import { localize } from '../localize/localize';
|
||||||
import mediaFilterStyle from '../scss/media-filter.scss';
|
import mediaFilterStyle from '../scss/media-filter.scss';
|
||||||
import { CameraConfig } from '../types';
|
|
||||||
import { createViewForEvents, createViewForRecordings } from '../utils/media-to-view.js';
|
import { createViewForEvents, createViewForRecordings } from '../utils/media-to-view.js';
|
||||||
import { errorToConsole, formatDate, prettifyTitle } from '../utils/basic';
|
import { errorToConsole, formatDate, prettifyTitle } from '../utils/basic';
|
||||||
import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin';
|
import { ScopedRegistryHost } from '@lit-labs/scoped-registry-mixin';
|
||||||
@@ -77,9 +76,6 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameras?: Map<string, CameraConfig>;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
@@ -173,7 +169,8 @@ 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> {
|
||||||
if (!this.hass || !this.cameras || !this.cameraManager || !this.view) {
|
const cameras = this.cameraManager?.getCameras();
|
||||||
|
if (!this.hass || !cameras || !this.cameraManager || !this.view) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,7 +184,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const cameraIDs =
|
const cameraIDs =
|
||||||
getArrayValueAsSet(this._refCamera.value?.value) ?? new Set(this.cameras.keys());
|
getArrayValueAsSet(this._refCamera.value?.value) ?? new Set(cameras.keys());
|
||||||
const mediaType = this._refMediaType.value?.value as
|
const mediaType = this._refMediaType.value?.value as
|
||||||
| MediaFilterMediaType
|
| MediaFilterMediaType
|
||||||
| undefined;
|
| undefined;
|
||||||
@@ -229,20 +226,13 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
];
|
];
|
||||||
|
|
||||||
(
|
(
|
||||||
await createViewForEvents(
|
await createViewForEvents(this, this.hass, this.cameraManager, this.view, {
|
||||||
this,
|
|
||||||
this.hass,
|
|
||||||
this.cameraManager,
|
|
||||||
this.cameras,
|
|
||||||
this.view,
|
|
||||||
{
|
|
||||||
query: new EventMediaQueries(queries),
|
query: new EventMediaQueries(queries),
|
||||||
|
|
||||||
// See 'A note on views' above for these two arguments.
|
// See 'A note on views' above for these two arguments.
|
||||||
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
|
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
|
||||||
targetView: mediaType === MediaFilterMediaType.Clips ? 'clips' : 'snapshots',
|
targetView: mediaType === MediaFilterMediaType.Clips ? 'clips' : 'snapshots',
|
||||||
},
|
})
|
||||||
)
|
|
||||||
)?.dispatchChangeEvent(this);
|
)?.dispatchChangeEvent(this);
|
||||||
} else if (mediaType === MediaFilterMediaType.Recordings) {
|
} else if (mediaType === MediaFilterMediaType.Recordings) {
|
||||||
const query: RecordingQuery = {
|
const query: RecordingQuery = {
|
||||||
@@ -252,33 +242,29 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
(
|
(
|
||||||
await createViewForRecordings(
|
await createViewForRecordings(this, this.hass, this.cameraManager, this.view, {
|
||||||
this,
|
|
||||||
this.hass,
|
|
||||||
this.cameraManager,
|
|
||||||
this.cameras,
|
|
||||||
this.view,
|
|
||||||
{
|
|
||||||
query: new RecordingMediaQueries([query]),
|
query: new RecordingMediaQueries([query]),
|
||||||
|
|
||||||
// See 'A note on views' above for these two arguments.
|
// See 'A note on views' above for these two arguments.
|
||||||
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
|
...(cameraIDs.size === 1 && { targetCameraID: [...cameraIDs][0] }),
|
||||||
targetView: 'recordings',
|
targetView: 'recordings',
|
||||||
},
|
})
|
||||||
)
|
|
||||||
)?.dispatchChangeEvent(this);
|
)?.dispatchChangeEvent(this);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected willUpdate(changedProps: PropertyValues): void {
|
protected willUpdate(changedProps: PropertyValues): void {
|
||||||
if (changedProps.has('cameras') && this.cameras) {
|
if (changedProps.has('cameraManager')) {
|
||||||
this._cameraOptions = Array.from(this.cameras.keys()).map((cameraID) => ({
|
const cameras = this.cameraManager?.getCameras();
|
||||||
|
if (cameras) {
|
||||||
|
this._cameraOptions = Array.from(cameras.keys()).map((cameraID) => ({
|
||||||
value: cameraID,
|
value: cameraID,
|
||||||
label: this.hass
|
label: this.hass
|
||||||
? this.cameraManager?.getCameraMetadata(this.hass, cameraID)?.title ?? ''
|
? this.cameraManager?.getCameraMetadata(this.hass, cameraID)?.title ?? ''
|
||||||
: '',
|
: '',
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (changedProps.has('cameraManager') && this.hass && this.cameraManager) {
|
if (changedProps.has('cameraManager') && this.hass && this.cameraManager) {
|
||||||
this._mediaMetadataController = new MediaMetadataController(
|
this._mediaMetadataController = new MediaMetadataController(
|
||||||
@@ -321,7 +307,8 @@ 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();
|
||||||
if (!this.view || !queries) {
|
const cameras = this.cameraManager?.getCameras();
|
||||||
|
if (!this.view || !queries || !cameras) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -337,7 +324,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
|||||||
);
|
);
|
||||||
// Special note: If all cameras are selected, this is the same as no
|
// Special note: If all cameras are selected, this is the same as no
|
||||||
// selector at all.
|
// selector at all.
|
||||||
if (cameraIDSets.length === 1 && queries[0].cameraIDs.size !== this.cameras?.size) {
|
if (cameraIDSets.length === 1 && queries[0].cameraIDs.size !== cameras.size) {
|
||||||
cameraIDs = [...queries[0].cameraIDs];
|
cameraIDs = [...queries[0].cameraIDs];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import surroundStyle from '../scss/surround.scss';
|
import surroundStyle from '../scss/surround.scss';
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
|
||||||
ClipsOrSnapshotsOrAll,
|
ClipsOrSnapshotsOrAll,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
MiniTimelineControlConfig,
|
MiniTimelineControlConfig,
|
||||||
@@ -55,9 +54,6 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
@property({ attribute: false, hasChanged: contentsChanged })
|
||||||
public fetchMedia?: ClipsOrSnapshotsOrAll;
|
public fetchMedia?: ClipsOrSnapshotsOrAll;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameras?: Map<string, CameraConfig>;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
@@ -71,7 +67,6 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
*/
|
*/
|
||||||
protected async _fetchMedia(): Promise<void> {
|
protected async _fetchMedia(): Promise<void> {
|
||||||
if (
|
if (
|
||||||
!this.cameras ||
|
|
||||||
!this.cameraManager ||
|
!this.cameraManager ||
|
||||||
!this.fetchMedia ||
|
!this.fetchMedia ||
|
||||||
this.inBackground ||
|
this.inBackground ||
|
||||||
@@ -88,7 +83,6 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
this,
|
this,
|
||||||
this.hass,
|
this.hass,
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.cameras,
|
|
||||||
this.view,
|
this.view,
|
||||||
{
|
{
|
||||||
targetView: this.view.view,
|
targetView: this.view.view,
|
||||||
@@ -138,11 +132,12 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected _getCameraIDsForTimeline(): Set<string> | null {
|
protected _getCameraIDsForTimeline(): Set<string> | null {
|
||||||
if (!this.view || !this.cameras) {
|
const cameras = this.cameraManager?.getCameras();
|
||||||
|
if (!this.view || !cameras) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (this.view?.is('live')) {
|
if (this.view?.is('live')) {
|
||||||
return getAllDependentCameras(this.cameras, this.view.camera);
|
return getAllDependentCameras(cameras, this.view.camera);
|
||||||
}
|
}
|
||||||
if (this.view.isViewerView()) {
|
if (this.view.isViewerView()) {
|
||||||
return new Set(
|
return new Set(
|
||||||
@@ -160,7 +155,7 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass || !this.view || !this.thumbnailConfig || !this.cameras) {
|
if (!this.hass || !this.view || !this.thumbnailConfig) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -220,7 +215,6 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
slot=${this.timelineConfig.mode}
|
slot=${this.timelineConfig.mode}
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.cameras=${this.cameras}
|
|
||||||
.cameraIDs=${this._cameraIDsForTimeline}
|
.cameraIDs=${this._cameraIDsForTimeline}
|
||||||
.mini=${true}
|
.mini=${true}
|
||||||
.timelineConfig=${this.timelineConfig}
|
.timelineConfig=${this.timelineConfig}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import { classMap } from 'lit/directives/class-map.js';
|
|||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
|
import thumbnailCarouselStyle from '../scss/thumbnail-carousel.scss';
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
ThumbnailsControlConfig,
|
ThumbnailsControlConfig,
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ import {
|
|||||||
TimelineOptionsCluster,
|
TimelineOptionsCluster,
|
||||||
TimelineWindow,
|
TimelineWindow,
|
||||||
} from 'vis-timeline/esnext';
|
} from 'vis-timeline/esnext';
|
||||||
import { CAMERA_BIRDSEYE } from '../const';
|
|
||||||
import { localize } from '../localize/localize';
|
import { localize } from '../localize/localize';
|
||||||
import timelineCoreStyle from '../scss/timeline-core.scss';
|
import timelineCoreStyle from '../scss/timeline-core.scss';
|
||||||
import {
|
import {
|
||||||
@@ -165,9 +164,6 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public view?: Readonly<View>;
|
public view?: Readonly<View>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameras?: Map<string, CameraConfig>;
|
|
||||||
|
|
||||||
@property({ attribute: false, hasChanged: contentsChanged })
|
@property({ attribute: false, hasChanged: contentsChanged })
|
||||||
public timelineConfig?: TimelineCoreConfig;
|
public timelineConfig?: TimelineCoreConfig;
|
||||||
|
|
||||||
@@ -240,11 +236,12 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
protected _handleThumbnailDataRequest(request: ThumbnailDataRequestEvent): void {
|
protected _handleThumbnailDataRequest(request: ThumbnailDataRequestEvent): void {
|
||||||
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
|
||||||
|
? this.cameraManager?.getCameraConfig(media.getCameraID()) ?? undefined
|
||||||
|
: undefined;
|
||||||
|
|
||||||
request.detail.hass = this.hass;
|
request.detail.hass = this.hass;
|
||||||
request.detail.cameraConfig = media
|
request.detail.cameraConfig = cameraConfig;
|
||||||
? this.cameras?.get(media.getCameraID())
|
|
||||||
: undefined;
|
|
||||||
request.detail.cameraManager = this.cameraManager;
|
request.detail.cameraManager = this.cameraManager;
|
||||||
request.detail.media = media;
|
request.detail.media = media;
|
||||||
request.detail.view = this.view;
|
request.detail.view = this.view;
|
||||||
@@ -255,13 +252,13 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass || !this.view || !this.timelineConfig) {
|
const cameraIDs = this._getTimelineCameraIDs();
|
||||||
|
|
||||||
|
if (!this.hass || !this.view || !this.timelineConfig || !cameraIDs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const capabilities = this.cameraManager?.getAggregateCameraCapabilities(
|
|
||||||
this._getTimelineCameraIDs(),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
const capabilities = this.cameraManager?.getAggregateCameraCapabilities(cameraIDs);
|
||||||
return html` ${capabilities?.supportsTimeline
|
return html` ${capabilities?.supportsTimeline
|
||||||
? html` <div
|
? html` <div
|
||||||
@frigate-card:timeline:thumbnail-data-request=${this._handleThumbnailDataRequest.bind(
|
@frigate-card:timeline:thumbnail-data-request=${this._handleThumbnailDataRequest.bind(
|
||||||
@@ -292,7 +289,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
* Get all the keys of the cameras in scope for this timeline.
|
* Get all the keys of the cameras in scope for this timeline.
|
||||||
* @returns A set of camera ids (may be empty).
|
* @returns A set of camera ids (may be empty).
|
||||||
*/
|
*/
|
||||||
protected _getTimelineCameraIDs(): Set<string> {
|
protected _getTimelineCameraIDs(): Set<string> | null {
|
||||||
return this.cameraIDs ?? this._getAllCameraIDs();
|
return this.cameraIDs ?? this._getAllCameraIDs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -300,8 +297,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
* Get all the keys of all cameras.
|
* Get all the keys of all cameras.
|
||||||
* @returns A set of camera ids (may be empty).
|
* @returns A set of camera ids (may be empty).
|
||||||
*/
|
*/
|
||||||
protected _getAllCameraIDs(): Set<string> {
|
protected _getAllCameraIDs(): Set<string> | null {
|
||||||
return new Set(this.cameras?.keys());
|
return this.cameraManager?.getCameraIDs() ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -395,6 +392,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const results = this.view?.queryResults;
|
const results = this.view?.queryResults;
|
||||||
const media = results?.getResults();
|
const media = results?.getResults();
|
||||||
|
const cameraIDs = this._getTimelineCameraIDs();
|
||||||
if (
|
if (
|
||||||
!media ||
|
!media ||
|
||||||
!results ||
|
!results ||
|
||||||
@@ -402,7 +400,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
!this.view ||
|
!this.view ||
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
!this.cameraManager ||
|
!this.cameraManager ||
|
||||||
!this.cameraManager ||
|
!cameraIDs ||
|
||||||
// Skip range changes that do not have hammerjs pan directions associated
|
// Skip range changes that do not have hammerjs pan directions associated
|
||||||
// with them, as these outliers cause media matching issues below.
|
// with them, as these outliers cause media matching issues below.
|
||||||
!properties.event.additionalEvent
|
!properties.event.additionalEvent
|
||||||
@@ -420,7 +418,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
findClosestMediaIndex(
|
findClosestMediaIndex(
|
||||||
media,
|
media,
|
||||||
targetTime,
|
targetTime,
|
||||||
this._getTimelineCameraIDs(),
|
cameraIDs,
|
||||||
properties.event.additionalEvent === 'panright' ? 'end' : 'start',
|
properties.event.additionalEvent === 'panright' ? 'end' : 'start',
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -462,13 +460,14 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
stopEventFromActivatingCardWideActions(properties.event);
|
stopEventFromActivatingCardWideActions(properties.event);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const timelineCameraIDs = this._getTimelineCameraIDs();
|
||||||
if (
|
if (
|
||||||
this._ignoreClick ||
|
this._ignoreClick ||
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
!this._timeline ||
|
!this._timeline ||
|
||||||
!this.cameras ||
|
|
||||||
!this.view ||
|
!this.view ||
|
||||||
!this.cameraManager ||
|
!this.cameraManager ||
|
||||||
|
!timelineCameraIDs ||
|
||||||
!properties.what
|
!properties.what
|
||||||
) {
|
) {
|
||||||
return;
|
return;
|
||||||
@@ -484,7 +483,6 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
this,
|
this,
|
||||||
this.hass,
|
this.hass,
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.cameras,
|
|
||||||
this.view,
|
this.view,
|
||||||
{
|
{
|
||||||
targetTime:
|
targetTime:
|
||||||
@@ -501,10 +499,9 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
this,
|
this,
|
||||||
this.hass,
|
this.hass,
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.cameras,
|
|
||||||
this.view,
|
this.view,
|
||||||
{
|
{
|
||||||
cameraIDs: this._getAllCameraIDs(),
|
cameraIDs: timelineCameraIDs,
|
||||||
start: startOfHour(properties.time),
|
start: startOfHour(properties.time),
|
||||||
end: endOfHour(properties.time),
|
end: endOfHour(properties.time),
|
||||||
targetTime: properties.time,
|
targetTime: properties.time,
|
||||||
@@ -514,9 +511,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
const newResults = this.view.queryResults
|
const newResults = this.view.queryResults
|
||||||
?.clone()
|
?.clone()
|
||||||
.resetSelectedResult()
|
.resetSelectedResult()
|
||||||
.selectResultIfFound(
|
.selectResultIfFound((media) => media.getID() === properties.item);
|
||||||
(media) => !!this.cameras && media.getID() === properties.item,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!newResults || !newResults.hasSelectedResult()) {
|
if (!newResults || !newResults.hasSelectedResult()) {
|
||||||
// This can happen if this is a recording query (with recorded hours)
|
// This can happen if this is a recording query (with recorded hours)
|
||||||
@@ -588,7 +583,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
}
|
}
|
||||||
this._removeTargetBar();
|
this._removeTargetBar();
|
||||||
|
|
||||||
if (!this.hass || !this.cameras) {
|
if (!this.hass) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -646,14 +641,13 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
selectedItem?: IdType;
|
selectedItem?: IdType;
|
||||||
},
|
},
|
||||||
): Promise<View | null> {
|
): Promise<View | null> {
|
||||||
if (!this.hass || !this.cameraManager || !this.cameras || !this.view || !query) {
|
if (!this.hass || !this.cameraManager || !this.view || !query) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const view = await createViewForEvents(
|
const view = await createViewForEvents(
|
||||||
this,
|
this,
|
||||||
this.hass,
|
this.hass,
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.cameras,
|
|
||||||
this.view,
|
this.view,
|
||||||
{
|
{
|
||||||
query: query,
|
query: query,
|
||||||
@@ -666,7 +660,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
}
|
}
|
||||||
if (options?.selectedItem) {
|
if (options?.selectedItem) {
|
||||||
view.queryResults?.selectResultIfFound(
|
view.queryResults?.selectResultIfFound(
|
||||||
(media) => !!this.cameras && media.getID() === options.selectedItem,
|
(media) => media.getID() === options.selectedItem,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
// If not asked to select a new item, persist the currently selected item
|
// If not asked to select a new item, persist the currently selected item
|
||||||
@@ -687,10 +681,8 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
*/
|
*/
|
||||||
protected _getGroups(): DataGroupCollectionType {
|
protected _getGroups(): DataGroupCollectionType {
|
||||||
const groups: FrigateCardGroupData[] = [];
|
const groups: FrigateCardGroupData[] = [];
|
||||||
|
(this._getTimelineCameraIDs() ?? []).forEach((cameraID) => {
|
||||||
this._getTimelineCameraIDs().forEach((cameraID) => {
|
if (!this.hass || !this.cameraManager) {
|
||||||
const cameraConfig = this.cameras?.get(cameraID);
|
|
||||||
if (!this.hass || !cameraConfig || !this.cameraManager) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const cameraMetadata = this.cameraManager.getCameraMetadata(this.hass, cameraID);
|
const cameraMetadata = this.cameraManager.getCameraMetadata(this.hass, cameraID);
|
||||||
@@ -806,10 +798,6 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
maxItems: this.timelineConfig.clustering_threshold,
|
maxItems: this.timelineConfig.clustering_threshold,
|
||||||
|
|
||||||
clusterCriteria: (first: TimelineItem, second: TimelineItem): boolean => {
|
clusterCriteria: (first: TimelineItem, second: TimelineItem): boolean => {
|
||||||
if (!this.cameras) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const media = this.view?.queryResults?.getSelectedResult();
|
const media = this.view?.queryResults?.getSelectedResult();
|
||||||
const selectedId = media?.getID();
|
const selectedId = media?.getID();
|
||||||
const firstMedia = (<FrigateCardTimelineItem>first).media;
|
const firstMedia = (<FrigateCardTimelineItem>first).media;
|
||||||
@@ -866,7 +854,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
*/
|
*/
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
protected shouldUpdate(_changedProps: PropertyValues): boolean {
|
protected shouldUpdate(_changedProps: PropertyValues): boolean {
|
||||||
return !!this.hass && !!this.cameras && this.cameras.size > 0;
|
return !!this.hass && !!this.cameraManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -875,7 +863,6 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
protected async _updateTimelineFromView(): Promise<void> {
|
protected async _updateTimelineFromView(): Promise<void> {
|
||||||
if (
|
if (
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
!this.cameras ||
|
|
||||||
!this.view ||
|
!this.view ||
|
||||||
!this.timelineConfig ||
|
!this.timelineConfig ||
|
||||||
!this._timelineSource ||
|
!this._timelineSource ||
|
||||||
@@ -1040,10 +1027,11 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
changedProps.has('timelineConfig') ||
|
changedProps.has('timelineConfig') ||
|
||||||
changedProps.has('cameraIDs')
|
changedProps.has('cameraIDs')
|
||||||
) {
|
) {
|
||||||
if (this.cameraManager && this.cameras && this.timelineConfig) {
|
const cameraIDs = this._getTimelineCameraIDs();
|
||||||
|
if (cameraIDs && this.cameraManager && this.timelineConfig) {
|
||||||
this._timelineSource = new TimelineDataSource(
|
this._timelineSource = new TimelineDataSource(
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this._getTimelineCameraIDs(),
|
cameraIDs,
|
||||||
this.timelineConfig.media,
|
this.timelineConfig.media,
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
import { CSSResultGroup, html, LitElement, TemplateResult, unsafeCSS } from 'lit';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
import timelineStyle from '../scss/timeline.scss';
|
import timelineStyle from '../scss/timeline.scss';
|
||||||
import { CameraConfig, ExtendedHomeAssistant, TimelineConfig } from '../types';
|
import { ExtendedHomeAssistant, TimelineConfig } from '../types';
|
||||||
import { CameraManager } from '../camera-manager/manager';
|
import { CameraManager } from '../camera-manager/manager';
|
||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
import './surround.js';
|
import './surround.js';
|
||||||
@@ -20,9 +20,6 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public view?: Readonly<View>;
|
public view?: Readonly<View>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameras?: Map<string, CameraConfig>;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public timelineConfig?: TimelineConfig;
|
public timelineConfig?: TimelineConfig;
|
||||||
|
|
||||||
@@ -43,12 +40,10 @@ export class FrigateCardTimeline extends LitElement {
|
|||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.thumbnailConfig=${this.timelineConfig.controls.thumbnails}
|
.thumbnailConfig=${this.timelineConfig.controls.thumbnails}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cameras=${this.cameras}
|
|
||||||
>
|
>
|
||||||
<frigate-card-timeline-core
|
<frigate-card-timeline-core
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.cameras=${this.cameras}
|
|
||||||
.timelineConfig=${this.timelineConfig}
|
.timelineConfig=${this.timelineConfig}
|
||||||
.thumbnailDetails=${this.timelineConfig.controls.thumbnails.show_details}
|
.thumbnailDetails=${this.timelineConfig.controls.thumbnails.show_details}
|
||||||
.thumbnailSize=${this.timelineConfig.controls.thumbnails.size}
|
.thumbnailSize=${this.timelineConfig.controls.thumbnails.size}
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import { dispatchMessageEvent, renderProgressIndicator } from '../components/mes
|
|||||||
import viewerStyle from '../scss/viewer.scss';
|
import viewerStyle from '../scss/viewer.scss';
|
||||||
import viewerCarouselStyle from '../scss/viewer-carousel.scss';
|
import viewerCarouselStyle from '../scss/viewer-carousel.scss';
|
||||||
import {
|
import {
|
||||||
CameraConfig,
|
|
||||||
CardWideConfig,
|
CardWideConfig,
|
||||||
ExtendedHomeAssistant,
|
ExtendedHomeAssistant,
|
||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
@@ -78,9 +77,6 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public viewerConfig?: ViewerConfig;
|
public viewerConfig?: ViewerConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameras?: Map<string, CameraConfig>;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public resolvedMediaCache?: ResolvedMediaCache;
|
public resolvedMediaCache?: ResolvedMediaCache;
|
||||||
|
|
||||||
@@ -98,7 +94,6 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
if (
|
if (
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
!this.view ||
|
!this.view ||
|
||||||
!this.cameras ||
|
|
||||||
!this.viewerConfig ||
|
!this.viewerConfig ||
|
||||||
!this.cameraManager
|
!this.cameraManager
|
||||||
) {
|
) {
|
||||||
@@ -120,7 +115,6 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
this,
|
this,
|
||||||
this.hass,
|
this.hass,
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.cameras,
|
|
||||||
this.view,
|
this.view,
|
||||||
{
|
{
|
||||||
targetView: 'recording',
|
targetView: 'recording',
|
||||||
@@ -131,7 +125,6 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
this,
|
this,
|
||||||
this.hass,
|
this.hass,
|
||||||
this.cameraManager,
|
this.cameraManager,
|
||||||
this.cameras,
|
|
||||||
this.view,
|
this.view,
|
||||||
{
|
{
|
||||||
targetView: 'media',
|
targetView: 'media',
|
||||||
@@ -148,12 +141,10 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
.thumbnailConfig=${this.viewerConfig.controls.thumbnails}
|
.thumbnailConfig=${this.viewerConfig.controls.thumbnails}
|
||||||
.timelineConfig=${this.viewerConfig.controls.timeline}
|
.timelineConfig=${this.viewerConfig.controls.timeline}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
.cameras=${this.cameras}
|
|
||||||
>
|
>
|
||||||
<frigate-card-viewer-carousel
|
<frigate-card-viewer-carousel
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.cameras=${this.cameras}
|
|
||||||
.viewerConfig=${this.viewerConfig}
|
.viewerConfig=${this.viewerConfig}
|
||||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||||
.cameraManager=${this.cameraManager}
|
.cameraManager=${this.cameraManager}
|
||||||
@@ -195,9 +186,6 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cardWideConfig?: CardWideConfig;
|
public cardWideConfig?: CardWideConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
|
||||||
public cameras?: Map<string, CameraConfig>;
|
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
public cameraManager?: CameraManager;
|
public cameraManager?: CameraManager;
|
||||||
|
|
||||||
@@ -205,19 +193,17 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
|
|
||||||
// A task to resolve target media if lazy loading is disabled.
|
// A task to resolve target media if lazy loading is disabled.
|
||||||
protected _mediaResolutionTask = new Task<
|
protected _mediaResolutionTask = new Task<
|
||||||
[ViewerConfig | undefined, Map<string, CameraConfig> | undefined, View | undefined],
|
[ViewerConfig | undefined, View | undefined],
|
||||||
void
|
void
|
||||||
>(
|
>(
|
||||||
this,
|
this,
|
||||||
async ([viewerConfig, cameras, view]: [
|
async ([viewerConfig, view]: [
|
||||||
ViewerConfig | undefined,
|
ViewerConfig | undefined,
|
||||||
Map<string, CameraConfig> | undefined,
|
|
||||||
View | undefined,
|
View | undefined,
|
||||||
]): Promise<void> => {
|
]): Promise<void> => {
|
||||||
if (
|
if (
|
||||||
!this.hass ||
|
!this.hass ||
|
||||||
!viewerConfig?.lazy_load ||
|
!viewerConfig?.lazy_load ||
|
||||||
!cameras ||
|
|
||||||
!view ||
|
!view ||
|
||||||
!view.queryResults?.hasResults()
|
!view.queryResults?.hasResults()
|
||||||
) {
|
) {
|
||||||
@@ -234,7 +220,7 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
});
|
});
|
||||||
await Promise.all(promises);
|
await Promise.all(promises);
|
||||||
},
|
},
|
||||||
() => [this.viewerConfig, this.cameras, this.view],
|
() => [this.viewerConfig, this.view],
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -453,7 +439,7 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
* @param slide The slide to lazy load.
|
* @param slide The slide to lazy load.
|
||||||
*/
|
*/
|
||||||
protected _lazyloadSlide(index: number, slide: HTMLElement): void {
|
protected _lazyloadSlide(index: number, slide: HTMLElement): void {
|
||||||
if (!this.hass || !this.view || !this.view.query || !this.cameras) {
|
if (!this.hass || !this.view || !this.view.query) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -512,7 +498,7 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
* Determine if all the media in the carousel are resolved.
|
* Determine if all the media in the carousel are resolved.
|
||||||
*/
|
*/
|
||||||
protected _isMediaFullyResolved(): boolean {
|
protected _isMediaFullyResolved(): boolean {
|
||||||
if (!this.resolvedMediaCache || !this.cameras) {
|
if (!this.resolvedMediaCache) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (const media of this.view?.queryResults?.getResults() ?? []) {
|
for (const media of this.view?.queryResults?.getResults() ?? []) {
|
||||||
@@ -560,7 +546,7 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const media = this.view?.queryResults?.getSelectedResult();
|
const media = this.view?.queryResults?.getSelectedResult();
|
||||||
if (!media || !this.cameras || !this.view || !this.view.queryResults) {
|
if (!media || !this.view || !this.view.queryResults) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,7 +635,7 @@ export class FrigateCardViewerCarousel extends LitElement {
|
|||||||
*/
|
*/
|
||||||
protected _renderMediaItem(media: ViewMedia, index: number): TemplateResult | null {
|
protected _renderMediaItem(media: ViewMedia, index: number): TemplateResult | null {
|
||||||
// Skip folders as they cannot be rendered by this viewer.
|
// Skip folders as they cannot be rendered by this viewer.
|
||||||
if (!this.hass || !this.view || !this.viewerConfig || !this.cameras) {
|
if (!this.hass || !this.view || !this.viewerConfig) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -354,6 +354,8 @@
|
|||||||
"jsmpeg_no_sign": "Could not retrieve or sign JSMPEG websocket path",
|
"jsmpeg_no_sign": "Could not retrieve or sign JSMPEG websocket path",
|
||||||
"live_camera_not_found": "The configured camera_entity was not found",
|
"live_camera_not_found": "The configured camera_entity was not found",
|
||||||
"live_camera_unavailable": "Camera unavailable",
|
"live_camera_unavailable": "Camera unavailable",
|
||||||
|
"no_camera_engine": "Could not determine suitable engine for camera",
|
||||||
|
"no_camera_entity": "Could not find camera entity",
|
||||||
"no_camera_id": "Could not determine camera id for the following camera, may need to set 'id' parameter manually",
|
"no_camera_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_cameras": "No valid cameras found, you must configure at least one camera entry",
|
||||||
|
|||||||
@@ -325,6 +325,8 @@
|
|||||||
"jsmpeg_no_sign": "Impossibile recuperare o firmare il percorso WebSocket JSMPEG",
|
"jsmpeg_no_sign": "Impossibile recuperare o firmare il percorso WebSocket JSMPEG",
|
||||||
"live_camera_not_found": "La telecamera configurata non è stata trovata",
|
"live_camera_not_found": "La telecamera configurata non è stata trovata",
|
||||||
"live_camera_unavailable": "Telecamera non disponibile",
|
"live_camera_unavailable": "Telecamera non disponibile",
|
||||||
|
"no_camera_engine": "",
|
||||||
|
"no_camera_entity": "",
|
||||||
"no_camera_id": "Impossibile determinare l'ID della telecamera , potrebbe essere necessario impostare manualmente il parametro 'ID'",
|
"no_camera_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_cameras": "Nessuna telecamera valida trovata, è necessario configurare almeno una voce della telecamera",
|
||||||
|
|||||||
@@ -325,6 +325,8 @@
|
|||||||
"jsmpeg_no_sign": "Não foi possível recuperar ou assinar o caminho do websocket JSMPEG",
|
"jsmpeg_no_sign": "Não foi possível recuperar ou assinar o caminho do websocket JSMPEG",
|
||||||
"live_camera_not_found": "",
|
"live_camera_not_found": "",
|
||||||
"live_camera_unavailable": "",
|
"live_camera_unavailable": "",
|
||||||
|
"no_camera_engine": "",
|
||||||
|
"no_camera_entity": "",
|
||||||
"no_camera_id": "Não foi possível determinar o ID da câmera para a câmera a seguir, pode ser necessário definir o parâmetro 'id' manualmente",
|
"no_camera_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_cameras": "Nenhuma câmera válida encontrada, você deve configurar pelo menos uma câmera",
|
||||||
|
|||||||
+13
-20
@@ -49,8 +49,6 @@ const FRIGATE_CARD_VIEWS = [
|
|||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number];
|
export type FrigateCardView = typeof FRIGATE_CARD_VIEWS[number];
|
||||||
export type FrigateCardUserSpecifiedView =
|
|
||||||
typeof FRIGATE_CARD_VIEWS_USER_SPECIFIED[number];
|
|
||||||
export const FRIGATE_CARD_VIEW_DEFAULT = 'live' as const;
|
export const FRIGATE_CARD_VIEW_DEFAULT = 'live' as const;
|
||||||
|
|
||||||
const FRIGATE_MENU_STYLES = [
|
const FRIGATE_MENU_STYLES = [
|
||||||
@@ -89,6 +87,12 @@ const MEDIA_ACTION_POSITIVE_CONDITIONS = [
|
|||||||
export type AutoUnmuteCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
|
export type AutoUnmuteCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
|
||||||
export type AutoPlayCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
|
export type AutoPlayCondition = typeof MEDIA_ACTION_POSITIVE_CONDITIONS[number];
|
||||||
|
|
||||||
|
const ENGINES = [
|
||||||
|
'auto',
|
||||||
|
'frigate',
|
||||||
|
'generic',
|
||||||
|
] as const;
|
||||||
|
|
||||||
export class FrigateCardError extends Error {
|
export class FrigateCardError extends Error {
|
||||||
context?: unknown;
|
context?: unknown;
|
||||||
|
|
||||||
@@ -382,6 +386,7 @@ const customSchema = z
|
|||||||
*/
|
*/
|
||||||
const cameraConfigDefault = {
|
const cameraConfigDefault = {
|
||||||
live_provider: 'auto' as const,
|
live_provider: 'auto' as const,
|
||||||
|
engine: 'auto' as const,
|
||||||
frigate: {
|
frigate: {
|
||||||
client_id: 'frigate' as const,
|
client_id: 'frigate' as const,
|
||||||
},
|
},
|
||||||
@@ -413,6 +418,8 @@ const cameraConfigSchema = z
|
|||||||
// this card.
|
// this card.
|
||||||
id: z.string().optional(),
|
id: z.string().optional(),
|
||||||
|
|
||||||
|
engine: z.enum(ENGINES).default('auto'),
|
||||||
|
|
||||||
frigate: z
|
frigate: z
|
||||||
.object({
|
.object({
|
||||||
// No URL validation to allow relative URLs within HA (e.g. Frigate addon).
|
// No URL validation to allow relative URLs within HA (e.g. Frigate addon).
|
||||||
@@ -445,6 +452,9 @@ const cameraConfigSchema = z
|
|||||||
.default(cameraConfigDefault);
|
.default(cameraConfigDefault);
|
||||||
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
||||||
|
|
||||||
|
const camerasConfigSchema = cameraConfigSchema.array().nonempty();
|
||||||
|
export type CamerasConfig = z.infer<typeof camerasConfigSchema>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Custom Element Types.
|
* Custom Element Types.
|
||||||
*/
|
*/
|
||||||
@@ -1234,7 +1244,7 @@ export interface CardWideConfig {
|
|||||||
*/
|
*/
|
||||||
export const frigateCardConfigSchema = z.object({
|
export const frigateCardConfigSchema = z.object({
|
||||||
// Main configuration sections.
|
// Main configuration sections.
|
||||||
cameras: cameraConfigSchema.array().nonempty(),
|
cameras: camerasConfigSchema,
|
||||||
view: viewConfigSchema,
|
view: viewConfigSchema,
|
||||||
menu: menuConfigSchema,
|
menu: menuConfigSchema,
|
||||||
live: liveConfigSchema,
|
live: liveConfigSchema,
|
||||||
@@ -1351,20 +1361,3 @@ export const signedPathSchema = z.object({
|
|||||||
path: z.string(),
|
path: z.string(),
|
||||||
});
|
});
|
||||||
export type SignedPath = z.infer<typeof signedPathSchema>;
|
export type SignedPath = z.infer<typeof signedPathSchema>;
|
||||||
|
|
||||||
const entitySchema = z.object({
|
|
||||||
config_entry_id: z.string().nullable(),
|
|
||||||
disabled_by: z.string().nullable(),
|
|
||||||
entity_id: z.string(),
|
|
||||||
platform: z.string(),
|
|
||||||
});
|
|
||||||
export type Entity = z.infer<typeof entitySchema>;
|
|
||||||
|
|
||||||
export const extendedEntitySchema = entitySchema.extend({
|
|
||||||
// Extended entity results.
|
|
||||||
unique_id: z.string().optional(),
|
|
||||||
});
|
|
||||||
export type ExtendedEntity = z.infer<typeof extendedEntitySchema>;
|
|
||||||
|
|
||||||
export const entityListSchema = entitySchema.array();
|
|
||||||
export type EntityList = z.infer<typeof entityListSchema>;
|
|
||||||
|
|||||||
+5
-5
@@ -167,11 +167,11 @@ export function getDurationString(start: Date, end: Date): string {
|
|||||||
return duration;
|
return duration;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const allPromises = async <T>(
|
export const allPromises = async <T, R>(
|
||||||
items: T[],
|
items: Iterable<T>,
|
||||||
func: (arg: T) => void,
|
func: (arg: T) => R,
|
||||||
): Promise<void> => {
|
): Promise<Awaited<R>[]> => {
|
||||||
await Promise.all(Array.from(items).map((item) => func(item)));
|
return await Promise.all(Array.from(items).map((item) => func(item)));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+1
-1
@@ -26,7 +26,7 @@ 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 cameras Cameras map.
|
||||||
* @param camera Name of the target camera.
|
* @param cameraID ID of the target camera.
|
||||||
* @returns A set of query parameters.
|
* @returns A set of query parameters.
|
||||||
*/
|
*/
|
||||||
export const getAllDependentCameras = (
|
export const getAllDependentCameras = (
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
|
||||||
import { homeAssistantWSRequest } from '.';
|
|
||||||
import {
|
|
||||||
Entity,
|
|
||||||
EntityList,
|
|
||||||
entityListSchema,
|
|
||||||
ExtendedEntity,
|
|
||||||
extendedEntitySchema,
|
|
||||||
} from '../../types.js';
|
|
||||||
|
|
||||||
export class ExtendedEntityCache {
|
|
||||||
protected _cache: Map<string, ExtendedEntity> = new Map();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Determine if the cache has a given entity_id.
|
|
||||||
* @param id
|
|
||||||
* @returns `true` if the id is in the cache, `false` otherwise.
|
|
||||||
*/
|
|
||||||
public has(id: string): boolean {
|
|
||||||
return this._cache.has(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the first value that returns true for the given predicate.
|
|
||||||
* @param func A callback function that returns a boolean.
|
|
||||||
* @returns The first matching value.
|
|
||||||
*/
|
|
||||||
public getMatch(func: (arg: ExtendedEntity) => boolean): ExtendedEntity | null {
|
|
||||||
return [...this._cache.values()].find(func) ?? null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get entity information given an id.
|
|
||||||
* @param id The entity id.
|
|
||||||
* @returns The `ExtendedEntity` for this id.
|
|
||||||
*/
|
|
||||||
public get(id: string): ExtendedEntity | undefined {
|
|
||||||
return this._cache.get(id);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Add a given ExtendedEntity to the cache.
|
|
||||||
* @param extendedEntity
|
|
||||||
*/
|
|
||||||
public set(extendedEntity: ExtendedEntity): void {
|
|
||||||
this._cache.set(extendedEntity.entity_id, extendedEntity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the extended entity information for an entity. May throw.
|
|
||||||
* @param hass The Home Assistant object.
|
|
||||||
* @param entity The entity id.
|
|
||||||
* @param cache An optional ExtendedEntityCache.
|
|
||||||
* @returns The ExtendedEntity information.
|
|
||||||
*/
|
|
||||||
export const getExtendedEntity = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entity: string,
|
|
||||||
cache?: ExtendedEntityCache,
|
|
||||||
): Promise<ExtendedEntity> => {
|
|
||||||
const cachedValue = cache ? cache.get(entity) : undefined;
|
|
||||||
if (cachedValue) {
|
|
||||||
return cachedValue;
|
|
||||||
}
|
|
||||||
const result = await homeAssistantWSRequest<ExtendedEntity>(
|
|
||||||
hass,
|
|
||||||
extendedEntitySchema,
|
|
||||||
{
|
|
||||||
type: 'config/entity_registry/get',
|
|
||||||
entity_id: entity,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
if (cache) {
|
|
||||||
cache.set(result);
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the extended entity information for an array of entities. May throw.
|
|
||||||
* @param hass The Home Assistant object.
|
|
||||||
* @param entities An array of entity ids.
|
|
||||||
* @param cache An optional ExtendedEntityCache.
|
|
||||||
* @returns A map of entity id to ExtendedEntity objects.
|
|
||||||
*/
|
|
||||||
export const getExtendedEntities = async (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entities: string[],
|
|
||||||
cache?: ExtendedEntityCache,
|
|
||||||
): Promise<Map<string, Entity>> => {
|
|
||||||
const output: Map<string, Entity> = new Map();
|
|
||||||
const _storeExtendedEntity = async (entity: string): Promise<void> => {
|
|
||||||
output.set(entity, await getExtendedEntity(hass, entity, cache));
|
|
||||||
};
|
|
||||||
await Promise.all(entities.map(_storeExtendedEntity));
|
|
||||||
return output;
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a list of all entities from the entity registry. May throw.
|
|
||||||
* @param hass The Home Assistant object.
|
|
||||||
* @returns An entity list object.
|
|
||||||
*/
|
|
||||||
export const getAllEntities = async (hass: HomeAssistant): Promise<EntityList> => {
|
|
||||||
return await homeAssistantWSRequest<EntityList>(hass, entityListSchema, {
|
|
||||||
type: 'config/entity_registry/list',
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { Entity, ExtendedEntity } from './types.js';
|
||||||
|
|
||||||
|
export class EntityCache<T extends Entity | ExtendedEntity> {
|
||||||
|
protected _cache: Map<string, T> = new Map();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if the cache has a given entity_id.
|
||||||
|
* @param id
|
||||||
|
* @returns `true` if the id is in the cache, `false` otherwise.
|
||||||
|
*/
|
||||||
|
public has(id: string): boolean {
|
||||||
|
return this._cache.has(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the first value that returns true for the given predicate.
|
||||||
|
* @param func A callback function that returns a boolean.
|
||||||
|
* @returns The first matching value.
|
||||||
|
*/
|
||||||
|
// public getFirstMatch(func: (arg: T) => boolean): T | null {
|
||||||
|
// return [...this._cache.values()].find(func) ?? null;
|
||||||
|
// }
|
||||||
|
|
||||||
|
public getMatches(func: (arg: T) => boolean): T[] {
|
||||||
|
return [...this._cache.values()].filter(func);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get entity information given an id.
|
||||||
|
* @param id The entity id.
|
||||||
|
* @returns The `ExtendedEntity` for this id.
|
||||||
|
*/
|
||||||
|
public get(id: string): T | undefined {
|
||||||
|
return this._cache.get(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a given entity to the cache.
|
||||||
|
* @param extendedEntity
|
||||||
|
*/
|
||||||
|
public set(input: T | T[]): void {
|
||||||
|
const _set = (entity: T) => this._cache.set(entity.entity_id, entity);
|
||||||
|
|
||||||
|
if (Array.isArray(input)) {
|
||||||
|
input.forEach(_set);
|
||||||
|
} else {
|
||||||
|
_set(input);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
import { homeAssistantWSRequest } from '..';
|
||||||
|
import { EntityCache } from './cache';
|
||||||
|
import {
|
||||||
|
Entity,
|
||||||
|
EntityList,
|
||||||
|
entityListSchema,
|
||||||
|
ExtendedEntity,
|
||||||
|
extendedEntitySchema,
|
||||||
|
} from './types.js';
|
||||||
|
|
||||||
|
type EntityRegistryCache = EntityCache<Entity>;
|
||||||
|
type ExtendedEntityRegistryCache = EntityCache<ExtendedEntity>;
|
||||||
|
|
||||||
|
// Tne `entity_registry/list` call returns a smaller set of information for
|
||||||
|
// every entity, than the full `entity_registry/get` call returns for a single
|
||||||
|
// entity. This class manages interactions with entities, caching results
|
||||||
|
// (either the partial or extended versions) and fetching as necessary. Some
|
||||||
|
// calls require every entity to be fetched, which may be non-trivial in size
|
||||||
|
// (after which it is cached forever).
|
||||||
|
|
||||||
|
export class EntityRegistryManager {
|
||||||
|
protected _cache: EntityRegistryCache;
|
||||||
|
protected _extendedCache: ExtendedEntityRegistryCache;
|
||||||
|
protected _fetchedEntityList = false;
|
||||||
|
|
||||||
|
constructor(cache: EntityCache<Entity>, extendedCache: EntityCache<ExtendedEntity>) {
|
||||||
|
this._cache = cache;
|
||||||
|
this._extendedCache = extendedCache;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getEntity(hass: HomeAssistant, entityID: string): Promise<Entity | null> {
|
||||||
|
const cachedEntity = this._cache.get(entityID);
|
||||||
|
if (cachedEntity) {
|
||||||
|
return cachedEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cachedExtendedEntity = this._extendedCache.get(entityID);
|
||||||
|
if (cachedExtendedEntity) {
|
||||||
|
return cachedExtendedEntity;
|
||||||
|
}
|
||||||
|
return await this.getExtendedEntity(hass, entityID);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getMatchingEntities(hass: HomeAssistant, func: (arg: Entity) => boolean): Promise<Entity[]> {
|
||||||
|
await this.fetchEntityList(hass);
|
||||||
|
return this._cache.getMatches(func);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getExtendedEntity(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityID: string,
|
||||||
|
): Promise<ExtendedEntity | null> {
|
||||||
|
const cachedValue = this._extendedCache.get(entityID);
|
||||||
|
if (cachedValue) {
|
||||||
|
return cachedValue;
|
||||||
|
}
|
||||||
|
const extendedEntity = await homeAssistantWSRequest<ExtendedEntity>(
|
||||||
|
hass,
|
||||||
|
extendedEntitySchema,
|
||||||
|
{
|
||||||
|
type: 'config/entity_registry/get',
|
||||||
|
entity_id: entityID,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
this._extendedCache.set(extendedEntity);
|
||||||
|
return extendedEntity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async getExtendedEntities(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityIDs: string[],
|
||||||
|
): Promise<Map<string, ExtendedEntity>> {
|
||||||
|
const output: Map<string, ExtendedEntity> = new Map();
|
||||||
|
const _storeExtendedEntity = async (entityID: string): Promise<void> => {
|
||||||
|
const extendedEntity = await this.getExtendedEntity(hass, entityID);
|
||||||
|
if (extendedEntity) {
|
||||||
|
output.set(entityID, extendedEntity);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
await Promise.all(entityIDs.map(_storeExtendedEntity));
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async fetchEntityList(hass: HomeAssistant): Promise<void> {
|
||||||
|
if (this._fetchedEntityList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entityList = await homeAssistantWSRequest<EntityList>(hass, entityListSchema, {
|
||||||
|
type: 'config/entity_registry/list',
|
||||||
|
});
|
||||||
|
this._cache.set(entityList);
|
||||||
|
this._fetchedEntityList = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const entitySchema = z.object({
|
||||||
|
config_entry_id: z.string().nullable(),
|
||||||
|
disabled_by: z.string().nullable(),
|
||||||
|
entity_id: z.string(),
|
||||||
|
hidden_by: z.string().nullable(),
|
||||||
|
platform: z.string(),
|
||||||
|
});
|
||||||
|
export type Entity = z.infer<typeof entitySchema>;
|
||||||
|
|
||||||
|
export const extendedEntitySchema = entitySchema.extend({
|
||||||
|
// Extended entity results.
|
||||||
|
unique_id: z.string().optional(),
|
||||||
|
});
|
||||||
|
export type ExtendedEntity = z.infer<typeof extendedEntitySchema>;
|
||||||
|
|
||||||
|
export const entityListSchema = entitySchema.array();
|
||||||
|
export type EntityList = z.infer<typeof entityListSchema>;
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import add from 'date-fns/add';
|
import add from 'date-fns/add';
|
||||||
import sub from 'date-fns/sub';
|
import sub from 'date-fns/sub';
|
||||||
import { ViewContext } from 'view';
|
import { ViewContext } from 'view';
|
||||||
import { CameraConfig, ClipsOrSnapshotsOrAll, FrigateCardView } from '../types';
|
import { ClipsOrSnapshotsOrAll, FrigateCardView } from '../types';
|
||||||
import { View } from '../view/view';
|
import { View } from '../view/view';
|
||||||
import {
|
import {
|
||||||
EventMediaQueries,
|
EventMediaQueries,
|
||||||
@@ -21,7 +21,6 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
|
|||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameraManager: CameraManager,
|
cameraManager: CameraManager,
|
||||||
cameras: Map<string, CameraConfig>,
|
|
||||||
view: View,
|
view: View,
|
||||||
options?: {
|
options?: {
|
||||||
mediaType?: ClipsOrSnapshotsOrAll;
|
mediaType?: ClipsOrSnapshotsOrAll;
|
||||||
@@ -29,7 +28,7 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
|
|||||||
},
|
},
|
||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
(
|
(
|
||||||
await createViewForEvents(element, hass, cameraManager, cameras, view, {
|
await createViewForEvents(element, hass, cameraManager, view, {
|
||||||
...options,
|
...options,
|
||||||
limit: 50, // Capture the 50 most recent events.
|
limit: 50, // Capture the 50 most recent events.
|
||||||
})
|
})
|
||||||
@@ -40,7 +39,6 @@ export const createViewForEvents = async (
|
|||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameraManager: CameraManager,
|
cameraManager: CameraManager,
|
||||||
cameras: Map<string, CameraConfig>,
|
|
||||||
view: View,
|
view: View,
|
||||||
options?: {
|
options?: {
|
||||||
query?: EventMediaQueries;
|
query?: EventMediaQueries;
|
||||||
@@ -51,6 +49,10 @@ export const createViewForEvents = async (
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
},
|
},
|
||||||
): Promise<View | null> => {
|
): Promise<View | null> => {
|
||||||
|
const cameras = cameraManager.getCameras();
|
||||||
|
if (!cameras) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
let query: EventMediaQueries;
|
let query: EventMediaQueries;
|
||||||
const cameraIDs: Set<string> = options?.cameraIDs
|
const cameraIDs: Set<string> = options?.cameraIDs
|
||||||
? options.cameraIDs
|
? options.cameraIDs
|
||||||
@@ -94,7 +96,6 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
|
|||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameraManager: CameraManager,
|
cameraManager: CameraManager,
|
||||||
cameras: Map<string, CameraConfig>,
|
|
||||||
view: View,
|
view: View,
|
||||||
options?: {
|
options?: {
|
||||||
targetView?: 'recording' | 'recordings';
|
targetView?: 'recording' | 'recordings';
|
||||||
@@ -102,7 +103,7 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
|
|||||||
): Promise<void> => {
|
): Promise<void> => {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
(
|
(
|
||||||
await createViewForRecordings(element, hass, cameraManager, cameras, view, {
|
await createViewForRecordings(element, hass, cameraManager, view, {
|
||||||
...options,
|
...options,
|
||||||
// Fetch 7 days worth of recordings (including recordings that are for the
|
// Fetch 7 days worth of recordings (including recordings that are for the
|
||||||
// current hour).
|
// current hour).
|
||||||
@@ -127,7 +128,6 @@ export const createViewForRecordings = async (
|
|||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
cameraManager: CameraManager,
|
cameraManager: CameraManager,
|
||||||
cameras: Map<string, CameraConfig>,
|
|
||||||
view: View,
|
view: View,
|
||||||
options?: {
|
options?: {
|
||||||
query?: RecordingMediaQueries;
|
query?: RecordingMediaQueries;
|
||||||
@@ -139,6 +139,10 @@ export const createViewForRecordings = async (
|
|||||||
end?: Date;
|
end?: Date;
|
||||||
},
|
},
|
||||||
): Promise<View | null> => {
|
): Promise<View | null> => {
|
||||||
|
const cameras = cameraManager.getCameras();
|
||||||
|
if (!cameras) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
const cameraIDs: Set<string> = options?.cameraIDs
|
const cameraIDs: Set<string> = options?.cameraIDs
|
||||||
? options.cameraIDs
|
? options.cameraIDs
|
||||||
: new Set(getAllDependentCameras(cameras, view.camera));
|
: new Set(getAllDependentCameras(cameras, view.camera));
|
||||||
|
|||||||
+3
-30
@@ -1,10 +1,5 @@
|
|||||||
import { ViewContext } from 'view';
|
import { ViewContext } from 'view';
|
||||||
import {
|
import { FrigateCardView } from '../types.js';
|
||||||
FrigateCardUserSpecifiedView,
|
|
||||||
FrigateCardView,
|
|
||||||
FRIGATE_CARD_VIEWS_USER_SPECIFIED,
|
|
||||||
FRIGATE_CARD_VIEW_DEFAULT,
|
|
||||||
} from '../types.js';
|
|
||||||
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
import { dispatchFrigateCardEvent } from '../utils/basic.js';
|
||||||
import { MediaQueries } from './media-queries';
|
import { MediaQueries } from './media-queries';
|
||||||
import { MediaQueriesResults } from './media-queries-results';
|
import { MediaQueriesResults } from './media-queries-results';
|
||||||
@@ -37,28 +32,6 @@ export class View {
|
|||||||
this.context = params.context ?? null;
|
this.context = params.context ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Selects the best view for a non-Frigate camera.
|
|
||||||
* @param view The wanted view.
|
|
||||||
* @returns The closest view supported by the non-Frigate camera.
|
|
||||||
*/
|
|
||||||
public static selectBestViewForNonFrigateCameras(view: FrigateCardView) {
|
|
||||||
return ['timeline', 'image'].includes(view) ? view : FRIGATE_CARD_VIEW_DEFAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Selects the best view for a user specified view.
|
|
||||||
* @param view The wanted view.
|
|
||||||
* @returns The closest view supported that is user changeable.
|
|
||||||
*/
|
|
||||||
public static selectBestViewForUserSpecified(view: FrigateCardView) {
|
|
||||||
return FRIGATE_CARD_VIEWS_USER_SPECIFIED.includes(
|
|
||||||
view as FrigateCardUserSpecifiedView,
|
|
||||||
)
|
|
||||||
? view
|
|
||||||
: FRIGATE_CARD_VIEW_DEFAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect if a view change represents a major "media change" for the given
|
* Detect if a view change represents a major "media change" for the given
|
||||||
* view.
|
* view.
|
||||||
@@ -138,8 +111,8 @@ export class View {
|
|||||||
/**
|
/**
|
||||||
* Determine if current view matches a named view.
|
* Determine if current view matches a named view.
|
||||||
*/
|
*/
|
||||||
public is(name: string): boolean {
|
public is(view: FrigateCardView): boolean {
|
||||||
return this.view == name;
|
return this.view == view;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user