Merge pull request #963 from dermotduffy/substreams

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