Merge pull request #963 from dermotduffy/substreams
Add support for live substreams
This commit is contained in:
@@ -103,6 +103,7 @@ See the [fully expanded cameras configuration example](#config-expanded-cameras)
|
||||
| `live_provider` | `auto` | :heavy_multiplication_x: | The choice of live stream provider. See [Live Providers](#live-providers) below.|
|
||||
| `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. |
|
||||
| `hide` | `false` | :heavy_multiplication_x: | Whether or not to hide this as an independent camera (e.g. hidden on the live carousel, media filter, camera menu, and triggers cannot trigger this camera). This may be useful if this camera is exclusively used as a dependency of another camera. |
|
||||
| `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. |
|
||||
@@ -194,7 +195,7 @@ See [Using the WebRTC Card](#webrtc) below for more details on how to use the We
|
||||
|
||||
#### Camera Dependency Configuration
|
||||
|
||||
The `dependencies` block configures other cameras as dependents of this camera. Dependent cameras have their events fetched and merged with this camera. Configuration is under:
|
||||
The `dependencies` block configures other cameras as dependents of this camera. Dependent cameras have their media fetched and merged with this camera by default, and offer their respective live views as 'substreams' of the main (depended upon) camera. Configuration is under:
|
||||
|
||||
```yaml
|
||||
cameras:
|
||||
@@ -203,7 +204,7 @@ cameras:
|
||||
|
||||
| Option | Default | Overridable | Description |
|
||||
| - | - | - | - |
|
||||
| `cameras` | | :heavy_multiplication_x: | An optional array of other camera identifiers (see [camera IDs](#camera-ids)). If specified the card will fetch events for this camera and *also* recursively events for the named cameras. All dependent cameras must themselves be a configured camera in the card. This can be useful to group events for cameras that are close together, to always have clips/snapshots show fully merged events across all cameras or to show events for the `birdseye` camera that otherwise would not have events itself.|
|
||||
| `cameras` | | :heavy_multiplication_x: | An optional array of other camera identifiers (see [camera IDs](#camera-ids)). If specified the card will fetch media for this camera and *also* recursively for the named cameras by default. Live views for the involved cameras will be available as 'substreams' of the main (depended upon) camera. All dependent cameras must themselves be a configured camera in the card. This can be useful to group events for cameras that are close together, to show multiple related live views, to always have clips/snapshots show fully merged events across all cameras or to show events for the `birdseye` camera that otherwise would not have events itself.|
|
||||
| `all_cameras` | `false` | :heavy_multiplication_x: | Shortcut to specify all other cameras as dependent cameras.|
|
||||
|
||||
<a name="camera-triggers-configuration"></a>
|
||||
@@ -225,7 +226,7 @@ cameras:
|
||||
|
||||
<a name="camera-ids"></a>
|
||||
|
||||
#### Camera IDs: Refering to cameras in card configuration
|
||||
#### Camera IDs: Referring to cameras in card configuration
|
||||
|
||||
Each camera configured in the card has a single identifier (`id`). For a given camera, this will be one of the camera {`id`, `camera_entity`, `webrtc_card.entity` or `frigate.camera_name`} parameters for that camera -- in that order of precedence. These ids may be used in conditions, dependencies or custom actions to refer to a given camera unambiguously. |
|
||||
|
||||
@@ -1341,6 +1342,7 @@ cameras:
|
||||
- camera_entity: camera.front_Door
|
||||
live_provider: ha
|
||||
engine: auto
|
||||
hide: false
|
||||
frigate:
|
||||
url: http://my.frigate.local
|
||||
client_id: frigate
|
||||
@@ -1370,6 +1372,8 @@ cameras:
|
||||
title: 'Front entrance'
|
||||
# Custom identifier for the camera to refer to it above.
|
||||
id: 'camera-2'
|
||||
# Don't show this camera on the UI (will only be available as a dependent substream).
|
||||
hide: true
|
||||
webrtc_card:
|
||||
entity: camera.entrance_rtsp
|
||||
url: 'rtsp://username:password@camera:554/av_stream/ch0'
|
||||
@@ -1456,6 +1460,10 @@ menu:
|
||||
enabled: true
|
||||
alignment: matching
|
||||
icon: mdi:video-switch
|
||||
substreams:
|
||||
priority: 50
|
||||
enabled: true
|
||||
icon: mdi:video-input-component
|
||||
live:
|
||||
priority: 50
|
||||
enabled: true
|
||||
@@ -2837,6 +2845,66 @@ elements:
|
||||
```
|
||||
</details>
|
||||
|
||||
### Using live substreams
|
||||
|
||||
The card supports configuring 'substreams' to show up for a given live camera through the use of [camera dependencies](#camera-dependencies-configuration).
|
||||
|
||||
<details>
|
||||
<summary>Expand: Having an SD and HD substream</summary>
|
||||
|
||||
This example shows two substreams for a single live camera, and uses the 'HD' icon.
|
||||
|
||||
```yaml
|
||||
[...]
|
||||
cameras:
|
||||
- camera_entity: camera.sitting_room
|
||||
live_provider: image
|
||||
dependencies:
|
||||
cameras:
|
||||
- sitting_room_hd
|
||||
- camera_entity: camera.sitting_room
|
||||
title: Sitting Room HD
|
||||
live_provider: go2rtc
|
||||
id: sitting_room_hd
|
||||
# Do not show the HD camera independently on the UI.
|
||||
hide: true
|
||||
menu:
|
||||
buttons:
|
||||
substreams:
|
||||
icon: mdi:high-definition
|
||||
```
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary>Expand: Having a substream menu with different live providers</summary>
|
||||
|
||||
This example shows a substream menu for three different live providers for a given camera.
|
||||
|
||||
```yaml
|
||||
[...]
|
||||
cameras:
|
||||
- camera_entity: camera.sitting_room
|
||||
live_provider: image
|
||||
dependencies:
|
||||
cameras:
|
||||
- sitting_room_go2rtc
|
||||
- sitting_room_ha
|
||||
icon: mdi:image
|
||||
- camera_entity: camera.sitting_room
|
||||
live_provider: go2rtc
|
||||
id: sitting_room_go2rtc
|
||||
hide: true
|
||||
title: Sitting Room go2rtc
|
||||
icon: mdi:alpha-g
|
||||
- camera_entity: camera.sitting_room
|
||||
live_provider: ha
|
||||
id: sitting_room_ha
|
||||
hide: true
|
||||
title: Sitting Room HA
|
||||
icon: mdi:home
|
||||
```
|
||||
</details>
|
||||
|
||||
<a name="card-updates"></a>
|
||||
|
||||
### Using `card-mod` to style the card
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
DataQuery,
|
||||
EventQuery,
|
||||
EventQueryResultsMap,
|
||||
MediaMetadata,
|
||||
PartialEventQuery,
|
||||
PartialRecordingQuery,
|
||||
PartialRecordingSegmentsQuery,
|
||||
|
||||
@@ -19,7 +19,6 @@ import {
|
||||
EventQuery,
|
||||
EventQueryResults,
|
||||
EventQueryResultsMap,
|
||||
MediaMetadata,
|
||||
PartialEventQuery,
|
||||
PartialRecordingQuery,
|
||||
PartialRecordingSegmentsQuery,
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import { CameraConfig, CamerasConfig, CardWideConfig } from '../types.js';
|
||||
import {
|
||||
CameraConfig,
|
||||
CamerasConfig,
|
||||
CardWideConfig,
|
||||
} from '../types.js';
|
||||
import { allPromises, arrayify, setify } from '../utils/basic.js';
|
||||
import {
|
||||
CameraManagerCameraCapabilities,
|
||||
@@ -46,7 +50,7 @@ import { EntityRegistryManager } from '../utils/ha/entity-registry/index.js';
|
||||
import { getCameraID } from '../utils/camera.js';
|
||||
import { localize } from '../localize/localize.js';
|
||||
import { CameraInitializationError } from './error.js';
|
||||
import { CameraManagerStore } from './store.js';
|
||||
import { CameraManagerReadOnlyConfigStore, CameraManagerStore } from './store.js';
|
||||
import cloneDeep from 'lodash-es/cloneDeep';
|
||||
import { MEDIA_CHUNK_SIZE_DEFAULT } from '../const.js';
|
||||
|
||||
@@ -126,14 +130,18 @@ export class CameraManager {
|
||||
const output: Map<CameraConfig, CameraManagerEngine> = new Map();
|
||||
const engines: Map<Engine, CameraManagerEngine> = new Map();
|
||||
|
||||
for (const cameraConfig of camerasConfig) {
|
||||
const engineType = await this._engineFactory.getEngineForCamera(
|
||||
hass,
|
||||
cameraConfig,
|
||||
const getEngineTypes = async (configs: CameraConfig[]) => {
|
||||
return await allPromises(configs, (config) =>
|
||||
this._engineFactory.getEngineForCamera(hass, config),
|
||||
);
|
||||
};
|
||||
|
||||
const engineTypes = await getEngineTypes(camerasConfig);
|
||||
for (const [index, cameraConfig] of camerasConfig.entries()) {
|
||||
const engineType = engineTypes[index];
|
||||
const engine = engineType
|
||||
? engines.get(engineType) ?? this._engineFactory.createEngine(engineType)
|
||||
: null;
|
||||
? engines.get(engineType) ?? this._engineFactory.createEngine(engineType)
|
||||
: null;
|
||||
if (!engine || !engineType) {
|
||||
throw new CameraInitializationError(
|
||||
localize('error.no_camera_engine'),
|
||||
@@ -218,8 +226,8 @@ export class CameraManager {
|
||||
this._store.addCamera(id, result.initializedConfig, result.engine);
|
||||
});
|
||||
|
||||
if (!this._store.getCameraCount()) {
|
||||
throw new CameraInitializationError(localize('error.no_cameras'));
|
||||
if (!this._store.getVisibleCameraCount()) {
|
||||
throw new CameraInitializationError(localize('error.no_visible_cameras'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,22 +235,8 @@ export class CameraManager {
|
||||
return this._store.getCameraCount() > 0;
|
||||
}
|
||||
|
||||
public getCameras(): Map<string, CameraConfig> | null {
|
||||
return this._store.getCameras();
|
||||
}
|
||||
|
||||
public getCameraConfig(cameraID: string): CameraConfig | null {
|
||||
return this._store.getCameraConfig(cameraID);
|
||||
}
|
||||
|
||||
public getCameraIDs(): Set<string> | null {
|
||||
return this._store.getCameraCount()
|
||||
? new Set(this._store.getCameras().keys())
|
||||
: null;
|
||||
}
|
||||
|
||||
public hasCameraID(cameraID: string): boolean {
|
||||
return this._store.hasCameraID(cameraID);
|
||||
public getStore(): CameraManagerReadOnlyConfigStore {
|
||||
return this._store;
|
||||
}
|
||||
|
||||
public generateDefaultEventQueries(
|
||||
@@ -287,7 +281,7 @@ export class CameraManager {
|
||||
|
||||
const results = await this._handleQuery(hass, query);
|
||||
|
||||
for (const [query, result] of results?.entries() ?? []) {
|
||||
for (const result of results?.values() ?? []) {
|
||||
if (result.metadata.what) {
|
||||
result.metadata.what.forEach(what.add, what);
|
||||
}
|
||||
@@ -325,19 +319,19 @@ export class CameraManager {
|
||||
let queries: DataQuery[] | null = null;
|
||||
if (QueryClassifier.isEventQuery(partialQuery)) {
|
||||
queries = engine.generateDefaultEventQuery(
|
||||
this._store.getCameras(),
|
||||
this._store.getVisibleCameras(),
|
||||
cameraIDs,
|
||||
partialQuery,
|
||||
);
|
||||
} else if (QueryClassifier.isRecordingQuery(partialQuery)) {
|
||||
queries = engine.generateDefaultRecordingQuery(
|
||||
this._store.getCameras(),
|
||||
this._store.getVisibleCameras(),
|
||||
cameraIDs,
|
||||
partialQuery,
|
||||
);
|
||||
} else if (QueryClassifier.isRecordingSegmentsQuery(partialQuery)) {
|
||||
queries = engine.generateDefaultRecordingSegmentsQuery(
|
||||
this._store.getCameras(),
|
||||
this._store.getVisibleCameras(),
|
||||
cameraIDs,
|
||||
partialQuery,
|
||||
);
|
||||
|
||||
+43
-10
@@ -5,8 +5,26 @@ import { CameraConfigs, Engine } from './types';
|
||||
|
||||
type CameraManagerEngineCameraIDMap = Map<CameraManagerEngine, Set<string>>;
|
||||
|
||||
export class CameraManagerStore {
|
||||
protected _configs: Map<string, CameraConfig> = new Map();
|
||||
export interface CameraManagerReadOnlyConfigStore {
|
||||
getCameraConfig(cameraID: string): CameraConfig | null;
|
||||
getCameraConfigForMedia(media: ViewMedia): CameraConfig | null;
|
||||
|
||||
hasCameraID(cameraID: string): boolean;
|
||||
hasVisibleCameraID(cameraID: string): boolean;
|
||||
|
||||
getCameraCount(): number;
|
||||
getVisibleCameraCount(): number;
|
||||
|
||||
getCameras(): CameraConfigs;
|
||||
getVisibleCameras(): CameraConfigs;
|
||||
|
||||
getCameraIDs(): Set<string>;
|
||||
getVisibleCameraIDs(): Set<string>;
|
||||
}
|
||||
|
||||
export class CameraManagerStore implements CameraManagerReadOnlyConfigStore {
|
||||
protected _allConfigs: Map<string, CameraConfig> = new Map();
|
||||
protected _visibleConfigs: Map<string, CameraConfig> = new Map();
|
||||
protected _enginesByCamera: Map<string, CameraManagerEngine> = new Map();
|
||||
protected _enginesByType: Map<Engine, CameraManagerEngine> = new Map();
|
||||
|
||||
@@ -15,29 +33,44 @@ export class CameraManagerStore {
|
||||
cameraConfig: CameraConfig,
|
||||
engine: CameraManagerEngine,
|
||||
): void {
|
||||
this._configs.set(cameraID, cameraConfig);
|
||||
if (!cameraConfig.hide) {
|
||||
this._visibleConfigs.set(cameraID, cameraConfig);
|
||||
}
|
||||
this._allConfigs.set(cameraID, cameraConfig);
|
||||
this._enginesByCamera.set(cameraID, engine);
|
||||
this._enginesByType.set(engine.getEngineType(), engine);
|
||||
}
|
||||
|
||||
public getCameraCount(): number {
|
||||
return this._configs.size;
|
||||
public getCameraConfig(cameraID: string): CameraConfig | null {
|
||||
return this._allConfigs.get(cameraID) ?? null;
|
||||
}
|
||||
|
||||
public hasCameraID(cameraID: string): boolean {
|
||||
return this._configs.has(cameraID);
|
||||
return this._allConfigs.has(cameraID);
|
||||
}
|
||||
public hasVisibleCameraID(cameraID: string): boolean {
|
||||
return this._visibleConfigs.has(cameraID);
|
||||
}
|
||||
|
||||
public getCameraConfig(cameraID: string): CameraConfig | null {
|
||||
return this._configs.get(cameraID) ?? null;
|
||||
public getCameraCount(): number {
|
||||
return this._allConfigs.size;
|
||||
}
|
||||
public getVisibleCameraCount(): number {
|
||||
return this._visibleConfigs.size;
|
||||
}
|
||||
|
||||
public getCameras(): CameraConfigs {
|
||||
return this._configs;
|
||||
return this._allConfigs;
|
||||
}
|
||||
public getVisibleCameras(): CameraConfigs {
|
||||
return this._visibleConfigs;
|
||||
}
|
||||
|
||||
public getCameraIDs(): Set<string> {
|
||||
return new Set(this._configs.keys());
|
||||
return new Set(this._allConfigs.keys());
|
||||
}
|
||||
public getVisibleCameraIDs(): Set<string> {
|
||||
return new Set(this._visibleConfigs.keys());
|
||||
}
|
||||
|
||||
public getCameraConfigForMedia(media: ViewMedia): CameraConfig | null {
|
||||
|
||||
+93
-24
@@ -384,13 +384,14 @@ class FrigateCard extends LitElement {
|
||||
protected _getMenuButtons(): MenuButton[] {
|
||||
const buttons: MenuButton[] = [];
|
||||
|
||||
const cameras = this._cameraManager?.getCameras();
|
||||
const visibleCameras = this._cameraManager?.getStore().getVisibleCameras();
|
||||
const selectedCameraID = this._view?.camera;
|
||||
const selectedCameraConfig = this._getSelectedCameraConfig();
|
||||
const allSelectedCameraIDs =
|
||||
cameras && selectedCameraID
|
||||
? getAllDependentCameras(cameras, selectedCameraID)
|
||||
: null;
|
||||
const allSelectedCameraIDs = getAllDependentCameras(
|
||||
this._cameraManager,
|
||||
selectedCameraID,
|
||||
);
|
||||
|
||||
const cameraCapabilities = allSelectedCameraIDs
|
||||
? this._cameraManager?.getAggregateCameraCapabilities(allSelectedCameraIDs)
|
||||
: null;
|
||||
@@ -410,8 +411,8 @@ class FrigateCard extends LitElement {
|
||||
) as FrigateCardCustomAction,
|
||||
});
|
||||
|
||||
if (cameras) {
|
||||
const menuItems = Array.from(cameras, ([cameraID, config]) => {
|
||||
if (visibleCameras) {
|
||||
const menuItems = Array.from(visibleCameras, ([cameraID, config]) => {
|
||||
const action = createFrigateCardCustomAction('camera_select', {
|
||||
camera: cameraID,
|
||||
});
|
||||
@@ -439,6 +440,61 @@ class FrigateCard extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedCameraID && allSelectedCameraIDs && this._view?.is('live')) {
|
||||
const dependencies = [...allSelectedCameraIDs];
|
||||
const override = this._view?.context?.live?.overrides?.get(selectedCameraID);
|
||||
|
||||
if (dependencies.length === 2) {
|
||||
// If there are only two dependencies (the main camera, and 1 other)
|
||||
// then use a button not a menu to toggle.
|
||||
buttons.push({
|
||||
icon: 'mdi:video-input-component',
|
||||
style:
|
||||
override && override !== selectedCameraID ? this._getEmphasizedStyle() : {},
|
||||
title: localize('config.menu.buttons.substreams'),
|
||||
...this._getConfig().menu.buttons.substreams,
|
||||
type: 'custom:frigate-card-menu-icon',
|
||||
tap_action: createFrigateCardCustomAction('live_substream_select', {
|
||||
camera:
|
||||
override === undefined || override === dependencies[0]
|
||||
? dependencies[1]
|
||||
: dependencies[0],
|
||||
}) as FrigateCardCustomAction,
|
||||
});
|
||||
} else if (dependencies.length > 2) {
|
||||
const menuItems = Array.from(dependencies, (cameraID) => {
|
||||
const action = createFrigateCardCustomAction('live_substream_select', {
|
||||
camera: cameraID,
|
||||
});
|
||||
const metadata = this._hass
|
||||
? this._cameraManager?.getCameraMetadata(this._hass, cameraID) ?? undefined
|
||||
: undefined;
|
||||
const cameraConfig = this._cameraManager?.getStore().getCameraConfig(cameraID);
|
||||
return {
|
||||
enabled: true,
|
||||
icon: metadata?.icon,
|
||||
entity: cameraConfig?.camera_entity,
|
||||
state_color: true,
|
||||
title: metadata?.title,
|
||||
selected:
|
||||
(this._view?.context?.live?.overrides?.get(selectedCameraID) ??
|
||||
selectedCameraID) === cameraID,
|
||||
...(action && { tap_action: action }),
|
||||
};
|
||||
});
|
||||
|
||||
buttons.push({
|
||||
icon: 'mdi:video-input-component',
|
||||
title: localize('config.menu.buttons.substreams'),
|
||||
style:
|
||||
override && override !== selectedCameraID ? this._getEmphasizedStyle() : {},
|
||||
...this._getConfig().menu.buttons.substreams,
|
||||
type: 'custom:frigate-card-menu-submenu',
|
||||
items: menuItems,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
buttons.push({
|
||||
icon: 'mdi:cctv',
|
||||
...this._getConfig().menu.buttons.live,
|
||||
@@ -634,7 +690,7 @@ class FrigateCard extends LitElement {
|
||||
if (!this._view || !this._cameraManager) {
|
||||
return null;
|
||||
}
|
||||
return this._cameraManager.getCameraConfig(this._view.camera);
|
||||
return this._cameraManager.getStore().getCameraConfig(this._view.camera);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -767,7 +823,7 @@ class FrigateCard extends LitElement {
|
||||
protected _changeView(args?: { view?: View; resetMessage?: boolean }): void {
|
||||
log(this._cardWideConfig, `Frigate Card view change: `, args?.view ?? '[default]');
|
||||
const changeView = (view: View): void => {
|
||||
if (View.isMediaChange(this._view, view)) {
|
||||
if (View.isMajorMediaChange(this._view, view)) {
|
||||
this._currentMediaLoadedInfo = null;
|
||||
}
|
||||
if (this._view?.view !== view.view) {
|
||||
@@ -785,7 +841,7 @@ class FrigateCard extends LitElement {
|
||||
// Load the default view.
|
||||
let cameraID: string | null = null;
|
||||
if (this._cameraManager) {
|
||||
const cameras = this._cameraManager.getCameras();
|
||||
const cameras = this._cameraManager.getStore().getVisibleCameras();
|
||||
if (cameras) {
|
||||
if (this._view?.camera && this._getConfig().view.update_cycle_camera) {
|
||||
const keys = Array.from(cameras.keys());
|
||||
@@ -1007,7 +1063,7 @@ class FrigateCard extends LitElement {
|
||||
let changedCamera = false;
|
||||
let triggerChanges = false;
|
||||
|
||||
const cameras = this._cameraManager?.getCameras();
|
||||
const cameras = this._cameraManager?.getStore().getVisibleCameras();
|
||||
for (const [cameraID, config] of cameras?.entries() ?? []) {
|
||||
const triggerEntities = config.triggers.entities ?? [];
|
||||
const diffs = getHassDifferences(this._hass, oldHass, triggerEntities, {
|
||||
@@ -1307,7 +1363,7 @@ class FrigateCard extends LitElement {
|
||||
*/
|
||||
protected _cardActionHandler(ev: CustomEvent<ActionType>): void {
|
||||
const frigateCardAction = convertActionToFrigateCardCustomAction(ev.detail);
|
||||
if (!frigateCardAction) {
|
||||
if (!this._view || !frigateCardAction) {
|
||||
return;
|
||||
}
|
||||
const action = frigateCardAction.frigate_card_action;
|
||||
@@ -1325,14 +1381,12 @@ class FrigateCard extends LitElement {
|
||||
case 'snapshot':
|
||||
case 'snapshots':
|
||||
case 'timeline':
|
||||
if (this._view) {
|
||||
this._changeView({
|
||||
view: new View({
|
||||
view: action,
|
||||
camera: this._view.camera,
|
||||
}),
|
||||
});
|
||||
}
|
||||
this._changeView({
|
||||
view: new View({
|
||||
view: action,
|
||||
camera: this._view.camera,
|
||||
}),
|
||||
});
|
||||
break;
|
||||
case 'download':
|
||||
this._downloadViewerMedia();
|
||||
@@ -1355,17 +1409,32 @@ class FrigateCard extends LitElement {
|
||||
this._refMenu.value?.toggleMenu();
|
||||
break;
|
||||
case 'camera_select':
|
||||
const cameraID = frigateCardAction.camera;
|
||||
if (this._cameraManager?.hasCameraID(cameraID) && this._view) {
|
||||
const selectCameraID = frigateCardAction.camera;
|
||||
if (
|
||||
this._view &&
|
||||
this._cameraManager?.getStore().hasVisibleCameraID(selectCameraID)
|
||||
) {
|
||||
const viewOnCameraSelect = this._getConfig().view.camera_select;
|
||||
const targetView =
|
||||
viewOnCameraSelect === 'current' ? this._view.view : viewOnCameraSelect;
|
||||
const actualView = this.isViewSupportedByCamera(cameraID, targetView)
|
||||
const actualView = this.isViewSupportedByCamera(selectCameraID, targetView)
|
||||
? targetView
|
||||
: FRIGATE_CARD_VIEW_DEFAULT;
|
||||
this._changeView({ view: new View({ view: actualView, camera: cameraID }) });
|
||||
this._changeView({
|
||||
view: new View({ view: actualView, camera: selectCameraID }),
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'live_substream_select':
|
||||
const overrides: Map<string, string> =
|
||||
this._view.context?.live?.overrides ?? new Map();
|
||||
overrides.set(this._view.camera, frigateCardAction.camera);
|
||||
this._changeView({
|
||||
view: this._view.clone().mergeInContext({
|
||||
live: { overrides: overrides },
|
||||
}),
|
||||
});
|
||||
break;
|
||||
case 'media_player':
|
||||
this._mediaPlayerAction(
|
||||
frigateCardAction.media_player,
|
||||
|
||||
+43
-20
@@ -56,6 +56,18 @@ import { dispatchMessageEvent, dispatchErrorMessageEvent } from '../message.js';
|
||||
import { HassEntity } from 'home-assistant-js-websocket';
|
||||
import { CameraEndpoints } from '../../camera-manager/types.js';
|
||||
|
||||
interface LiveViewContext {
|
||||
// A cameraID override (used for dependencies/substreams to force a different
|
||||
// camera to be live rather than the camera selected in the view).
|
||||
overrides?: Map<string, string>;
|
||||
}
|
||||
|
||||
declare module 'view' {
|
||||
interface ViewContext {
|
||||
live?: LiveViewContext;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the state object or dispatch an error. Used in `ha` and `image` live
|
||||
* providers.
|
||||
@@ -339,7 +351,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
}
|
||||
|
||||
protected _getSelectedCameraIndex(): number {
|
||||
const cameraIDs = this.cameraManager?.getCameraIDs();
|
||||
const cameraIDs = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||
if (!cameraIDs || !this.view) {
|
||||
return 0;
|
||||
}
|
||||
@@ -362,7 +374,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
* @returns A list of EmblaOptionsTypes.
|
||||
*/
|
||||
protected _getPlugins(): EmblaCarouselPlugins {
|
||||
const cameras = this.cameraManager?.getCameraIDs();
|
||||
const cameras = this.cameraManager?.getStore().getVisibleCameraIDs();
|
||||
return [
|
||||
// Only enable wheel plugin if there is more than one camera.
|
||||
...(cameras && cameras.size > 1
|
||||
@@ -420,18 +432,27 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
* name to slide number.
|
||||
*/
|
||||
protected _getSlides(): [TemplateResult[], Record<string, number>] {
|
||||
const cameras = this.cameraManager?.getCameras();
|
||||
if (!cameras) {
|
||||
const visibleCameras = this.cameraManager?.getStore().getVisibleCameras();
|
||||
if (!visibleCameras) {
|
||||
return [[], {}];
|
||||
}
|
||||
|
||||
const slides: TemplateResult[] = [];
|
||||
const cameraToSlide: Record<string, number> = {};
|
||||
|
||||
for (const [camera, cameraConfig] of cameras) {
|
||||
const slide = this._renderLive(camera, cameraConfig, slides.length);
|
||||
for (const [cameraID, cameraConfig] of visibleCameras) {
|
||||
const liveCameraID =
|
||||
this.view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
|
||||
const liveCameraConfig =
|
||||
cameraID === liveCameraID
|
||||
? cameraConfig
|
||||
: this.cameraManager?.getStore().getCameraConfig(liveCameraID);
|
||||
|
||||
const slide = liveCameraConfig
|
||||
? this._renderLive(liveCameraID, liveCameraConfig, slides.length)
|
||||
: null;
|
||||
if (slide) {
|
||||
cameraToSlide[camera] = slides.length;
|
||||
cameraToSlide[cameraID] = slides.length;
|
||||
slides.push(slide);
|
||||
}
|
||||
}
|
||||
@@ -442,7 +463,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
* Handle the user selecting a new slide in the carousel.
|
||||
*/
|
||||
protected _setViewHandler(ev: CustomEvent<CarouselSelect>): void {
|
||||
const cameras = this.cameraManager?.getCameras();
|
||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
||||
if (cameras && ev.detail.index !== this._getSelectedCameraIndex()) {
|
||||
this._setViewCameraID(Array.from(cameras.keys())[ev.detail.index]);
|
||||
}
|
||||
@@ -515,7 +536,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
<frigate-card-live-provider
|
||||
?disabled=${this.liveConfig.lazy_load}
|
||||
.cameraConfig=${cameraConfig}
|
||||
.cameraEndpoints=${guard([this.cameraManager], () =>
|
||||
.cameraEndpoints=${guard([this.cameraManager, cameraID], () =>
|
||||
this.cameraManager?.getCameraEndpoints(cameraID),
|
||||
)}
|
||||
.label=${cameraMetadata?.title ?? ''}
|
||||
@@ -535,7 +556,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
}
|
||||
|
||||
protected _getCameraIDsOfNeighbors(): [string | null, string | null] {
|
||||
const cameras = this.cameraManager?.getCameras();
|
||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
||||
if (!cameras || !this.view || !this.hass) {
|
||||
return [null, null];
|
||||
}
|
||||
@@ -557,15 +578,13 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
* @returns A template to display to the user.
|
||||
*/
|
||||
protected render(): TemplateResult | void {
|
||||
if (!this.liveConfig || !this.view || !this.hass || !this.cameraManager) {
|
||||
return;
|
||||
}
|
||||
|
||||
const [slides, cameraToSlide] = this._getSlides();
|
||||
this._cameraToSlide = cameraToSlide;
|
||||
if (
|
||||
!slides.length ||
|
||||
!this.liveConfig ||
|
||||
!this.view ||
|
||||
!this.hass ||
|
||||
!this.cameraManager
|
||||
) {
|
||||
if (!slides.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -577,15 +596,19 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
|
||||
const [prevID, nextID] = this._getCameraIDsOfNeighbors();
|
||||
|
||||
const overrideCameraID = (cameraID: string): string => {
|
||||
return this.view?.context?.live?.overrides?.get(cameraID) ?? cameraID;
|
||||
};
|
||||
|
||||
const cameraMetadataPrevious = prevID
|
||||
? this.cameraManager.getCameraMetadata(this.hass, prevID)
|
||||
? this.cameraManager.getCameraMetadata(this.hass, overrideCameraID(prevID))
|
||||
: null;
|
||||
const cameraMetadataCurrent = this.cameraManager.getCameraMetadata(
|
||||
this.hass,
|
||||
this.view.camera,
|
||||
overrideCameraID(this.view.camera),
|
||||
);
|
||||
const cameraMetadataNext = nextID
|
||||
? this.cameraManager.getCameraMetadata(this.hass, nextID)
|
||||
? this.cameraManager.getCameraMetadata(this.hass, overrideCameraID(nextID))
|
||||
: null;
|
||||
|
||||
// Notes on the below:
|
||||
|
||||
@@ -31,10 +31,7 @@ import { MediaQueriesClassifier } from '../view/media-queries-classifier';
|
||||
import { View } from '../view/view';
|
||||
import { CameraManager } from '../camera-manager/manager';
|
||||
import { HomeAssistant } from 'custom-card-helpers';
|
||||
import {
|
||||
MediaMetadata,
|
||||
QueryType,
|
||||
} from '../camera-manager/types';
|
||||
import { DataQuery, MediaMetadata, QueryType } from '../camera-manager/types';
|
||||
import format from 'date-fns/format';
|
||||
import endOfMonth from 'date-fns/endOfMonth';
|
||||
import isEqual from 'lodash-es/isEqual';
|
||||
@@ -168,7 +165,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
_ev: CustomEvent<{ value: unknown }>,
|
||||
): Promise<void> {
|
||||
const cameras = this.cameraManager?.getCameras();
|
||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
||||
if (!this.hass || !cameras || !this.cameraManager || !this.view) {
|
||||
return;
|
||||
}
|
||||
@@ -269,7 +266,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
if (changedProps.has('cameraManager')) {
|
||||
const cameras = this.cameraManager?.getCameras();
|
||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
||||
if (cameras) {
|
||||
this._cameraOptions = Array.from(cameras.keys()).map((cameraID) => ({
|
||||
value: cameraID,
|
||||
@@ -321,7 +318,7 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||
|
||||
protected _getDefaultsFromView(): MediaFilterCoreDefaults | null {
|
||||
const queries = this.view?.query?.getQueries();
|
||||
const cameras = this.cameraManager?.getCameras();
|
||||
const cameras = this.cameraManager?.getStore().getVisibleCameras();
|
||||
if (!this.view || !queries || !cameras) {
|
||||
return null;
|
||||
}
|
||||
@@ -333,12 +330,12 @@ class FrigateCardMediaFilter extends ScopedRegistryHost(LitElement) {
|
||||
let favorite: MediaFilterCoreFavoriteSelection | undefined;
|
||||
|
||||
const cameraIDSets = uniqWith(
|
||||
queries.map((query) => query.cameraIDs),
|
||||
queries.map((query: DataQuery) => query.cameraIDs),
|
||||
isEqual,
|
||||
);
|
||||
// Special note: If all cameras are selected, this is the same as no
|
||||
// Special note: If all visible cameras are selected, this is the same as no
|
||||
// selector at all.
|
||||
if (cameraIDSets.length === 1 && queries[0].cameraIDs.size !== cameras.size) {
|
||||
if (cameraIDSets.length === 1 && isEqual(queries[0].cameraIDs, cameras)) {
|
||||
cameraIDs = [...queries[0].cameraIDs];
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import { ThumbnailCarouselTap } from './thumbnail-carousel.js';
|
||||
import './surround-basic.js';
|
||||
import { changeViewToRecentEventsForCameraAndDependents } from '../utils/media-to-view';
|
||||
import { getAllDependentCameras } from '../utils/camera.js';
|
||||
import type { DataQuery } from '../camera-manager/types';
|
||||
|
||||
interface ThumbnailViewContext {
|
||||
// Whether or not to fetch thumbnails.
|
||||
@@ -120,7 +121,7 @@ export class FrigateCardSurround extends LitElement {
|
||||
// user is scrubbing video).
|
||||
if (
|
||||
changedProperties.has('view') &&
|
||||
View.isMediaChange(changedProperties.get('view'), this.view)
|
||||
View.isMajorMediaChange(changedProperties.get('view'), this.view)
|
||||
) {
|
||||
this._cameraIDsForTimeline = this._getCameraIDsForTimeline() ?? undefined;
|
||||
}
|
||||
@@ -138,18 +139,20 @@ export class FrigateCardSurround extends LitElement {
|
||||
}
|
||||
|
||||
protected _getCameraIDsForTimeline(): Set<string> | null {
|
||||
const cameras = this.cameraManager?.getCameras();
|
||||
if (!this.view || !cameras) {
|
||||
if (!this.view) {
|
||||
return null;
|
||||
}
|
||||
if (this.view?.is('live')) {
|
||||
return getAllDependentCameras(cameras, this.view.camera);
|
||||
return getAllDependentCameras(
|
||||
this.cameraManager,
|
||||
this.view.camera,
|
||||
);
|
||||
}
|
||||
if (this.view.isViewerView()) {
|
||||
return new Set(
|
||||
this.view.query
|
||||
?.getQueries()
|
||||
?.map((query) => [...query.cameraIDs])
|
||||
?.map((query: DataQuery) => [...query.cameraIDs])
|
||||
.flat(),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -238,7 +238,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
const item = request.detail.item;
|
||||
const media = this._timelineSource?.dataset.get(item)?.media;
|
||||
const cameraConfig = media
|
||||
? this.cameraManager?.getCameraConfig(media.getCameraID()) ?? undefined
|
||||
? this.cameraManager?.getStore().getCameraConfigForMedia(media) ?? undefined
|
||||
: undefined;
|
||||
|
||||
request.detail.hass = this.hass;
|
||||
@@ -291,15 +291,9 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
* @returns A set of camera ids (may be empty).
|
||||
*/
|
||||
protected _getTimelineCameraIDs(): Set<string> | null {
|
||||
return this.cameraIDs ?? this._getAllCameraIDs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the keys of all cameras.
|
||||
* @returns A set of camera ids (may be empty).
|
||||
*/
|
||||
protected _getAllCameraIDs(): Set<string> | null {
|
||||
return this.cameraManager?.getCameraIDs() ?? null;
|
||||
return (
|
||||
this.cameraIDs ?? this.cameraManager?.getStore().getVisibleCameraIDs() ?? null
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1042,11 +1036,7 @@ export class FrigateCardTimelineCore extends LitElement {
|
||||
changedProps.has('cameraIDs')
|
||||
) {
|
||||
const cameraIDs = this._getTimelineCameraIDs();
|
||||
if (
|
||||
cameraIDs &&
|
||||
this.cameraManager &&
|
||||
this.timelineConfig
|
||||
) {
|
||||
if (cameraIDs && this.cameraManager && this.timelineConfig) {
|
||||
this._timelineSource = new TimelineDataSource(
|
||||
this.cameraManager,
|
||||
cameraIDs,
|
||||
|
||||
@@ -13,6 +13,7 @@ export const CONF_CAMERAS_ARRAY_FRIGATE_LABEL =
|
||||
export const CONF_CAMERAS_ARRAY_FRIGATE_URL = `${CONF_CAMERAS}.#.frigate.url` as const;
|
||||
export const CONF_CAMERAS_ARRAY_FRIGATE_ZONE = `${CONF_CAMERAS}.#.frigate.zone` as const;
|
||||
export const CONF_CAMERAS_ARRAY_GO2RTC_MODES = `${CONF_CAMERAS}.#.go2rtc.modes` as const;
|
||||
export const CONF_CAMERAS_ARRAY_HIDE = `${CONF_CAMERAS}.#.hide` as const;
|
||||
export const CONF_CAMERAS_ARRAY_ID = `${CONF_CAMERAS}.#.id` as const;
|
||||
export const CONF_CAMERAS_ARRAY_TITLE = `${CONF_CAMERAS}.#.title` as const;
|
||||
export const CONF_CAMERAS_ARRAY_ICON = `${CONF_CAMERAS}.#.icon` as const;
|
||||
|
||||
@@ -130,6 +130,7 @@ import {
|
||||
CONF_PERFORMANCE_FEATURES_MEDIA_CHUNK_SIZE,
|
||||
MEDIA_CHUNK_SIZE_MAX,
|
||||
CONF_CAMERAS_ARRAY_GO2RTC_MODES,
|
||||
CONF_CAMERAS_ARRAY_HIDE,
|
||||
} from './const.js';
|
||||
import { localize } from './localize/localize.js';
|
||||
import frigate_card_editor_style from './scss/editor.scss';
|
||||
@@ -1341,6 +1342,13 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
${this._renderStringInput(
|
||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_ID, cameraIndex),
|
||||
)}
|
||||
${this._renderSwitch(
|
||||
getArrayConfigPath(
|
||||
CONF_CAMERAS_ARRAY_HIDE,
|
||||
cameraIndex,
|
||||
),
|
||||
this._defaults.cameras.hide,
|
||||
)}
|
||||
${this._putInSubmenu(
|
||||
MENU_CAMERAS_FRIGATE,
|
||||
cameraIndex,
|
||||
@@ -1604,6 +1612,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
||||
})}
|
||||
${this._renderMenuButton('frigate') /* */}
|
||||
${this._renderMenuButton('cameras') /* */}
|
||||
${this._renderMenuButton('substreams') /* */}
|
||||
${this._renderMenuButton('live') /* */}
|
||||
${this._renderMenuButton('clips') /* */}
|
||||
${this._renderMenuButton('snapshots')}
|
||||
|
||||
@@ -33,6 +33,7 @@
|
||||
"mjpeg": "Motion JPEG (MJPEG)"
|
||||
}
|
||||
},
|
||||
"hide": "Hide camera from UI",
|
||||
"icon": "Icon for this camera (Autodetected from entity)",
|
||||
"id": "Unique id for this camera in this card",
|
||||
"live_provider": "Live view provider for this camera",
|
||||
@@ -239,6 +240,7 @@
|
||||
"media_player": "Send to media player",
|
||||
"priority": "Priority",
|
||||
"snapshots": "Snapshots",
|
||||
"substreams": "Substream(s)",
|
||||
"timeline": "Timeline",
|
||||
"recordings": "Recordings"
|
||||
},
|
||||
@@ -368,10 +370,11 @@
|
||||
"live_camera_unavailable": "Camera unavailable",
|
||||
"no_camera_engine": "Could not determine suitable engine for camera",
|
||||
"no_camera_entity": "Could not find camera entity",
|
||||
"no_camera_entity_for_triggers": "A camera entity is required in order to autodetect triggers",
|
||||
"no_camera_id": "Could not determine camera id for the following camera, may need to set 'id' parameter manually",
|
||||
"no_camera_name": "Could not determine a Frigate camera name for camera (or one of its dependents), please specify either 'camera_entity' or 'camera_name'",
|
||||
"no_cameras": "No valid cameras found, you must configure at least one camera entry",
|
||||
"no_live_camera": "The camera_entity parameter must be set and valid for this live provider",
|
||||
"no_visible_cameras": "No visible cameras found, you must configure at least one non-hidden camera",
|
||||
"reconnecting": "Reconnecting",
|
||||
"timeline_no_cameras": "No Frigate cameras to show in timeline",
|
||||
"troubleshooting": "Check troubleshooting",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"url": "Frigate URL del server",
|
||||
"zone": "Frigate zona"
|
||||
},
|
||||
"hide": "",
|
||||
"icon": "Icona per questa telecamera (Autoidentificato dall'entità)",
|
||||
"id": "ID univoco per questa telecamera in questa carta",
|
||||
"live_provider": "Provider di visualizzazione dal vivo per questa telecamera",
|
||||
@@ -223,6 +224,7 @@
|
||||
"live": "Abitare",
|
||||
"media_player": "Invia a Media Player",
|
||||
"priority": "Priorità",
|
||||
"substreams": "",
|
||||
"snapshots": "Istantanee",
|
||||
"timeline": "Timeline"
|
||||
},
|
||||
@@ -347,10 +349,11 @@
|
||||
"live_camera_unavailable": "Telecamera non disponibile",
|
||||
"no_camera_engine": "",
|
||||
"no_camera_entity": "",
|
||||
"no_camera_entity_for_triggers": "",
|
||||
"no_camera_id": "Impossibile determinare l'ID della telecamera , potrebbe essere necessario impostare manualmente il parametro 'ID'",
|
||||
"no_camera_name": "Impossibile determinare un nome della telecamera in Frigate, si prega di specificare 'camera_enty' o 'camera_name'",
|
||||
"no_cameras": "Nessuna telecamera valida trovata, è necessario configurare almeno una voce della telecamera",
|
||||
"no_live_camera": "Il parametro fotocamera_enty deve essere impostato e valido per questo provider live",
|
||||
"no_visible_cameras": "",
|
||||
"reconnecting": "Riconnessione",
|
||||
"timeline_no_cameras": "Nessuna telecamera damostrare in Frigate nella timeline",
|
||||
"troubleshooting": "Controllare la risoluzione dei problemi",
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"url": "URL do servidor Frigate",
|
||||
"zone": "Zona do Frigate"
|
||||
},
|
||||
"hide": "",
|
||||
"icon": "Ícone para esta câmera (detectado automaticamente pela entidade)",
|
||||
"id": "ID exclusivo para esta câmera nesse cartão",
|
||||
"live_provider": "Provedor de visualização ao vivo para esta câmera",
|
||||
@@ -223,6 +224,7 @@
|
||||
"live": "Ao vivo",
|
||||
"media_player": "Enviar para o reprodutor de mídia",
|
||||
"priority": "Prioridade",
|
||||
"substreams": "",
|
||||
"snapshots": "Instantâneos",
|
||||
"timeline": "Linha do tempo"
|
||||
},
|
||||
@@ -347,10 +349,11 @@
|
||||
"live_camera_unavailable": "",
|
||||
"no_camera_engine": "",
|
||||
"no_camera_entity": "",
|
||||
"no_camera_entity_for_triggers": "",
|
||||
"no_camera_id": "Não foi possível determinar o ID da câmera para a câmera a seguir, pode ser necessário definir o parâmetro 'id' manualmente",
|
||||
"no_camera_name": "Não foi possível determinar o nome da câmera da Frigate, especifique 'camera_entity' ou 'camera_name' para a câmera a seguir",
|
||||
"no_cameras": "Nenhuma câmera válida encontrada, você deve configurar pelo menos uma câmera",
|
||||
"no_live_camera": "O parâmetro camera_entity deve ser definido e válido para este provedor ativo",
|
||||
"no_visible_cameras": "",
|
||||
"reconnecting": "Reconectando",
|
||||
"timeline_no_cameras": "Nenhuma câmera do Frigate para mostrar na linha do tempo",
|
||||
"troubleshooting": "Verifique a solução de problemas",
|
||||
|
||||
+17
-4
@@ -199,7 +199,7 @@ const noActionSchema = schemaForType<
|
||||
}),
|
||||
);
|
||||
|
||||
const frigateCardCustomactionsBaseSchema = customActionSchema.extend({
|
||||
const frigateCardCustomActionsBaseSchema = customActionSchema.extend({
|
||||
action: z
|
||||
.literal('custom:frigate-card-action')
|
||||
// Syntactic sugar to avoid 'fire-dom-event' as part of an external API.
|
||||
@@ -227,18 +227,24 @@ const FRIGATE_CARD_GENERAL_ACTIONS = [
|
||||
const FRIGATE_CARD_ACTIONS = [
|
||||
...FRIGATE_CARD_GENERAL_ACTIONS,
|
||||
'camera_select',
|
||||
'live_substream_select',
|
||||
'media_player',
|
||||
] as const;
|
||||
export type FrigateCardAction = (typeof FRIGATE_CARD_ACTIONS)[number];
|
||||
|
||||
const frigateCardGeneralActionSchema = frigateCardCustomactionsBaseSchema.extend({
|
||||
const frigateCardGeneralActionSchema = frigateCardCustomActionsBaseSchema.extend({
|
||||
frigate_card_action: z.enum(FRIGATE_CARD_GENERAL_ACTIONS),
|
||||
});
|
||||
const frigateCardCameraSelectActionSchema = frigateCardCustomactionsBaseSchema.extend({
|
||||
const frigateCardCameraSelectActionSchema = frigateCardCustomActionsBaseSchema.extend({
|
||||
frigate_card_action: z.literal('camera_select'),
|
||||
camera: z.string(),
|
||||
});
|
||||
const frigateCarMediaPlayerActionSchema = frigateCardCustomactionsBaseSchema.extend({
|
||||
const frigateCardLiveDependencySelectActionSchema =
|
||||
frigateCardCustomActionsBaseSchema.extend({
|
||||
frigate_card_action: z.literal('live_substream_select'),
|
||||
camera: z.string(),
|
||||
});
|
||||
const frigateCarMediaPlayerActionSchema = frigateCardCustomActionsBaseSchema.extend({
|
||||
frigate_card_action: z.literal('media_player'),
|
||||
media_player: z.string(),
|
||||
media_player_action: z.enum(['play', 'stop']),
|
||||
@@ -247,6 +253,7 @@ const frigateCarMediaPlayerActionSchema = frigateCardCustomactionsBaseSchema.ext
|
||||
export const frigateCardCustomActionSchema = z.union([
|
||||
frigateCardGeneralActionSchema,
|
||||
frigateCardCameraSelectActionSchema,
|
||||
frigateCardLiveDependencySelectActionSchema,
|
||||
frigateCarMediaPlayerActionSchema,
|
||||
]);
|
||||
export type FrigateCardCustomAction = z.infer<typeof frigateCardCustomActionSchema>;
|
||||
@@ -398,6 +405,7 @@ const cameraConfigDefault = {
|
||||
all_cameras: false,
|
||||
cameras: [],
|
||||
},
|
||||
hide: false,
|
||||
triggers: {
|
||||
motion: false,
|
||||
occupancy: true,
|
||||
@@ -418,6 +426,9 @@ const cameraConfigSchema = z
|
||||
icon: z.string().optional(),
|
||||
title: z.string().optional(),
|
||||
|
||||
// Used to hide the camera (e.g. when used only as a dependency).
|
||||
hide: z.boolean().optional(),
|
||||
|
||||
// Optional identifier to separate different camera configurations used in
|
||||
// this card.
|
||||
id: z.string().optional(),
|
||||
@@ -944,6 +955,7 @@ const menuConfigDefault = {
|
||||
buttons: {
|
||||
frigate: visibleButtonDefault,
|
||||
cameras: visibleButtonDefault,
|
||||
substreams: visibleButtonDefault,
|
||||
live: visibleButtonDefault,
|
||||
clips: visibleButtonDefault,
|
||||
snapshots: visibleButtonDefault,
|
||||
@@ -976,6 +988,7 @@ const menuConfigSchema = z
|
||||
.object({
|
||||
frigate: visibleButtonSchema.default(menuConfigDefault.buttons.frigate),
|
||||
cameras: visibleButtonSchema.default(menuConfigDefault.buttons.cameras),
|
||||
substreams: visibleButtonSchema.default(menuConfigDefault.buttons.substreams),
|
||||
live: visibleButtonSchema.default(menuConfigDefault.buttons.live),
|
||||
clips: visibleButtonSchema.default(menuConfigDefault.buttons.clips),
|
||||
snapshots: visibleButtonSchema.default(menuConfigDefault.buttons.snapshots),
|
||||
|
||||
+11
-11
@@ -1,16 +1,16 @@
|
||||
import {
|
||||
ActionConfig,
|
||||
handleActionConfig,
|
||||
hasAction,
|
||||
HomeAssistant
|
||||
ActionConfig,
|
||||
handleActionConfig,
|
||||
hasAction,
|
||||
HomeAssistant,
|
||||
} from 'custom-card-helpers';
|
||||
import {
|
||||
Actions,
|
||||
ActionsConfig,
|
||||
ActionType,
|
||||
FrigateCardAction,
|
||||
FrigateCardCustomAction,
|
||||
frigateCardCustomActionSchema
|
||||
Actions,
|
||||
ActionsConfig,
|
||||
ActionType,
|
||||
FrigateCardAction,
|
||||
FrigateCardCustomAction,
|
||||
frigateCardCustomActionSchema,
|
||||
} from '../types.js';
|
||||
|
||||
/**
|
||||
@@ -43,7 +43,7 @@ export function createFrigateCardCustomAction(
|
||||
media_player_action?: 'play' | 'stop';
|
||||
},
|
||||
): FrigateCardCustomAction | null {
|
||||
if (action === 'camera_select') {
|
||||
if (action === 'camera_select' || action === 'live_substream_select') {
|
||||
if (!args?.camera) {
|
||||
return null;
|
||||
}
|
||||
|
||||
+10
-4
@@ -1,3 +1,4 @@
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import { CameraConfig, RawFrigateCardConfig } from '../types.js';
|
||||
|
||||
/**
|
||||
@@ -25,14 +26,19 @@ export function getCameraID(
|
||||
|
||||
/**
|
||||
* Get all cameras that depend on a given camera.
|
||||
* @param cameras Cameras map.
|
||||
* @param cameraManager The camera manager.
|
||||
* @param cameraID ID of the target camera.
|
||||
* @returns A set of query parameters.
|
||||
* @returns A set of dependent cameraIDs or null.
|
||||
*/
|
||||
export const getAllDependentCameras = (
|
||||
cameras: Map<string, CameraConfig>,
|
||||
cameraManager?: CameraManager,
|
||||
cameraID?: string,
|
||||
): Set<string> => {
|
||||
): Set<string> | null => {
|
||||
if (!cameraManager || !cameraID) {
|
||||
return null;
|
||||
}
|
||||
const cameras = cameraManager.getStore().getCameras();
|
||||
|
||||
const cameraIDs: Set<string> = new Set();
|
||||
const getDependentCameras = (cameraID: string): void => {
|
||||
const cameraConfig = cameras.get(cameraID);
|
||||
|
||||
@@ -53,7 +53,7 @@ export class EntityRegistryManager {
|
||||
public async getExtendedEntity(
|
||||
hass: HomeAssistant,
|
||||
entityID: string,
|
||||
): Promise<ExtendedEntity | null> {
|
||||
): Promise<ExtendedEntity> {
|
||||
const cachedValue = this._extendedCache.get(entityID);
|
||||
if (cachedValue) {
|
||||
return cachedValue;
|
||||
|
||||
@@ -27,11 +27,11 @@ export const changeViewToRecentEventsForCameraAndDependents = async (
|
||||
targetView?: FrigateCardView;
|
||||
},
|
||||
): Promise<void> => {
|
||||
const cameras = cameraManager.getCameras();
|
||||
if (!cameras) {
|
||||
const cameraIDs = getAllDependentCameras(cameraManager, view.camera);
|
||||
if (!cameraIDs) {
|
||||
return;
|
||||
}
|
||||
const cameraIDs = new Set(getAllDependentCameras(cameras, view.camera));
|
||||
|
||||
const queries = createQueriesForEventsView(cameraManager, cardWideConfig, cameraIDs, {
|
||||
mediaType: options?.mediaType,
|
||||
});
|
||||
@@ -83,18 +83,16 @@ export const changeViewToRecentRecordingForCameraAndDependents = async (
|
||||
targetView?: 'recording' | 'recordings';
|
||||
},
|
||||
): Promise<void> => {
|
||||
const cameras = cameraManager.getCameras();
|
||||
if (!cameras) {
|
||||
const cameraIDs = getAllDependentCameras(cameraManager, view.camera);
|
||||
if (!cameraIDs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cameraIDs = new Set(getAllDependentCameras(cameras, view.camera));
|
||||
const queries = createQueriesForRecordingsView(
|
||||
cameraManager,
|
||||
cardWideConfig,
|
||||
cameraIDs,
|
||||
);
|
||||
|
||||
if (!queries) {
|
||||
return;
|
||||
}
|
||||
|
||||
+6
-1
@@ -39,12 +39,17 @@ export class View {
|
||||
* @param curr The current view.
|
||||
* @returns True if the view change is a real media change.
|
||||
*/
|
||||
public static isMediaChange(prev?: View, curr?: View): boolean {
|
||||
public static isMajorMediaChange(prev?: View, curr?: View): boolean {
|
||||
return (
|
||||
!prev ||
|
||||
!curr ||
|
||||
prev.view !== curr.view ||
|
||||
prev.camera !== curr.camera ||
|
||||
// When in live mode, take overrides into account in deciding if this is a
|
||||
// major media change.
|
||||
(curr.view === 'live' &&
|
||||
prev.context?.live?.overrides?.get(prev.camera) !==
|
||||
curr.context?.live?.overrides?.get(curr.camera)) ||
|
||||
// When in the live view, the queryResults contain the events that
|
||||
// happened in the past -- not reflective of the actual live media viewer
|
||||
// the user is seeing.
|
||||
|
||||
Reference in New Issue
Block a user