Merge pull request #523 from dermotduffy/event-cameras
Allow cameras to link to dependent_cameras to show events from multiple cameras.
This commit is contained in:
@@ -101,6 +101,7 @@ cameras:
|
|||||||
| `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. |
|
||||||
| `webrtc_card` | | :heavy_multiplication_x: | The WebRTC entity/URL to use for this camera with the `webrtc-card` live provider. See below. |
|
| `webrtc_card` | | :heavy_multiplication_x: | The WebRTC entity/URL to use for this camera with the `webrtc-card` live provider. See below. |
|
||||||
| `id` | `camera_entity`, `webrtc_card.entity` or `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 `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). |
|
||||||
|
| `dependent_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 `dependent_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, or to show events for the `birdseye` camera that otherwise would not have events itself.|
|
||||||
|
|
||||||
<a name="live-providers"></a>
|
<a name="live-providers"></a>
|
||||||
|
|
||||||
@@ -1287,6 +1288,27 @@ card_mod:
|
|||||||
```
|
```
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
### Using a dependent camera
|
||||||
|
|
||||||
|
`dependent_cameras` allows events for other cameras to be shown along with the currently selected camera. For example, this can be used to show events with the `birdseye` camera (since it will not have events of its own).
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Expand: Using dependent cameras with birdseye</summary>
|
||||||
|
|
||||||
|
This example shows events for two other cameras when `birdseye` is selected.
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
[...]
|
||||||
|
cameras:
|
||||||
|
- camera_entity: camera.kitchen
|
||||||
|
- camera_entity: camera.sitting_room
|
||||||
|
- camera_name: birdseye
|
||||||
|
dependent_cameras:
|
||||||
|
- camera.kitchen
|
||||||
|
- camera.sitting_room
|
||||||
|
```
|
||||||
|
</details>
|
||||||
|
|
||||||
<a name="card-updates"></a>
|
<a name="card-updates"></a>
|
||||||
|
|
||||||
## Card Refreshes
|
## Card Refreshes
|
||||||
|
|||||||
@@ -54,5 +54,11 @@ export default [
|
|||||||
format: 'es',
|
format: 'es',
|
||||||
},
|
},
|
||||||
plugins: [...plugins],
|
plugins: [...plugins],
|
||||||
|
// These two files use this at the toplevel, which causes rollup warning
|
||||||
|
// spam on build: `this` has been rewritten to `undefined`
|
||||||
|
moduleContext: {
|
||||||
|
'./node_modules/@formatjs/intl-utils/lib/src/diff.js': 'window',
|
||||||
|
'./node_modules/@formatjs/intl-utils/lib/src/resolve-locale.js': 'window',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
+220
-51
@@ -1,16 +1,18 @@
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
|
|
||||||
import type {
|
import {
|
||||||
BrowseMediaQueryParametersBase,
|
|
||||||
BrowseMediaQueryParameters,
|
BrowseMediaQueryParameters,
|
||||||
FrigateBrowseMediaSource,
|
FrigateBrowseMediaSource,
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
|
MEDIA_CLASS_PLAYLIST,
|
||||||
|
MEDIA_TYPE_PLAYLIST,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
import { View } from './view.js';
|
import { View } from './view.js';
|
||||||
import { frigateBrowseMediaSourceSchema } from './types.js';
|
import { frigateBrowseMediaSourceSchema } from './types.js';
|
||||||
import {
|
import {
|
||||||
dispatchErrorMessageEvent,
|
dispatchErrorMessageEvent,
|
||||||
dispatchMessageEvent,
|
dispatchMessageEvent,
|
||||||
|
getCameraTitle,
|
||||||
homeAssistantWSRequest,
|
homeAssistantWSRequest,
|
||||||
} from './common.js';
|
} from './common.js';
|
||||||
import { localize } from './localize/localize.js';
|
import { localize } from './localize/localize.js';
|
||||||
@@ -73,11 +75,9 @@ export class BrowseMediaUtil {
|
|||||||
type: 'media_source/browse_media',
|
type: 'media_source/browse_media',
|
||||||
media_content_id: media_content_id,
|
media_content_id: media_content_id,
|
||||||
};
|
};
|
||||||
return homeAssistantWSRequest(hass, frigateBrowseMediaSourceSchema, request);
|
return await homeAssistantWSRequest(hass, frigateBrowseMediaSourceSchema, request);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Browse Frigate media with query parameters.
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Browse Frigate media with a media query. May throw.
|
* Browse Frigate media with a media query. May throw.
|
||||||
* @param hass The HomeAssistant object.
|
* @param hass The HomeAssistant object.
|
||||||
@@ -109,13 +109,100 @@ export class BrowseMediaUtil {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browse multiple Frigate media queries. May throw.
|
||||||
|
* @param hass The HomeAssistant object.
|
||||||
|
* @param params An array of search parameters to use to search for media.
|
||||||
|
* @returns A map of FrigateBrowseMediaSource object or null on malformed.
|
||||||
|
*/
|
||||||
|
static async multipleBrowseMediaQuery(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
params: BrowseMediaQueryParameters | BrowseMediaQueryParameters[],
|
||||||
|
): Promise<Map<BrowseMediaQueryParameters, FrigateBrowseMediaSource>> {
|
||||||
|
params = Array.isArray(params) ? params : [params];
|
||||||
|
const output: Map<BrowseMediaQueryParameters, FrigateBrowseMediaSource> = new Map();
|
||||||
|
await Promise.all(
|
||||||
|
params.map(async (param: BrowseMediaQueryParameters): Promise<void> => {
|
||||||
|
output.set(param, await this.browseMediaQuery(hass, param));
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Browse multiple Frigate media queries, then merged them. May throw.
|
||||||
|
* @param hass The HomeAssistant object.
|
||||||
|
* @param params An array of search parameters to use to search for media.
|
||||||
|
* @returns A single FrigateBrowseMediaSource object or null on malformed.
|
||||||
|
*/
|
||||||
|
static async multipleBrowseMediaQueryMerged(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
params: BrowseMediaQueryParameters | BrowseMediaQueryParameters[],
|
||||||
|
): Promise<FrigateBrowseMediaSource> {
|
||||||
|
return this.mergeFrigateBrowseMediaSources(
|
||||||
|
await this.multipleBrowseMediaQuery(hass, params),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Merge multiple FrigateBrowseMediaSource into a single. Note that this may
|
||||||
|
* use information from the query to differentiate results that may otherwise
|
||||||
|
* be identical.
|
||||||
|
* @param input A map of query -> result.
|
||||||
|
* @returns A single FrigateBrowseMediaSource object.
|
||||||
|
*/
|
||||||
|
static mergeFrigateBrowseMediaSources(
|
||||||
|
input: Map<BrowseMediaQueryParameters, FrigateBrowseMediaSource>,
|
||||||
|
): FrigateBrowseMediaSource {
|
||||||
|
const children: FrigateBrowseMediaSource[] = [];
|
||||||
|
|
||||||
|
for (const [query, result] of input.entries()) {
|
||||||
|
for (const child of result.children || []) {
|
||||||
|
if (this.isTrueMedia(child)) {
|
||||||
|
children.push(child);
|
||||||
|
} else {
|
||||||
|
if (query.title) {
|
||||||
|
children.push({ ...child, title: `[${query.title}] ${child.title}` });
|
||||||
|
} else {
|
||||||
|
children.push(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventSort = (
|
||||||
|
a: FrigateBrowseMediaSource,
|
||||||
|
b: FrigateBrowseMediaSource,
|
||||||
|
): number => {
|
||||||
|
if (
|
||||||
|
!a.frigate?.event ||
|
||||||
|
(b.frigate?.event && b.frigate.event.start_time > a.frigate.event.start_time)
|
||||||
|
) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!b.frigate?.event ||
|
||||||
|
(a.frigate?.event && b.frigate.event.start_time < a.frigate.event.start_time)
|
||||||
|
) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
return this.createEventParentForChildren('Merged events', children.sort(eventSort));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the parameters to search for media.
|
* Get the parameters to search for media.
|
||||||
* @returns A BrowseMediaQueryParameters object.
|
* @returns A BrowseMediaQueryParameters object.
|
||||||
*/
|
*/
|
||||||
static getBrowseMediaQueryParametersBase(
|
static getBrowseMediaQueryParameters(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cameraID: string,
|
||||||
cameraConfig?: CameraConfig,
|
cameraConfig?: CameraConfig,
|
||||||
): BrowseMediaQueryParametersBase | null {
|
overrides?: Partial<BrowseMediaQueryParameters>,
|
||||||
|
): BrowseMediaQueryParameters | null {
|
||||||
if (!cameraConfig || !cameraConfig.camera_name) {
|
if (!cameraConfig || !cameraConfig.camera_name) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -124,84 +211,143 @@ export class BrowseMediaUtil {
|
|||||||
cameraName: cameraConfig.camera_name,
|
cameraName: cameraConfig.camera_name,
|
||||||
label: cameraConfig.label,
|
label: cameraConfig.label,
|
||||||
zone: cameraConfig.zone,
|
zone: cameraConfig.zone,
|
||||||
|
title: getCameraTitle(hass, cameraConfig),
|
||||||
|
cameraID: cameraID,
|
||||||
|
...overrides,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set the mediaType parameter from the current view.
|
* Apply overrides to multiple query parameters.
|
||||||
* @param browseMediaQueryParametersBase The base media query parameters object.
|
* @param parameters An array of query parameters.
|
||||||
* @param view The current view.
|
* @param overrides The overrides to apply.
|
||||||
* @returns A fully populated BrowseMediaQueryParameters or null.
|
* @returns The override query parameters.
|
||||||
*/
|
*/
|
||||||
static setMediaTypeFromView(
|
static overrideMultiBrowseMediaQueryParameters(
|
||||||
browseMediaQueryParametersBase: BrowseMediaQueryParametersBase | null,
|
parameters: BrowseMediaQueryParameters[],
|
||||||
view: View,
|
overrides: Partial<BrowseMediaQueryParameters>,
|
||||||
): BrowseMediaQueryParameters | null {
|
): BrowseMediaQueryParameters[] {
|
||||||
if (
|
const output: BrowseMediaQueryParameters[] = [];
|
||||||
!browseMediaQueryParametersBase ||
|
parameters.forEach((param) => {
|
||||||
!(view.isClipRelatedView() || view.isSnapshotRelatedView())
|
output.push({ ...param, ...overrides });
|
||||||
) {
|
});
|
||||||
return null;
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get BrowseMediaQueryParameters for a camera (including its dependencies).
|
||||||
|
* @param hass Home Assistant object.
|
||||||
|
* @param cameras Cameras map.
|
||||||
|
* @param camera Name of the current camera.
|
||||||
|
* @param mediaType Optional media type to include in the parameters.
|
||||||
|
* @returns An array of query parameters.
|
||||||
|
*/
|
||||||
|
static getFullDependentBrowseMediaQueryParameters(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
cameras: Map<string, CameraConfig>,
|
||||||
|
camera: string,
|
||||||
|
mediaType?: 'clips' | 'snapshots',
|
||||||
|
): BrowseMediaQueryParameters[] | null {
|
||||||
|
const cameraIDs: Set<string> = new Set();
|
||||||
|
const getDependentCameras = (camera: string): void => {
|
||||||
|
const cameraConfig = cameras.get(camera);
|
||||||
|
if (cameraConfig) {
|
||||||
|
cameraIDs.add(camera);
|
||||||
|
for (const eventCameraID of cameraConfig.dependent_cameras || []) {
|
||||||
|
if (!cameraIDs.has(eventCameraID)) {
|
||||||
|
getDependentCameras(eventCameraID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
getDependentCameras(camera);
|
||||||
|
|
||||||
|
const params: BrowseMediaQueryParameters[] = [];
|
||||||
|
for (const cameraID of cameraIDs) {
|
||||||
|
const param = BrowseMediaUtil.getBrowseMediaQueryParameters(
|
||||||
|
hass,
|
||||||
|
cameraID,
|
||||||
|
cameras.get(cameraID),
|
||||||
|
mediaType ? { mediaType: mediaType } : {},
|
||||||
|
);
|
||||||
|
// Fail on a single bad camera, as it's almost certainly a user error that
|
||||||
|
// should be fixed rather than hidden.
|
||||||
|
if (!param) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
params.push(param);
|
||||||
}
|
}
|
||||||
return {
|
return params.length ? params : null;
|
||||||
...browseMediaQueryParametersBase,
|
|
||||||
mediaType: view.isClipRelatedView() ? 'clips' : 'snapshots',
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the parameters to search for media related to the current view.
|
* Get BrowseMediaQueryParameters for a camera (including its dependencies) or dispatch an error.
|
||||||
* @returns A BrowseMediaQueryParameters object.
|
* @param element The element from which to dispatch the error.
|
||||||
|
* @param hass Home Assistant object.
|
||||||
|
* @param cameras Cameras map.
|
||||||
|
* @param camera Name of the current camera.
|
||||||
|
* @param mediaType Optional media type to include in the parameters.
|
||||||
|
* @returns An array of query parameters.
|
||||||
*/
|
*/
|
||||||
static getBrowseMediaQueryParametersBaseOrDispatchError(
|
static getFullDependentBrowseMediaQueryParametersOrDispatchError(
|
||||||
node: HTMLElement,
|
element: HTMLElement,
|
||||||
cameraConfig: CameraConfig,
|
hass: HomeAssistant,
|
||||||
): BrowseMediaQueryParametersBase | null {
|
cameras: Map<string, CameraConfig>,
|
||||||
// Verify there is a camera name, otherwise getBrowseMediaQueryParametersBase()
|
camera: string,
|
||||||
// will return undefined.
|
mediaType?: 'clips' | 'snapshots',
|
||||||
if (!cameraConfig.camera_name) {
|
): BrowseMediaQueryParameters[] | null {
|
||||||
|
const params = this.getFullDependentBrowseMediaQueryParameters(
|
||||||
|
hass,
|
||||||
|
cameras,
|
||||||
|
camera,
|
||||||
|
mediaType,
|
||||||
|
);
|
||||||
|
if (!params) {
|
||||||
dispatchErrorMessageEvent(
|
dispatchErrorMessageEvent(
|
||||||
node,
|
element,
|
||||||
localize('error.no_camera_name') + `: ${JSON.stringify(cameraConfig)}`,
|
localize('error.no_camera_name'),
|
||||||
|
cameras.get(camera),
|
||||||
);
|
);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
return params;
|
||||||
return BrowseMediaUtil.getBrowseMediaQueryParametersBase(cameraConfig);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fetch the latest media and dispatch a change view event to reflect the
|
* Fetch the latest media and dispatch a change view event to reflect the
|
||||||
* results. If no media is found a suitable message event will be triggered
|
* results. If no media is found a suitable message event will be triggered
|
||||||
* instead.
|
* instead.
|
||||||
* @param node The HTMLElement to dispatch events from.
|
* @param element The HTMLElement to dispatch events from.
|
||||||
* @param hass The Home Assistant object.
|
* @param hass The Home Assistant object.
|
||||||
* @param view The current view to evolve.
|
* @param view The current view to evolve.
|
||||||
* @param browseMediaQueryParameters The media parameters to query with.
|
* @param browseMediaQueryParameters The media parameters to query with.
|
||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static async fetchLatestMediaAndDispatchViewChange(
|
static async fetchLatestMediaAndDispatchViewChange(
|
||||||
node: HTMLElement,
|
element: HTMLElement,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
view: Readonly<View>,
|
view: Readonly<View>,
|
||||||
browseMediaQueryParameters: BrowseMediaQueryParameters,
|
browseMediaQueryParameters:
|
||||||
|
| BrowseMediaQueryParameters
|
||||||
|
| BrowseMediaQueryParameters[],
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let parent: FrigateBrowseMediaSource | null;
|
let parent: FrigateBrowseMediaSource | null;
|
||||||
try {
|
try {
|
||||||
parent = await BrowseMediaUtil.browseMediaQuery(hass, browseMediaQueryParameters);
|
parent = await BrowseMediaUtil.multipleBrowseMediaQueryMerged(
|
||||||
|
hass,
|
||||||
|
browseMediaQueryParameters,
|
||||||
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return dispatchErrorMessageEvent(node, (e as Error).message);
|
return dispatchErrorMessageEvent(element, (e as Error).message);
|
||||||
}
|
}
|
||||||
const childIndex = BrowseMediaUtil.getFirstTrueMediaChildIndex(parent);
|
const childIndex = BrowseMediaUtil.getFirstTrueMediaChildIndex(parent);
|
||||||
if (!parent || !parent.children || childIndex == null) {
|
if (!parent || !parent.children || childIndex == null) {
|
||||||
return dispatchMessageEvent(
|
return dispatchMessageEvent(
|
||||||
node,
|
element,
|
||||||
browseMediaQueryParameters.mediaType == 'clips'
|
view.isClipRelatedView()
|
||||||
? localize('common.no_clip')
|
? localize('common.no_clip')
|
||||||
: localize('common.no_snapshot'),
|
: localize('common.no_snapshot'),
|
||||||
browseMediaQueryParameters.mediaType == 'clips'
|
view.isClipRelatedView() ? 'mdi:filmstrip-off' : 'mdi:camera-off',
|
||||||
? 'mdi:filmstrip-off'
|
|
||||||
: 'mdi:camera-off',
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,7 +356,7 @@ export class BrowseMediaUtil {
|
|||||||
target: parent,
|
target: parent,
|
||||||
childIndex: childIndex,
|
childIndex: childIndex,
|
||||||
})
|
})
|
||||||
.dispatchChangeEvent(node);
|
.dispatchChangeEvent(element);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -223,7 +369,7 @@ export class BrowseMediaUtil {
|
|||||||
* @returns
|
* @returns
|
||||||
*/
|
*/
|
||||||
static async fetchChildMediaAndDispatchViewChange(
|
static async fetchChildMediaAndDispatchViewChange(
|
||||||
node: HTMLElement,
|
element: HTMLElement,
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
view: Readonly<View>,
|
view: Readonly<View>,
|
||||||
child: Readonly<FrigateBrowseMediaSource>,
|
child: Readonly<FrigateBrowseMediaSource>,
|
||||||
@@ -232,13 +378,36 @@ export class BrowseMediaUtil {
|
|||||||
try {
|
try {
|
||||||
parent = await BrowseMediaUtil.browseMedia(hass, child.media_content_id);
|
parent = await BrowseMediaUtil.browseMedia(hass, child.media_content_id);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return dispatchErrorMessageEvent(node, (e as Error).message);
|
return dispatchErrorMessageEvent(element, (e as Error).message);
|
||||||
}
|
}
|
||||||
|
|
||||||
view
|
view
|
||||||
.evolve({
|
.evolve({
|
||||||
target: parent,
|
target: parent,
|
||||||
})
|
})
|
||||||
.dispatchChangeEvent(node);
|
.dispatchChangeEvent(element);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given an array of media children, create a parent for them.
|
||||||
|
* @param title The title to use for the parent.
|
||||||
|
* @param children The children media items.
|
||||||
|
* @returns A single parent containing the children.
|
||||||
|
*/
|
||||||
|
static createEventParentForChildren(
|
||||||
|
title: string,
|
||||||
|
children: FrigateBrowseMediaSource[],
|
||||||
|
): FrigateBrowseMediaSource {
|
||||||
|
return {
|
||||||
|
title: title,
|
||||||
|
media_class: MEDIA_CLASS_PLAYLIST,
|
||||||
|
media_content_type: MEDIA_TYPE_PLAYLIST,
|
||||||
|
media_content_id: '',
|
||||||
|
can_play: false,
|
||||||
|
can_expand: true,
|
||||||
|
children_media_class: MEDIA_CLASS_PLAYLIST,
|
||||||
|
thumbnail: null,
|
||||||
|
children: children,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-14
@@ -8,7 +8,7 @@ import {
|
|||||||
} from 'lit';
|
} from 'lit';
|
||||||
import { HomeAssistant, LovelaceCardEditor, getLovelace } from 'custom-card-helpers';
|
import { HomeAssistant, LovelaceCardEditor, getLovelace } from 'custom-card-helpers';
|
||||||
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
import { StyleInfo, styleMap } from 'lit/directives/style-map.js';
|
||||||
import { customElement, property, query, state } from 'lit/decorators.js';
|
import { customElement, property, state } from 'lit/decorators.js';
|
||||||
import { classMap } from 'lit/directives/class-map.js';
|
import { classMap } from 'lit/directives/class-map.js';
|
||||||
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
import { createRef, ref, Ref } from 'lit/directives/ref.js';
|
||||||
import screenfull from 'screenfull';
|
import screenfull from 'screenfull';
|
||||||
@@ -35,7 +35,7 @@ import type {
|
|||||||
Message,
|
Message,
|
||||||
} from './types.js';
|
} from './types.js';
|
||||||
|
|
||||||
import { CARD_VERSION, REPO_URL } from './const.js';
|
import { CAMERA_BIRDSEYE, CARD_VERSION, REPO_URL } from './const.js';
|
||||||
import { FrigateCardElements } from './components/elements.js';
|
import { FrigateCardElements } from './components/elements.js';
|
||||||
import { FrigateCardImage } from './components/image.js';
|
import { FrigateCardImage } from './components/image.js';
|
||||||
import { FRIGATE_BUTTON_MENU_ICON, FrigateCardMenu } from './components/menu.js';
|
import { FRIGATE_BUTTON_MENU_ICON, FrigateCardMenu } from './components/menu.js';
|
||||||
@@ -48,6 +48,7 @@ import {
|
|||||||
frigateCardHasAction,
|
frigateCardHasAction,
|
||||||
getActionConfigGivenAction,
|
getActionConfigGivenAction,
|
||||||
getCameraIcon,
|
getCameraIcon,
|
||||||
|
getCameraID,
|
||||||
getCameraTitle,
|
getCameraTitle,
|
||||||
homeAssistantSignPath,
|
homeAssistantSignPath,
|
||||||
homeAssistantWSRequest,
|
homeAssistantWSRequest,
|
||||||
@@ -328,11 +329,12 @@ export class FrigateCard extends LitElement {
|
|||||||
const cameraConfig = this._getSelectedCameraConfig();
|
const cameraConfig = this._getSelectedCameraConfig();
|
||||||
|
|
||||||
// Don't show `clips` button if there's no `camera_name` (e.g. non-Frigate
|
// Don't show `clips` button if there's no `camera_name` (e.g. non-Frigate
|
||||||
// cameras), or is birdseye.
|
// cameras), or is birdseye (unless there are dependent cameras).
|
||||||
if (
|
if (
|
||||||
this._getConfig().menu.buttons.clips &&
|
this._getConfig().menu.buttons.clips &&
|
||||||
cameraConfig?.camera_name &&
|
cameraConfig?.camera_name &&
|
||||||
cameraConfig?.camera_name !== 'birdseye'
|
(cameraConfig?.camera_name !== CAMERA_BIRDSEYE ||
|
||||||
|
cameraConfig?.dependent_cameras?.length)
|
||||||
) {
|
) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
@@ -345,11 +347,12 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't show `snapshots` button if there's no `camera_name` (e.g. non-Frigate
|
// Don't show `snapshots` button if there's no `camera_name` (e.g. non-Frigate
|
||||||
// cameras), or is birdseye.
|
// cameras), or is birdseye (unless there are dependent cameras).
|
||||||
if (
|
if (
|
||||||
this._getConfig().menu.buttons.snapshots &&
|
this._getConfig().menu.buttons.snapshots &&
|
||||||
cameraConfig?.camera_name &&
|
cameraConfig?.camera_name &&
|
||||||
cameraConfig?.camera_name !== 'birdseye'
|
(cameraConfig?.camera_name !== CAMERA_BIRDSEYE ||
|
||||||
|
cameraConfig?.dependent_cameras?.length)
|
||||||
) {
|
) {
|
||||||
buttons.push({
|
buttons.push({
|
||||||
type: 'custom:frigate-card-menu-icon',
|
type: 'custom:frigate-card-menu-icon',
|
||||||
@@ -465,12 +468,7 @@ export class FrigateCard extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const id =
|
const id = getCameraID(config);
|
||||||
config.id ||
|
|
||||||
config.camera_entity ||
|
|
||||||
config.webrtc_card?.entity ||
|
|
||||||
config.camera_name;
|
|
||||||
|
|
||||||
if (!id) {
|
if (!id) {
|
||||||
this._setMessageAndUpdate({
|
this._setMessageAndUpdate({
|
||||||
message: localize('error.no_camera_id'),
|
message: localize('error.no_camera_id'),
|
||||||
@@ -1299,7 +1297,7 @@ export class FrigateCard extends LitElement {
|
|||||||
? html` <frigate-card-gallery
|
? html` <frigate-card-gallery
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.cameraConfig=${cameraConfig}
|
.cameras=${this._cameras}
|
||||||
.galleryConfig=${this._getConfig().event_gallery}
|
.galleryConfig=${this._getConfig().event_gallery}
|
||||||
>
|
>
|
||||||
</frigate-card-gallery>`
|
</frigate-card-gallery>`
|
||||||
@@ -1308,7 +1306,7 @@ export class FrigateCard extends LitElement {
|
|||||||
? html` <frigate-card-viewer
|
? html` <frigate-card-viewer
|
||||||
.hass=${this._hass}
|
.hass=${this._hass}
|
||||||
.view=${this._view}
|
.view=${this._view}
|
||||||
.cameraConfig=${cameraConfig}
|
.cameras=${this._cameras}
|
||||||
.viewerConfig=${this._getConfig().event_viewer}
|
.viewerConfig=${this._getConfig().event_viewer}
|
||||||
.resolvedMediaCache=${this._resolvedMediaCache}
|
.resolvedMediaCache=${this._resolvedMediaCache}
|
||||||
>
|
>
|
||||||
|
|||||||
+39
-5
@@ -28,6 +28,7 @@ import {
|
|||||||
FrigateEvent,
|
FrigateEvent,
|
||||||
MediaShowInfo,
|
MediaShowInfo,
|
||||||
Message,
|
Message,
|
||||||
|
RawFrigateCardConfig,
|
||||||
SignedPath,
|
SignedPath,
|
||||||
signedPathSchema,
|
signedPathSchema,
|
||||||
StateParameters,
|
StateParameters,
|
||||||
@@ -463,20 +464,53 @@ export function getEntityIcon(
|
|||||||
return hass && entity ? stateIcon(hass.states[entity]) : undefined;
|
return hass && entity ? stateIcon(hass.states[entity]) : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a camera id.
|
||||||
|
* @param config The camera config (either parsed or raw).
|
||||||
|
* @returns A camera id.
|
||||||
|
*/
|
||||||
|
export function getCameraID(
|
||||||
|
config?: CameraConfig | RawFrigateCardConfig | null,
|
||||||
|
): string {
|
||||||
|
return (
|
||||||
|
(typeof config?.id === 'string' && config.id) ||
|
||||||
|
(typeof config?.camera_entity === 'string' && config.camera_entity) ||
|
||||||
|
(typeof config?.webrtc_card === 'object' &&
|
||||||
|
config.webrtc_card &&
|
||||||
|
typeof config.webrtc_card['entity'] === 'string' &&
|
||||||
|
config.webrtc_card['entity']) ||
|
||||||
|
(typeof config?.camera_name === 'string' && config.camera_name) ||
|
||||||
|
''
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a camera text title.
|
* Get a camera text title.
|
||||||
* @param hass The Home Assistant object.
|
* @param hass The Home Assistant object.
|
||||||
* @param config The camera config.
|
* @param config The camera config (either parsed or raw).
|
||||||
* @returns A title string.
|
* @returns A title string.
|
||||||
*/
|
*/
|
||||||
export function getCameraTitle(
|
export function getCameraTitle(
|
||||||
hass?: HomeAssistant,
|
hass?: HomeAssistant,
|
||||||
config?: CameraConfig | null,
|
config?: CameraConfig | RawFrigateCardConfig | null,
|
||||||
): string {
|
): string {
|
||||||
|
// Attempt to render a recognizable name for the camera,
|
||||||
|
// starting with the most likely to be useful and working our
|
||||||
|
// ways towards the least useful. Extra type checking here since this is also
|
||||||
|
// used on raw configuration in the editor.
|
||||||
return (
|
return (
|
||||||
config?.title ||
|
(typeof config?.title === 'string' && config.title) ||
|
||||||
(config?.camera_entity ? getEntityTitle(hass, config.camera_entity) : '') ||
|
(typeof config?.camera_entity === 'string'
|
||||||
(config?.camera_name ? prettifyFrigateName(config.camera_name) : '') ||
|
? getEntityTitle(hass, config.camera_entity)
|
||||||
|
: '') ||
|
||||||
|
(typeof config?.webrtc_card === 'object' &&
|
||||||
|
config.webrtc_card &&
|
||||||
|
typeof config.webrtc_card['entity'] === 'string' &&
|
||||||
|
config.webrtc_card['entity']) ||
|
||||||
|
(typeof config?.camera_name === 'string'
|
||||||
|
? prettifyFrigateName(config.camera_name)
|
||||||
|
: '') ||
|
||||||
|
(typeof config?.id === 'string' && config.id) ||
|
||||||
''
|
''
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+19
-14
@@ -11,11 +11,7 @@ import {
|
|||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
import { customElement, property } from 'lit/decorators.js';
|
import { customElement, property } from 'lit/decorators.js';
|
||||||
|
|
||||||
import {
|
import { CameraConfig, GalleryConfig, frigateCardConfigDefaults } from '../types.js';
|
||||||
CameraConfig,
|
|
||||||
GalleryConfig,
|
|
||||||
frigateCardConfigDefaults,
|
|
||||||
} from '../types.js';
|
|
||||||
import { BrowseMediaUtil } from '../browse-media-util.js';
|
import { BrowseMediaUtil } from '../browse-media-util.js';
|
||||||
import { View } from '../view.js';
|
import { View } from '../view.js';
|
||||||
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
import { THUMBNAIL_DETAILS_WIDTH_MIN } from './thumbnail.js';
|
||||||
@@ -35,28 +31,37 @@ export class FrigateCardGallery extends LitElement {
|
|||||||
protected view?: Readonly<View>;
|
protected view?: Readonly<View>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected cameraConfig?: CameraConfig;
|
protected galleryConfig?: GalleryConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected galleryConfig?: GalleryConfig;
|
protected cameras?: Map<string, CameraConfig>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Master render method.
|
* Master render method.
|
||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass || !this.view || !this.cameraConfig || !this.view.isGalleryView()) {
|
const mediaType = this.view?.getMediaType();
|
||||||
|
if (
|
||||||
|
!this.hass ||
|
||||||
|
!this.view ||
|
||||||
|
!this.cameras ||
|
||||||
|
!this.view.isGalleryView() ||
|
||||||
|
!mediaType
|
||||||
|
) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!this.view.target) {
|
if (!this.view.target) {
|
||||||
const browseMediaQueryParameters = BrowseMediaUtil.setMediaTypeFromView(
|
const browseMediaQueryParameters =
|
||||||
BrowseMediaUtil.getBrowseMediaQueryParametersBaseOrDispatchError(
|
BrowseMediaUtil.getFullDependentBrowseMediaQueryParametersOrDispatchError(
|
||||||
this,
|
this,
|
||||||
this.cameraConfig,
|
this.hass,
|
||||||
),
|
this.cameras,
|
||||||
this.view,
|
this.view.camera,
|
||||||
);
|
mediaType,
|
||||||
|
);
|
||||||
|
|
||||||
if (!browseMediaQueryParameters) {
|
if (!browseMediaQueryParameters) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-9
@@ -19,7 +19,6 @@ import {
|
|||||||
LiveProvider,
|
LiveProvider,
|
||||||
TransitionEffect,
|
TransitionEffect,
|
||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
BrowseMediaQueryParameters,
|
|
||||||
} from '../types.js';
|
} from '../types.js';
|
||||||
import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel';
|
import { EmblaOptionsType, EmblaPluginType } from 'embla-carousel';
|
||||||
import { HomeAssistant } from 'custom-card-helpers';
|
import { HomeAssistant } from 'custom-card-helpers';
|
||||||
@@ -127,16 +126,18 @@ export class FrigateCardLive extends LitElement {
|
|||||||
this.conditionState,
|
this.conditionState,
|
||||||
) as LiveConfig;
|
) as LiveConfig;
|
||||||
|
|
||||||
const browseMediaParamsBase = BrowseMediaUtil.getBrowseMediaQueryParametersBase(
|
const browseMediaParams =
|
||||||
this.cameras.get(this.view.camera),
|
BrowseMediaUtil.getFullDependentBrowseMediaQueryParametersOrDispatchError(
|
||||||
);
|
this,
|
||||||
if (!browseMediaParamsBase) {
|
this.hass,
|
||||||
|
this.cameras,
|
||||||
|
this.view.camera,
|
||||||
|
config.controls.thumbnails.media,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!browseMediaParams) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const browseMediaParams: BrowseMediaQueryParameters = {
|
|
||||||
...browseMediaParamsBase,
|
|
||||||
mediaType: config.controls.thumbnails.media,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note use of liveConfig and not config below -- the carousel will
|
// Note use of liveConfig and not config below -- the carousel will
|
||||||
// independently override the liveconfig to reflect the camera in the
|
// independently override the liveconfig to reflect the camera in the
|
||||||
|
|||||||
@@ -34,7 +34,9 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
protected targetView?: FrigateCardView;
|
protected targetView?: FrigateCardView;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected browseMediaParams?: BrowseMediaQueryParameters;
|
protected browseMediaParams?:
|
||||||
|
| BrowseMediaQueryParameters
|
||||||
|
| BrowseMediaQueryParameters[];
|
||||||
|
|
||||||
// A task to await the load of the WebRTC component.
|
// A task to await the load of the WebRTC component.
|
||||||
protected _browseTask = new Task(this, this._fetchMedia.bind(this), () => [
|
protected _browseTask = new Task(this, this._fetchMedia.bind(this), () => [
|
||||||
@@ -53,22 +55,25 @@ export class FrigateCardSurround extends LitElement {
|
|||||||
| HomeAssistant
|
| HomeAssistant
|
||||||
| Readonly<View>
|
| Readonly<View>
|
||||||
| BrowseMediaQueryParameters
|
| BrowseMediaQueryParameters
|
||||||
|
| BrowseMediaQueryParameters[]
|
||||||
| undefined
|
| undefined
|
||||||
)[]): Promise<void> {
|
)[]): Promise<void> {
|
||||||
hass = hass as HomeAssistant;
|
hass = hass as HomeAssistant;
|
||||||
view = view as Readonly<View>;
|
view = view as Readonly<View>;
|
||||||
browseMediaParams = browseMediaParams as BrowseMediaQueryParameters;
|
browseMediaParams = browseMediaParams as
|
||||||
|
| BrowseMediaQueryParameters
|
||||||
|
| BrowseMediaQueryParameters[];
|
||||||
|
|
||||||
if (!hass || !view || view.target || !browseMediaParams) {
|
if (!hass || !view || view.target || !browseMediaParams) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let parent: FrigateBrowseMediaSource | null;
|
let parent: FrigateBrowseMediaSource | null;
|
||||||
try {
|
try {
|
||||||
parent = await BrowseMediaUtil.browseMediaQuery(hass, browseMediaParams);
|
parent = await BrowseMediaUtil.multipleBrowseMediaQueryMerged(hass, browseMediaParams);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return dispatchErrorMessageEvent(this, (e as Error).message);
|
return dispatchErrorMessageEvent(this, (e as Error).message);
|
||||||
}
|
}
|
||||||
if (BrowseMediaUtil.getFirstTrueMediaChildIndex(parent) != null) {
|
if (BrowseMediaUtil.getFirstTrueMediaChildIndex(parent) !== null) {
|
||||||
this.view
|
this.view
|
||||||
?.evolve({
|
?.evolve({
|
||||||
...(this.targetView && { view: this.targetView }),
|
...(this.targetView && { view: this.targetView }),
|
||||||
|
|||||||
+55
-59
@@ -25,14 +25,14 @@ import { isEqual } from 'lodash-es';
|
|||||||
|
|
||||||
import { BrowseMediaUtil } from '../browse-media-util';
|
import { BrowseMediaUtil } from '../browse-media-util';
|
||||||
import {
|
import {
|
||||||
|
BrowseMediaQueryParameters,
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
FrigateBrowseMediaSource,
|
FrigateBrowseMediaSource,
|
||||||
MEDIA_CLASS_PLAYLIST,
|
|
||||||
MEDIA_TYPE_PLAYLIST,
|
|
||||||
TimelineConfig,
|
TimelineConfig,
|
||||||
FrigateEvent,
|
FrigateEvent,
|
||||||
frigateCardConfigDefaults,
|
frigateCardConfigDefaults,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
import { CAMERA_BIRDSEYE } from '../const';
|
||||||
import { View, ViewContext } from '../view';
|
import { View, ViewContext } from '../view';
|
||||||
import {
|
import {
|
||||||
dispatchErrorMessageEvent,
|
dispatchErrorMessageEvent,
|
||||||
@@ -278,49 +278,51 @@ class TimelineEventManager {
|
|||||||
}
|
}
|
||||||
this._dateFetch = new Date();
|
this._dateFetch = new Date();
|
||||||
|
|
||||||
const fetchCameraEvents = async (
|
const params: BrowseMediaQueryParameters[] = [];
|
||||||
camera: string,
|
cameras.forEach((cameraConfig, cameraID) => {
|
||||||
fetchMedia: 'clips' | 'snapshots',
|
(media === 'all' ? ['clips', 'snapshots'] : [media]).forEach((mediaType) => {
|
||||||
): Promise<void> => {
|
if (
|
||||||
const cameraConfig = cameras.get(camera);
|
this._dateEnd &&
|
||||||
if (!cameraConfig || !this._dateStart || !this._dateEnd) {
|
this._dateStart &&
|
||||||
return;
|
cameraConfig.camera_name !== CAMERA_BIRDSEYE
|
||||||
}
|
) {
|
||||||
const browseMediaQueryParametersBase =
|
const param = BrowseMediaUtil.getBrowseMediaQueryParameters(
|
||||||
BrowseMediaUtil.getBrowseMediaQueryParametersBase(cameraConfig);
|
hass,
|
||||||
if (!browseMediaQueryParametersBase) {
|
cameraID,
|
||||||
return;
|
cameraConfig,
|
||||||
}
|
{
|
||||||
try {
|
// Events are always fetched for the maximum extent of the managed
|
||||||
this._addMediaSource(
|
// range. This is because events may change at any point in time
|
||||||
camera,
|
// (e.g. a long-running event that ends).
|
||||||
media,
|
before: this._dateEnd.getTime() / 1000,
|
||||||
await BrowseMediaUtil.browseMediaQuery(hass, {
|
after: this._dateStart.getTime() / 1000,
|
||||||
...browseMediaQueryParametersBase,
|
unlimited: true,
|
||||||
|
mediaType: mediaType as 'clips' | 'snapshots',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
if (param) {
|
||||||
|
params.push(param);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Events are always fetched for the maximum extent of the managed
|
if (!params.length) {
|
||||||
// range. This is because events may change at any point in time
|
return;
|
||||||
// (e.g. a long-running event that ends).
|
}
|
||||||
before: this._dateEnd.getTime() / 1000,
|
|
||||||
after: this._dateStart.getTime() / 1000,
|
|
||||||
unlimited: true,
|
|
||||||
mediaType: fetchMedia,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
return dispatchErrorMessageEvent(element, (e as Error).message);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const promises: Promise<void>[] = [];
|
let results: Map<BrowseMediaQueryParameters, FrigateBrowseMediaSource>;
|
||||||
(media === 'all' ? ['clips', 'snapshots'] : [media]).forEach((mediaType) =>
|
try {
|
||||||
promises.push(
|
results = await BrowseMediaUtil.multipleBrowseMediaQuery(hass, params);
|
||||||
...Array.from(cameras.keys()).map((camera) =>
|
} catch (e) {
|
||||||
fetchCameraEvents(camera, mediaType as 'clips' | 'snapshots'),
|
return dispatchErrorMessageEvent(element, (e as Error).message);
|
||||||
),
|
}
|
||||||
),
|
|
||||||
);
|
for (const [query, result] of results.entries()) {
|
||||||
await Promise.all(promises);
|
if (query.cameraID) {
|
||||||
|
this._addMediaSource(query.cameraID, media, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -574,18 +576,10 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = {
|
const target = BrowseMediaUtil.createEventParentForChildren(
|
||||||
title: `Timeline events`,
|
'Timeline events',
|
||||||
media_class: MEDIA_CLASS_PLAYLIST,
|
children,
|
||||||
media_content_type: MEDIA_TYPE_PLAYLIST,
|
);
|
||||||
media_content_id: '',
|
|
||||||
can_play: false,
|
|
||||||
can_expand: true,
|
|
||||||
children_media_class: MEDIA_CLASS_PLAYLIST,
|
|
||||||
thumbnail: null,
|
|
||||||
children: children,
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
target: target,
|
target: target,
|
||||||
childIndex: childIndex < 0 ? null : childIndex,
|
childIndex: childIndex < 0 ? null : childIndex,
|
||||||
@@ -599,10 +593,12 @@ export class FrigateCardTimelineCore extends LitElement {
|
|||||||
protected _getGroups(): DataGroupCollectionType {
|
protected _getGroups(): DataGroupCollectionType {
|
||||||
const groups: FrigateCardGroupData[] = [];
|
const groups: FrigateCardGroupData[] = [];
|
||||||
this.cameras?.forEach((cameraConfig, camera) => {
|
this.cameras?.forEach((cameraConfig, camera) => {
|
||||||
groups.push({
|
if (cameraConfig.camera_name !== CAMERA_BIRDSEYE) {
|
||||||
id: camera,
|
groups.push({
|
||||||
content: getCameraTitle(this.hass, cameraConfig),
|
id: camera,
|
||||||
});
|
content: getCameraTitle(this.hass, cameraConfig),
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return new DataSet(groups);
|
return new DataSet(groups);
|
||||||
}
|
}
|
||||||
|
|||||||
+29
-19
@@ -17,7 +17,7 @@ import { ref } from 'lit/directives/ref.js';
|
|||||||
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
import { AutoMediaPlugin } from './embla-plugins/automedia.js';
|
||||||
import type {
|
import type {
|
||||||
BrowseMediaNeighbors,
|
BrowseMediaNeighbors,
|
||||||
BrowseMediaQueryParametersBase,
|
BrowseMediaQueryParameters,
|
||||||
FrigateBrowseMediaSource,
|
FrigateBrowseMediaSource,
|
||||||
CameraConfig,
|
CameraConfig,
|
||||||
MediaShowInfo,
|
MediaShowInfo,
|
||||||
@@ -54,7 +54,7 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
protected viewerConfig?: ViewerConfig;
|
protected viewerConfig?: ViewerConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected cameraConfig?: CameraConfig;
|
protected cameras?: Map<string, CameraConfig>;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected resolvedMediaCache?: ResolvedMediaCache;
|
protected resolvedMediaCache?: ResolvedMediaCache;
|
||||||
@@ -64,22 +64,25 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
* @returns A rendered template.
|
* @returns A rendered template.
|
||||||
*/
|
*/
|
||||||
protected render(): TemplateResult | void {
|
protected render(): TemplateResult | void {
|
||||||
if (!this.hass || !this.view || !this.cameraConfig || !this.viewerConfig) {
|
if (!this.hass || !this.view || !this.cameras || !this.viewerConfig) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const browseMediaQueryParametersBase =
|
const browseMediaQueryParameters =
|
||||||
BrowseMediaUtil.getBrowseMediaQueryParametersBaseOrDispatchError(
|
BrowseMediaUtil.getFullDependentBrowseMediaQueryParametersOrDispatchError(
|
||||||
this,
|
this,
|
||||||
this.cameraConfig,
|
this.hass,
|
||||||
|
this.cameras,
|
||||||
|
this.view.camera,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!this.view.target) {
|
if (!this.view.target) {
|
||||||
const browseMediaQueryParameters = BrowseMediaUtil.setMediaTypeFromView(
|
// If the target is not specified, the view must tell us which mediaType
|
||||||
browseMediaQueryParametersBase,
|
// to search for. When the target *is* specified, the view is not required
|
||||||
this.view,
|
// to indicate the media type (e.g. the mixed 'events' view from the
|
||||||
);
|
// timeline).
|
||||||
if (!browseMediaQueryParameters) {
|
const mediaType = this.view.getMediaType();
|
||||||
|
if (!browseMediaQueryParameters || !mediaType) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,7 +90,10 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
this,
|
this,
|
||||||
this.hass,
|
this.hass,
|
||||||
this.view,
|
this.view,
|
||||||
browseMediaQueryParameters,
|
BrowseMediaUtil.overrideMultiBrowseMediaQueryParameters(
|
||||||
|
browseMediaQueryParameters,
|
||||||
|
{ mediaType: mediaType },
|
||||||
|
),
|
||||||
);
|
);
|
||||||
return renderProgressIndicator();
|
return renderProgressIndicator();
|
||||||
}
|
}
|
||||||
@@ -101,7 +107,7 @@ export class FrigateCardViewer extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.view=${this.view}
|
.view=${this.view}
|
||||||
.viewerConfig=${this.viewerConfig}
|
.viewerConfig=${this.viewerConfig}
|
||||||
.browseMediaQueryParametersBase=${browseMediaQueryParametersBase}
|
.browseMediaQueryParameters=${browseMediaQueryParameters}
|
||||||
.resolvedMediaCache=${this.resolvedMediaCache}
|
.resolvedMediaCache=${this.resolvedMediaCache}
|
||||||
>
|
>
|
||||||
</frigate-card-viewer-carousel>
|
</frigate-card-viewer-carousel>
|
||||||
@@ -133,7 +139,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
protected viewerConfig?: ViewerConfig;
|
protected viewerConfig?: ViewerConfig;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected browseMediaQueryParametersBase?: BrowseMediaQueryParametersBase;
|
protected browseMediaQueryParameters?: BrowseMediaQueryParameters[] | null;
|
||||||
|
|
||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected resolvedMediaCache?: ResolvedMediaCache;
|
protected resolvedMediaCache?: ResolvedMediaCache;
|
||||||
@@ -338,7 +344,7 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
!this.view.target ||
|
!this.view.target ||
|
||||||
!this.view.target.children ||
|
!this.view.target.children ||
|
||||||
!this.view.target.children.length ||
|
!this.view.target.children.length ||
|
||||||
!this.browseMediaQueryParametersBase
|
!this.browseMediaQueryParameters
|
||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -379,13 +385,17 @@ export class FrigateCardViewerCarousel extends FrigateCardMediaCarousel {
|
|||||||
|
|
||||||
let clips: FrigateBrowseMediaSource | null;
|
let clips: FrigateBrowseMediaSource | null;
|
||||||
|
|
||||||
try {
|
const params = BrowseMediaUtil.overrideMultiBrowseMediaQueryParameters(
|
||||||
clips = await BrowseMediaUtil.browseMediaQuery(this.hass, {
|
this.browseMediaQueryParameters,
|
||||||
...this.browseMediaQueryParametersBase,
|
{
|
||||||
mediaType: 'clips',
|
mediaType: 'clips',
|
||||||
before: latest,
|
before: latest,
|
||||||
after: earliest,
|
after: earliest,
|
||||||
});
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
try {
|
||||||
|
clips = await BrowseMediaUtil.multipleBrowseMediaQueryMerged(this.hass, params);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// This is best effort.
|
// This is best effort.
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
export const CARD_VERSION = '3.0.0' as const;
|
export const CARD_VERSION = '3.0.0' as const;
|
||||||
|
|
||||||
|
export const CAMERA_BIRDSEYE = 'birdseye' as const;
|
||||||
export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card' as const;
|
export const REPO_URL = 'https://github.com/dermotduffy/frigate-hass-card' as const;
|
||||||
export const TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting` as const;
|
export const TROUBLESHOOTING_URL = `${REPO_URL}#troubleshooting` as const;
|
||||||
|
|
||||||
@@ -18,6 +20,8 @@ export const CONF_CAMERAS_ARRAY_WEBRTC_CARD_ENTITY =
|
|||||||
export const CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL = `${CONF_CAMERAS}.#.webrtc_card.url` as const;
|
export const CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL = `${CONF_CAMERAS}.#.webrtc_card.url` as const;
|
||||||
export const CONF_CAMERAS_ARRAY_LIVE_PROVIDER =
|
export const CONF_CAMERAS_ARRAY_LIVE_PROVIDER =
|
||||||
`${CONF_CAMERAS}.#.live_provider` as const;
|
`${CONF_CAMERAS}.#.live_provider` as const;
|
||||||
|
export const CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS =
|
||||||
|
`${CONF_CAMERAS}.#.dependent_cameras` as const;
|
||||||
|
|
||||||
export const CONF_VIEW = 'view' as const;
|
export const CONF_VIEW = 'view' as const;
|
||||||
export const CONF_VIEW_CAMERA_SELECT = `${CONF_VIEW}.camera_select` as const;
|
export const CONF_VIEW_CAMERA_SELECT = `${CONF_VIEW}.camera_select` as const;
|
||||||
|
|||||||
+64
-45
@@ -18,6 +18,7 @@ import {
|
|||||||
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
|
CONF_CAMERAS_ARRAY_CAMERA_ENTITY,
|
||||||
CONF_CAMERAS_ARRAY_CAMERA_NAME,
|
CONF_CAMERAS_ARRAY_CAMERA_NAME,
|
||||||
CONF_CAMERAS_ARRAY_CLIENT_ID,
|
CONF_CAMERAS_ARRAY_CLIENT_ID,
|
||||||
|
CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS,
|
||||||
CONF_CAMERAS_ARRAY_ICON,
|
CONF_CAMERAS_ARRAY_ICON,
|
||||||
CONF_CAMERAS_ARRAY_ID,
|
CONF_CAMERAS_ARRAY_ID,
|
||||||
CONF_CAMERAS_ARRAY_LABEL,
|
CONF_CAMERAS_ARRAY_LABEL,
|
||||||
@@ -88,7 +89,11 @@ import {
|
|||||||
CONF_VIEW_UPDATE_FORCE,
|
CONF_VIEW_UPDATE_FORCE,
|
||||||
CONF_VIEW_UPDATE_SECONDS,
|
CONF_VIEW_UPDATE_SECONDS,
|
||||||
} from './const.js';
|
} from './const.js';
|
||||||
import { arrayMove, getEntityTitle, prettifyFrigateName } from './common.js';
|
import {
|
||||||
|
arrayMove,
|
||||||
|
getCameraID,
|
||||||
|
getCameraTitle,
|
||||||
|
} from './common.js';
|
||||||
import {
|
import {
|
||||||
copyConfig,
|
copyConfig,
|
||||||
deleteConfigValue,
|
deleteConfigValue,
|
||||||
@@ -119,6 +124,11 @@ interface EditorOptionSetTarget {
|
|||||||
optionSetName: string;
|
optionSetName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface EditorSelectOption {
|
||||||
|
value: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
|
||||||
const options: EditorOptions = {
|
const options: EditorOptions = {
|
||||||
cameras: {
|
cameras: {
|
||||||
icon: 'video',
|
icon: 'video',
|
||||||
@@ -193,7 +203,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
@property({ attribute: false })
|
@property({ attribute: false })
|
||||||
protected _expandedCameraIndex: number | null = null;
|
protected _expandedCameraIndex: number | null = null;
|
||||||
|
|
||||||
protected _viewModes = [
|
protected _viewModes: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'live', label: localize('config.view.views.live') },
|
{ value: 'live', label: localize('config.view.views.live') },
|
||||||
{ value: 'clips', label: localize('config.view.views.clips') },
|
{ value: 'clips', label: localize('config.view.views.clips') },
|
||||||
@@ -204,12 +214,12 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
{ value: 'timeline', label: localize('config.view.views.timeline') },
|
{ value: 'timeline', label: localize('config.view.views.timeline') },
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _cameraSelectViewModes = [
|
protected _cameraSelectViewModes: EditorSelectOption[] = [
|
||||||
...this._viewModes,
|
...this._viewModes,
|
||||||
{ value: 'current', label: localize('config.view.views.current') },
|
{ value: 'current', label: localize('config.view.views.current') },
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _menuModes = [
|
protected _menuModes: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'none', label: localize('config.menu.modes.none') },
|
{ value: 'none', label: localize('config.menu.modes.none') },
|
||||||
{ value: 'hidden-top', label: localize('config.menu.modes.hidden-top') },
|
{ value: 'hidden-top', label: localize('config.menu.modes.hidden-top') },
|
||||||
@@ -228,7 +238,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
{ value: 'below', label: localize('config.menu.modes.below') },
|
{ value: 'below', label: localize('config.menu.modes.below') },
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _eventViewerNextPreviousControlStyles = [
|
protected _eventViewerNextPreviousControlStyles: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{
|
{
|
||||||
value: 'thumbnails',
|
value: 'thumbnails',
|
||||||
@@ -244,7 +254,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _liveNextPreviousControlStyles = [
|
protected _liveNextPreviousControlStyles: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{
|
{
|
||||||
value: 'chevrons',
|
value: 'chevrons',
|
||||||
@@ -257,7 +267,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
{ value: 'none', label: localize('config.live.controls.next_previous.styles.none') },
|
{ value: 'none', label: localize('config.live.controls.next_previous.styles.none') },
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _aspectRatioModes = [
|
protected _aspectRatioModes: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{
|
{
|
||||||
value: 'dynamic',
|
value: 'dynamic',
|
||||||
@@ -270,7 +280,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _thumbnailModes = [
|
protected _thumbnailModes: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{
|
{
|
||||||
value: 'none',
|
value: 'none',
|
||||||
@@ -294,7 +304,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _thumbnailMedias = [
|
protected _thumbnailMedias: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'clips', label: localize('config.live.controls.thumbnails.medias.clips') },
|
{ value: 'clips', label: localize('config.live.controls.thumbnails.medias.clips') },
|
||||||
{
|
{
|
||||||
@@ -303,7 +313,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _titleModes = [
|
protected _titleModes: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'none', label: localize('config.event_viewer.controls.title.modes.none') },
|
{ value: 'none', label: localize('config.event_viewer.controls.title.modes.none') },
|
||||||
{
|
{
|
||||||
@@ -324,27 +334,27 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _transitionEffects = [
|
protected _transitionEffects: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'none', label: localize('config.event_viewer.transition_effects.none') },
|
{ value: 'none', label: localize('config.event_viewer.transition_effects.none') },
|
||||||
{ value: 'slide', label: localize('config.event_viewer.transition_effects.slide') },
|
{ value: 'slide', label: localize('config.event_viewer.transition_effects.slide') },
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _imageModes = [
|
protected _imageModes: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'camera', label: localize('config.image.modes.camera') },
|
{ value: 'camera', label: localize('config.image.modes.camera') },
|
||||||
{ value: 'screensaver', label: localize('config.image.modes.screensaver') },
|
{ value: 'screensaver', label: localize('config.image.modes.screensaver') },
|
||||||
{ value: 'url', label: localize('config.image.modes.url') },
|
{ value: 'url', label: localize('config.image.modes.url') },
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _timelineMediaTypes = [
|
protected _timelineMediaTypes: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'all', label: localize('config.timeline.medias.all') },
|
{ value: 'all', label: localize('config.timeline.medias.all') },
|
||||||
{ value: 'clips', label: localize('config.timeline.medias.clips') },
|
{ value: 'clips', label: localize('config.timeline.medias.clips') },
|
||||||
{ value: 'snapshots', label: localize('config.timeline.medias.snapshots') },
|
{ value: 'snapshots', label: localize('config.timeline.medias.snapshots') },
|
||||||
];
|
];
|
||||||
|
|
||||||
protected _darkModes = [
|
protected _darkModes: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'on', label: localize('config.view.dark_modes.on') },
|
{ value: 'on', label: localize('config.view.dark_modes.on') },
|
||||||
{ value: 'off', label: localize('config.view.dark_modes.off') },
|
{ value: 'off', label: localize('config.view.dark_modes.off') },
|
||||||
@@ -456,6 +466,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
protected _renderOptionSelector(
|
protected _renderOptionSelector(
|
||||||
configPath: string,
|
configPath: string,
|
||||||
options: string[] | { value: string; label: string }[],
|
options: string[] | { value: string; label: string }[],
|
||||||
|
multiple?: boolean,
|
||||||
): TemplateResult | void {
|
): TemplateResult | void {
|
||||||
if (!this._config) {
|
if (!this._config) {
|
||||||
return;
|
return;
|
||||||
@@ -464,7 +475,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
return html`
|
return html`
|
||||||
<ha-selector
|
<ha-selector
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.selector=${{ select: { options: options } }}
|
.selector=${{
|
||||||
|
select: { mode: 'dropdown', multiple: !!multiple, options: options },
|
||||||
|
}}
|
||||||
.label=${this._getLabel(configPath)}
|
.label=${this._getLabel(configPath)}
|
||||||
.value=${getConfigValue(this._config, configPath, '')}
|
.value=${getConfigValue(this._config, configPath, '')}
|
||||||
.required=${false}
|
.required=${false}
|
||||||
@@ -516,6 +529,22 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
return html` <span class="info">${info}</span>`;
|
return html` <span class="info">${info}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an editor title for the camera.
|
||||||
|
* @param cameraIndex The index of the camera in the cameras array.
|
||||||
|
* @param cameraConfig The raw camera configuration object.
|
||||||
|
* @returns A string title.
|
||||||
|
*/
|
||||||
|
protected _getEditorCameraTitle(
|
||||||
|
cameraIndex: number,
|
||||||
|
cameraConfig: RawFrigateCardConfig,
|
||||||
|
): string {
|
||||||
|
return (
|
||||||
|
getCameraTitle(this.hass, cameraConfig) ||
|
||||||
|
localize('editor.camera') + ' #' + cameraIndex
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render a camera header.
|
* Render a camera header.
|
||||||
* @param cameraIndex The index of the camera to edit/add.
|
* @param cameraIndex The index of the camera to edit/add.
|
||||||
@@ -540,31 +569,9 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
? html` <span class="new-camera">
|
? html` <span class="new-camera">
|
||||||
[${localize('editor.add_new_camera')}...]
|
[${localize('editor.add_new_camera')}...]
|
||||||
</span>`
|
</span>`
|
||||||
: // Attempt to render a recognizable name for the camera,
|
: html`<span
|
||||||
// starting with the most likely to be useful and working our
|
>${this._getEditorCameraTitle(cameraIndex, cameraConfig || {})}</span
|
||||||
// ways towards the least useful.
|
>`}
|
||||||
html` <span>
|
|
||||||
${cameraConfig?.title ||
|
|
||||||
cameraConfig?.id ||
|
|
||||||
[
|
|
||||||
cameraConfig?.camera_entity
|
|
||||||
? getEntityTitle(this.hass, String(cameraConfig.camera_entity))
|
|
||||||
: '',
|
|
||||||
cameraConfig?.client_id,
|
|
||||||
cameraConfig?.camera_name
|
|
||||||
? prettifyFrigateName(String(cameraConfig.camera_name))
|
|
||||||
: '',
|
|
||||||
cameraConfig?.label
|
|
||||||
? prettifyFrigateName(String(cameraConfig.label))
|
|
||||||
: '',
|
|
||||||
cameraConfig?.zone
|
|
||||||
? prettifyFrigateName(String(cameraConfig.zone))
|
|
||||||
: '',
|
|
||||||
]
|
|
||||||
.filter(Boolean)
|
|
||||||
.join(' / ') ||
|
|
||||||
localize('editor.camera') + ' #' + cameraIndex}
|
|
||||||
</span>`}
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
@@ -582,7 +589,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
cameraIndex: number,
|
cameraIndex: number,
|
||||||
addNewCamera?: boolean,
|
addNewCamera?: boolean,
|
||||||
): TemplateResult | void {
|
): TemplateResult | void {
|
||||||
const liveProviders = [
|
const liveProviders: EditorSelectOption[] = [
|
||||||
{ value: '', label: '' },
|
{ value: '', label: '' },
|
||||||
{ value: 'auto', label: localize('config.cameras.live_providers.auto') },
|
{ value: 'auto', label: localize('config.cameras.live_providers.auto') },
|
||||||
{ value: 'ha', label: localize('config.cameras.live_providers.ha') },
|
{ value: 'ha', label: localize('config.cameras.live_providers.ha') },
|
||||||
@@ -596,6 +603,16 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const dependentCameras: EditorSelectOption[] = [];
|
||||||
|
cameras.forEach((camera, index) => {
|
||||||
|
if (index !== cameraIndex) {
|
||||||
|
dependentCameras.push({
|
||||||
|
value: getCameraID(camera),
|
||||||
|
label: this._getEditorCameraTitle(index, camera),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// Make a new config and update the editor with changes on it,
|
// Make a new config and update the editor with changes on it,
|
||||||
const modifyConfig = (func: (config: RawFrigateCardConfig) => boolean): void => {
|
const modifyConfig = (func: (config: RawFrigateCardConfig) => boolean): void => {
|
||||||
if (this._config) {
|
if (this._config) {
|
||||||
@@ -711,6 +728,11 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
${this._renderStringInput(
|
${this._renderStringInput(
|
||||||
getArrayConfigPath(CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL, cameraIndex),
|
getArrayConfigPath(CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL, cameraIndex),
|
||||||
)}
|
)}
|
||||||
|
${this._renderOptionSelector(
|
||||||
|
getArrayConfigPath(CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS, cameraIndex),
|
||||||
|
dependentCameras,
|
||||||
|
true,
|
||||||
|
)}
|
||||||
</div>`
|
</div>`
|
||||||
: ``}
|
: ``}
|
||||||
`;
|
`;
|
||||||
@@ -841,10 +863,7 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor
|
|||||||
CONF_VIEW_CAMERA_SELECT,
|
CONF_VIEW_CAMERA_SELECT,
|
||||||
this._cameraSelectViewModes,
|
this._cameraSelectViewModes,
|
||||||
)}
|
)}
|
||||||
${this._renderOptionSelector(
|
${this._renderOptionSelector(CONF_VIEW_DARK_MODE, this._darkModes)}
|
||||||
CONF_VIEW_DARK_MODE,
|
|
||||||
this._darkModes,
|
|
||||||
)}
|
|
||||||
${this._renderNumberInput(CONF_VIEW_TIMEOUT_SECONDS)}
|
${this._renderNumberInput(CONF_VIEW_TIMEOUT_SECONDS)}
|
||||||
${this._renderNumberInput(CONF_VIEW_UPDATE_SECONDS)}
|
${this._renderNumberInput(CONF_VIEW_UPDATE_SECONDS)}
|
||||||
${this._renderSwitch(CONF_VIEW_UPDATE_FORCE, defaults.view.update_force)}
|
${this._renderSwitch(CONF_VIEW_UPDATE_FORCE, defaults.view.update_force)}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"camera_entity": "Camera Entity",
|
"camera_entity": "Camera Entity",
|
||||||
"camera_name": "Frigate camera name (Autodetected from entity)",
|
"camera_name": "Frigate camera name (Autodetected from entity)",
|
||||||
"client_id": "Frigate client id (For >1 Frigate server)",
|
"client_id": "Frigate client id (For >1 Frigate server)",
|
||||||
|
"dependent_cameras": "Dependent cameras to also show events for",
|
||||||
"id": "Unique id for this camera in this card",
|
"id": "Unique id for this camera in this card",
|
||||||
"label": "Frigate label/object filter",
|
"label": "Frigate label/object filter",
|
||||||
"frigate_url": "Frigate server URL",
|
"frigate_url": "Frigate server URL",
|
||||||
@@ -276,7 +277,7 @@
|
|||||||
"invalid_elements_config": "Invalid picture elements configuration",
|
"invalid_elements_config": "Invalid picture elements configuration",
|
||||||
"jsmpeg_no_sign": "Could not retrieve or sign JSMPEG websocket path",
|
"jsmpeg_no_sign": "Could not retrieve or sign JSMPEG websocket path",
|
||||||
"jsmpeg_no_player": "Could not start JSMPEG player",
|
"jsmpeg_no_player": "Could not start JSMPEG player",
|
||||||
"no_camera_name": "Could not determine Frigate camera name for camera, please specify either 'camera_entity' or 'camera_name' for the following camera",
|
"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'",
|
||||||
"download_no_media": "No media to download",
|
"download_no_media": "No media to download",
|
||||||
"download_no_event_id": "Could not extract Frigate event id from media",
|
"download_no_event_id": "Could not extract Frigate event id from media",
|
||||||
"download_sign_failed": "Could not sign media URL for download"
|
"download_sign_failed": "Could not sign media URL for download"
|
||||||
|
|||||||
@@ -28,10 +28,6 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
ha-formfield {
|
|
||||||
padding-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.upgrade {
|
div.upgrade {
|
||||||
width: auto;
|
width: auto;
|
||||||
border: 1px dotted var(--primary-color);
|
border: 1px dotted var(--primary-color);
|
||||||
@@ -79,3 +75,8 @@ div.upgrade span {
|
|||||||
span.info {
|
span.info {
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ha-selector {
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
}
|
||||||
|
|||||||
+18
-5
@@ -327,6 +327,9 @@ const cameraConfigSchema = z
|
|||||||
|
|
||||||
// Camera identifiers for WebRTC.
|
// Camera identifiers for WebRTC.
|
||||||
webrtc_card: webrtcCardCameraConfigSchema.optional(),
|
webrtc_card: webrtcCardCameraConfigSchema.optional(),
|
||||||
|
|
||||||
|
// Set of cameras IDs upon which this camera depends.
|
||||||
|
dependent_cameras: z.string().array().optional(),
|
||||||
})
|
})
|
||||||
.default(cameraConfigDefault);
|
.default(cameraConfigDefault);
|
||||||
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
export type CameraConfig = z.infer<typeof cameraConfigSchema>;
|
||||||
@@ -936,11 +939,14 @@ export type MenuButton = z.infer<typeof menuButtonSchema>;
|
|||||||
export interface ExtendedHomeAssistant extends HomeAssistant {
|
export interface ExtendedHomeAssistant extends HomeAssistant {
|
||||||
hassUrl(path?): string;
|
hassUrl(path?): string;
|
||||||
themes: Themes & {
|
themes: Themes & {
|
||||||
darkMode?: boolean
|
darkMode?: boolean;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BrowseMediaQueryParametersBase {
|
export interface BrowseMediaQueryParameters {
|
||||||
|
// ========================================
|
||||||
|
// Parameters used to construct media query
|
||||||
|
// ========================================
|
||||||
mediaType?: 'clips' | 'snapshots';
|
mediaType?: 'clips' | 'snapshots';
|
||||||
clientId: string;
|
clientId: string;
|
||||||
cameraName: string;
|
cameraName: string;
|
||||||
@@ -949,10 +955,17 @@ export interface BrowseMediaQueryParametersBase {
|
|||||||
before?: number;
|
before?: number;
|
||||||
after?: number;
|
after?: number;
|
||||||
unlimited?: boolean;
|
unlimited?: boolean;
|
||||||
}
|
|
||||||
|
|
||||||
export interface BrowseMediaQueryParameters extends BrowseMediaQueryParametersBase {
|
// ========================================
|
||||||
mediaType: 'clips' | 'snapshots';
|
// Parameters used to differentiate results
|
||||||
|
// ========================================
|
||||||
|
// Optional title to be used for separating results when merging multiple
|
||||||
|
// sets of results. See `mergeFrigateBrowseMediaSources()` .
|
||||||
|
title?: string;
|
||||||
|
|
||||||
|
// Optional camera-id to which this query is associated. May be used to map
|
||||||
|
// results to a particular camera within the card.
|
||||||
|
cameraID?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface BrowseMediaNeighbors {
|
export interface BrowseMediaNeighbors {
|
||||||
|
|||||||
+12
@@ -110,6 +110,18 @@ export class View {
|
|||||||
return ['snapshot', 'snapshots'].includes(this.view);
|
return ['snapshot', 'snapshots'].includes(this.view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the media type for this view if available.
|
||||||
|
* @returns Whether the media is `clips` or `snapshots` or unknown (`null`)
|
||||||
|
*/
|
||||||
|
public getMediaType(): 'clips' | 'snapshots' | null {
|
||||||
|
return this.isClipRelatedView()
|
||||||
|
? 'clips'
|
||||||
|
: this.isSnapshotRelatedView()
|
||||||
|
? 'snapshots'
|
||||||
|
: null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the media item that should be played.
|
* Get the media item that should be played.
|
||||||
**/
|
**/
|
||||||
|
|||||||
Reference in New Issue
Block a user