diff --git a/README.md b/README.md index 34933643..cdf38f2f 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ See the [fully expanded cameras configuration example](#config-expanded-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. | | `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). | -| `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.| +| `dependencies` | | :heavy_multiplication_x: | Other cameras that this camera should depend upon. See [camera dependencies](#camera-dependency-configuration) below. | | `trigger_by_motion` | `false` | :heavy_multiplication_x: | Whether to not to trigger the camera (used to trigger [scan mode](#scan-mode) or reseting the default view) by automatically detecting and using the motion `binary_sensor` for this camera. This autodetection only works for Frigate cameras, and only when the motion `binary_sensor` entity has been enabled in Home Assistant.| | `trigger_by_occupancy` | `true` | :heavy_multiplication_x: | Whether to not to trigger the camera (used to trigger [scan mode](#scan-mode) or reseting the default view) by automatically detecting and using the occupancy `binary_sensor` for this camera. This autodetection only works for Frigate cameras, and only when the occupancy `binary_sensor` entity has been enabled in Home Assistant.| | `trigger_by_entities` | | :heavy_multiplication_x: | Whether to not to trigger the camera (used to trigger [scan mode](#scan-mode) or reseting the default view) when the state of any Home Assistant entity becomes active (i.e. state becomes `on` or `open`). This works for Frigate or non-Frigate cameras.| @@ -146,11 +146,27 @@ cameras: See [Using the WebRTC Card](#webrtc) below for more details on how to use the WebRTC Card live provider. + + +#### 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: + +```yaml +cameras: + - dependencies: +``` + +| 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.| +| `all_cameras` | `false` | :heavy_multiplication_x: | Shortcut to specify all other cameras as dependent cameras.| + #### Camera IDs: Refering 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 `camera_name`} parameters for that camera -- in that order of precedence. These ids may be used in conditions 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 `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. | #### Example @@ -1011,8 +1027,10 @@ cameras: camera_entity: camera.front_Door live_provider: ha # Show events for camera-2 when this camera is viewed. - dependent_cameras: - - camera-2 + dependencies: + all_cameras: false + cameras: + - camera-2 trigger_by_motion: false trigger_by_occupancy: true trigger_by_entities: @@ -1035,6 +1053,8 @@ cameras: trigger_by_occupancy: true trigger_by_entities: - binary_sensor.entrance_sensor + dependencies: + all_cameras: false ``` @@ -2452,7 +2472,7 @@ menu: ### 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). +`dependencies.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).
Expand: Using dependent cameras with birdseye @@ -2465,9 +2485,26 @@ cameras: - camera_entity: camera.kitchen - camera_entity: camera.sitting_room - camera_name: birdseye - dependent_cameras: - - camera.kitchen - - camera.sitting_room + dependencies: + cameras: + - camera.kitchen + - camera.sitting_room +``` +
+ +
+ Expand: Using all dependent cameras with birdseye + +This example shows events for all other cameras when `birdseye` is selected. This is just a shortcut for naming all other cameras. + +```yaml +[...] +cameras: + - camera_entity: camera.kitchen + - camera_entity: camera.sitting_room + - camera_name: birdseye + dependencies: + all_cameras: true ```
diff --git a/src/card.ts b/src/card.ts index 2d84821c..33252401 100644 --- a/src/card.ts +++ b/src/card.ts @@ -5,7 +5,7 @@ import { LitElement, PropertyValues, TemplateResult, - unsafeCSS, + unsafeCSS } from 'lit'; import { customElement, property, state } from 'lit/decorators.js'; import { classMap } from 'lit/directives/class-map.js'; @@ -20,7 +20,7 @@ import { ConditionState, conditionStateRequestHandler, getOverriddenConfig, - getOverridesByKey, + getOverridesByKey } from './card-condition.js'; import './components/elements.js'; import { FrigateCardElements } from './components/elements.js'; @@ -40,7 +40,7 @@ import { CAMERA_BIRDSEYE, CARD_VERSION, MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA, - REPO_URL, + REPO_URL } from './const.js'; import './editor.js'; import { localize } from './localize/localize.js'; @@ -63,14 +63,14 @@ import { MediaShowInfo, MenuButton, Message, - RawFrigateCardConfig, + RawFrigateCardConfig } from './types.js'; import { convertActionToFrigateCardCustomAction, createFrigateCardCustomAction, frigateCardHandleAction, frigateCardHasAction, - getActionConfigGivenAction, + getActionConfigGivenAction } from './utils/action.js'; import { contentsChanged } from './utils/basic.js'; import { getCameraIcon, getCameraID, getCameraTitle } from './utils/camera.js'; @@ -81,7 +81,7 @@ import { homeAssistantSignPath, isHassDifferent, isTriggeredState, - sideLoadHomeAssistantElements, + sideLoadHomeAssistantElements } from './utils/ha'; import { getEventID } from './utils/ha/browse-media.js'; import { DeviceList, getAllDevices } from './utils/ha/device-registry.js'; @@ -89,7 +89,7 @@ import { ExtendedEntityCache, getAllEntities, getExtendedEntities, - getExtendedEntity, + getExtendedEntity } from './utils/ha/entity-registry.js'; import { ResolvedMediaCache } from './utils/ha/resolved-media.js'; import { supportsFeature } from './utils/ha/update.js'; @@ -398,7 +398,8 @@ export class FrigateCard extends LitElement { if ( cameraConfig?.camera_name && (cameraConfig?.camera_name !== CAMERA_BIRDSEYE || - cameraConfig?.dependent_cameras?.length) + cameraConfig.dependencies.cameras.length || + cameraConfig.dependencies.all_cameras) ) { buttons.push({ icon: 'mdi:filmstrip', @@ -416,7 +417,8 @@ export class FrigateCard extends LitElement { if ( cameraConfig?.camera_name && (cameraConfig?.camera_name !== CAMERA_BIRDSEYE || - cameraConfig?.dependent_cameras?.length) + cameraConfig?.dependencies.cameras.length || + cameraConfig?.dependencies.all_cameras) ) { buttons.push({ icon: 'mdi:camera', diff --git a/src/const.ts b/src/const.ts index fad0d643..ca6d8e9d 100644 --- a/src/const.ts +++ b/src/const.ts @@ -21,8 +21,10 @@ export const CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL = `${CONF_CAMERAS}.#.webrtc_card.url` as const; export const CONF_CAMERAS_ARRAY_LIVE_PROVIDER = `${CONF_CAMERAS}.#.live_provider` as const; -export const CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS = - `${CONF_CAMERAS}.#.dependent_cameras` as const; +export const CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS = + `${CONF_CAMERAS}.#.dependencies.cameras` as const; + export const CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS = + `${CONF_CAMERAS}.#.dependencies.all_cameras` as const; export const CONF_CAMERAS_ARRAY_TRIGGER_BY_MOTION = `${CONF_CAMERAS}.#.trigger_by_motion` as const; export const CONF_CAMERAS_ARRAY_TRIGGER_BY_OCCUPANCY = diff --git a/src/editor.ts b/src/editor.ts index 9eb6fc24..7fe3457f 100644 --- a/src/editor.ts +++ b/src/editor.ts @@ -16,7 +16,8 @@ import { CONF_CAMERAS_ARRAY_CAMERA_ENTITY, CONF_CAMERAS_ARRAY_CAMERA_NAME, CONF_CAMERAS_ARRAY_CLIENT_ID, - CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS, + CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS, + CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS, CONF_CAMERAS_ARRAY_ICON, CONF_CAMERAS_ARRAY_ID, CONF_CAMERAS_ARRAY_LABEL, @@ -882,8 +883,15 @@ export class FrigateCardEditor extends LitElement implements LovelaceCardEditor ${this._renderStringInput( getArrayConfigPath(CONF_CAMERAS_ARRAY_WEBRTC_CARD_URL, cameraIndex), )} + ${this._renderSwitch( + getArrayConfigPath( + CONF_CAMERAS_ARRAY_DEPENDENCIES_ALL_CAMERAS, + cameraIndex, + ), + frigateCardConfigDefaults.cameras.dependencies.all_cameras, + )} ${this._renderOptionSelector( - getArrayConfigPath(CONF_CAMERAS_ARRAY_DEPENDENT_CAMERAS, cameraIndex), + getArrayConfigPath(CONF_CAMERAS_ARRAY_DEPENDENCIES_CAMERAS, cameraIndex), dependentCameras, { multiple: true, diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 428e3fd2..6d261706 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -15,7 +15,10 @@ "camera_entity": "Camera Entity", "camera_name": "Frigate camera name (Autodetected from entity)", "client_id": "Frigate client id (For >1 Frigate server)", - "dependent_cameras": "Dependent cameras to also show events for", + "dependencies": { + "cameras": "Show events for specific cameras with this camera", + "all_cameras": "Show events for all cameras with this camera" + }, "id": "Unique id for this camera in this card", "label": "Frigate label/object filter", "frigate_url": "Frigate server URL", diff --git a/src/types.ts b/src/types.ts index 6e56c48d..0443c8b9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -375,6 +375,10 @@ export const cameraConfigDefault = { trigger_by_motion: false, trigger_by_occupancy: true, trigger_by_entities: [], + dependencies: { + all_cameras: false, + cameras: [], + }, }; const webrtcCardCameraConfigSchema = z.object({ entity: z.string().optional(), @@ -403,8 +407,12 @@ const cameraConfigSchema = z // Camera identifiers for WebRTC. webrtc_card: webrtcCardCameraConfigSchema.optional(), - // Set of cameras IDs upon which this camera depends. - dependent_cameras: z.string().array().optional(), + dependencies: z + .object({ + all_cameras: z.boolean().default(cameraConfigDefault.dependencies.all_cameras), + cameras: z.string().array().default(cameraConfigDefault.dependencies.cameras), + }) + .default(cameraConfigDefault.dependencies), trigger_by_motion: z.boolean().default(cameraConfigDefault.trigger_by_motion), trigger_by_occupancy: z.boolean().default(cameraConfigDefault.trigger_by_occupancy), @@ -1015,7 +1023,10 @@ const timelineConfigSchema = z .max(24 * 60 * 60) .optional() .default(timelineConfigDefault.window_seconds), - show_recordings: z.boolean().optional().default(timelineConfigDefault.show_recordings), + show_recordings: z + .boolean() + .optional() + .default(timelineConfigDefault.show_recordings), controls: z .object({ thumbnails: thumbnailsControlSchema diff --git a/src/utils/ha/browse-media.ts b/src/utils/ha/browse-media.ts index 5bb03d4f..95fc561c 100644 --- a/src/utils/ha/browse-media.ts +++ b/src/utils/ha/browse-media.ts @@ -264,7 +264,14 @@ export const getFullDependentBrowseMediaQueryParameters = ( const cameraConfig = cameras.get(camera); if (cameraConfig) { cameraIDs.add(camera); - for (const eventCameraID of cameraConfig.dependent_cameras || []) { + const dependentCameras: Set = new Set(); + (cameraConfig.dependencies.cameras || []).forEach((item) => + dependentCameras.add(item), + ); + if (cameraConfig.dependencies.all_cameras) { + cameras.forEach((_, key) => dependentCameras.add(key)); + } + for (const eventCameraID of dependentCameras) { if (!cameraIDs.has(eventCameraID)) { getDependentCameras(eventCameraID); } @@ -281,12 +288,9 @@ export const getFullDependentBrowseMediaQueryParameters = ( 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; + if (param) { + params.push(param); } - params.push(param); } return params.length ? params : null; };