Merge pull request #982 from dermotduffy/cameras-globals

Add support for global cameras options
This commit is contained in:
Dermot Duffy
2023-03-04 13:12:02 -08:00
committed by GitHub
7 changed files with 443 additions and 277 deletions
+152 -29
View File
@@ -83,6 +83,8 @@ lovelace:
At least 1 camera must be configured in the `cameras` section, but otherwise all configuration parameters are optional. At least 1 camera must be configured in the `cameras` section, but otherwise all configuration parameters are optional.
<a name="camera-options"></a>
### Camera Options ### Camera Options
The `cameras` block configures a list of cameras the card should support. The first listed camera is the default. Camera configuration is under: The `cameras` block configures a list of cameras the card should support. The first listed camera is the default. Camera configuration is under:
@@ -99,17 +101,17 @@ See the [fully expanded cameras configuration example](#config-expanded-cameras)
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `camera_entity` | | :heavy_multiplication_x: | The Home Assistant camera entity to use with the `frigate` live provider view. Also used to automatically detect the name of the underlying Frigate camera, and the title/icon of the camera. | | `camera_entity` | | :white_check_mark: | The Home Assistant camera entity to use with the `frigate` live provider view. Also used to automatically detect the name of the underlying Frigate camera, and the title/icon of the camera. |
| `live_provider` | `auto` | :heavy_multiplication_x: | The choice of live stream provider. See [Live Providers](#live-providers) below.| | `live_provider` | `auto` | :white_check_mark: | 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. | :white_check_mark: | 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. | :white_check_mark: | 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. | | `hide` | `false` | :white_check_mark: | 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). | :white_check_mark: | 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` | :white_check_mark: | 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` | | :white_check_mark: | Options for a Frigate camera. See [Frigate configuration](#camera-frigate-configuration) below. |
| `dependencies` | | :heavy_multiplication_x: | Other cameras that this camera should depend upon. See [camera dependencies](#camera-dependencies-configuration) below. | | `dependencies` | | :white_check_mark: | Other cameras that this camera should depend upon. See [camera dependencies](#camera-dependencies-configuration) below. |
| `triggers` | | :heavy_multiplication_x: | Define what should cause this camera to update/trigger. See [camera triggers](#camera-trigger-configuration) below. | | `triggers` | | :white_check_mark: | Define what should cause this camera to update/trigger. See [camera triggers](#camera-trigger-configuration) below. |
| `webrtc_card` | | :heavy_multiplication_x: | The WebRTC entity/URL to use for this camera with the `webrtc-card` live provider. See below. | | `webrtc_card` | | :white_check_mark: | The WebRTC entity/URL to use for this camera with the `webrtc-card` live provider. See below. |
<a name="live-providers"></a> <a name="live-providers"></a>
@@ -156,11 +158,11 @@ cameras:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `camera_name` | Autodetected from `camera_entity` if that is specified. | :heavy_multiplication_x: | The Frigate camera name to use when communicating with the Frigate server, e.g. for viewing clips/snapshots or the JSMPEG live view.| | `camera_name` | Autodetected from `camera_entity` if that is specified. | :white_check_mark: | The Frigate camera name to use when communicating with the Frigate server, e.g. for viewing clips/snapshots or the JSMPEG live view.|
| `url` | | :heavy_multiplication_x: | The URL of the frigate server. If set, this value will be (exclusively) used for a `Frigate UI` menu button. All other communication with Frigate goes via Home Assistant. | | `url` | | :white_check_mark: | The URL of the frigate server. If set, this value will be (exclusively) used for a `Frigate UI` menu button. All other communication with Frigate goes via Home Assistant. |
| `label` | | :heavy_multiplication_x: | A Frigate label / object filter used to filter events (clips & snapshots), e.g. `person`.| | `label` | | :white_check_mark: | A Frigate label / object filter used to filter events (clips & snapshots), e.g. `person`.|
| `zone` | | :heavy_multiplication_x: | A Frigate zone used to filter events (clips & snapshots), e.g. `front_door`.| | `zone` | | :white_check_mark: | A Frigate zone used to filter events (clips & snapshots), e.g. `front_door`.|
| `client_id` | `frigate` | :heavy_multiplication_x: | The Frigate client id to use. If this Home Assistant server has multiple Frigate server backends configured, this selects which server should be used. It should be set to the MQTT client id configured for this server, see [Frigate Integration Multiple Instance Support](https://docs.frigate.video/integrations/home-assistant/#multiple-instance-support).| | `client_id` | `frigate` | :white_check_mark: | The Frigate client id to use. If this Home Assistant server has multiple Frigate server backends configured, this selects which server should be used. It should be set to the MQTT client id configured for this server, see [Frigate Integration Multiple Instance Support](https://docs.frigate.video/integrations/home-assistant/#multiple-instance-support).|
#### Live Provider: Camera go2rtc configuration #### Live Provider: Camera go2rtc configuration
@@ -173,8 +175,8 @@ cameras:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `modes` | `[webrtc, mse, mp4, mjpeg]` | :heavy_multiplication_x: | An ordered array of `go2rtc` modes to use. Valid values are `webrtc`, `mse`, `mp4` or `mjpeg` values. | | `modes` | `[webrtc, mse, mp4, mjpeg]` | :white_check_mark: | An ordered array of `go2rtc` modes to use. Valid values are `webrtc`, `mse`, `mp4` or `mjpeg` values. |
| `stream` | Determind by camera engine (e.g. `frigate` camera name). | :heavy_multiplication_x: | A valid `go2rtc` stream name. | | `stream` | Determind by camera engine (e.g. `frigate` camera name). | :white_check_mark: | A valid `go2rtc` stream name. |
#### Live Provider: Camera WebRTC Card configuration #### Live Provider: Camera WebRTC Card configuration
@@ -187,9 +189,9 @@ cameras:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `entity` | | :heavy_multiplication_x: | The RTSP entity to pass to the WebRTC Card for this camera. | | `entity` | | :white_check_mark: | The RTSP entity to pass to the WebRTC Card for this camera. |
| `url` | Depends on the camera engine (e.g. Frigate will use the camera name by default since this is the [recommended setup](https://deploy-preview-4055--frigate-docs.netlify.app/guides/configuring_go2rtc/))| :heavy_multiplication_x: | The RTSP url to pass to the WebRTC Card. | | `url` | Depends on the camera engine (e.g. Frigate will use the camera name by default since this is the [recommended setup](https://deploy-preview-4055--frigate-docs.netlify.app/guides/configuring_go2rtc/))| :white_check_mark: | The RTSP url to pass to the WebRTC Card. |
| `*`| | :heavy_multiplication_x: | Any options specified in the `webrtc_card:` YAML dictionary are silently passed through to the AlexxIT's WebRTC Card. See [WebRTC Configuration](https://github.com/AlexxIT/WebRTC#configuration) for full details this external card provides. | | `*`| | :white_check_mark: | Any options specified in the `webrtc_card:` YAML dictionary are silently passed through to the AlexxIT's WebRTC Card. See [WebRTC Configuration](https://github.com/AlexxIT/WebRTC#configuration) for full details this external card provides. |
See [Using the WebRTC Card](#webrtc) below for more details on how to use the WebRTC Card live provider. See [Using the WebRTC Card](#webrtc) below for more details on how to use the WebRTC Card live provider.
@@ -205,8 +207,8 @@ cameras:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `refresh_seconds` | 1 | :heavy_multiplication_x: | The image will be refreshed at least every `refresh_seconds`. `0` implies no refreshing. | | `refresh_seconds` | 1 | :white_check_mark: | The image will be refreshed at least every `refresh_seconds`. `0` implies no refreshing. |
| `url` | | :heavy_multiplication_x: | **Advanced**: A static image URL to be fetched in lieu of the Home Assistant image for the given camera. This may be useful for advanced configurations where the camera image is being provided by some non-Home Assistant system. This will also set the temporary loading image used when `show_image_during_load` is set to true under the `live` configuration. | | `url` | | :white_check_mark: | **Advanced**: A static image URL to be fetched in lieu of the Home Assistant image for the given camera. This may be useful for advanced configurations where the camera image is being provided by some non-Home Assistant system. This will also set the temporary loading image used when `show_image_during_load` is set to true under the `live` configuration. |
#### Live Provider: JSMPEG Configuration #### Live Provider: JSMPEG Configuration
@@ -219,7 +221,7 @@ cameras:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `options` | | :heavy_multiplication_x: | **Advanced users only**: Control the underlying [JSMPEG library options](https://github.com/phoboslab/jsmpeg#usage). Supports setting these JSMPEG options `{audio, video, pauseWhenHidden, disableGl, disableWebAssembly, preserveDrawingBuffer, progressive, throttled, chunkSize, maxAudioLag, videoBufferSize, audioBufferSize}`. This is not necessary for the vast majority of users: only set these flags if you know what you're doing, as you may entirely break video rendering in the card.| | `options` | | :white_check_mark: | **Advanced users only**: Control the underlying [JSMPEG library options](https://github.com/phoboslab/jsmpeg#usage). Supports setting these JSMPEG options `{audio, video, pauseWhenHidden, disableGl, disableWebAssembly, preserveDrawingBuffer, progressive, throttled, chunkSize, maxAudioLag, videoBufferSize, audioBufferSize}`. This is not necessary for the vast majority of users: only set these flags if you know what you're doing, as you may entirely break video rendering in the card.|
<a name="webrtc-live-configuration"></a> <a name="webrtc-live-configuration"></a>
@@ -236,8 +238,8 @@ 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 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.| | `cameras` | | :white_check_mark: | 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` | :white_check_mark: | Shortcut to specify all other cameras as dependent cameras.|
<a name="camera-triggers-configuration"></a> <a name="camera-triggers-configuration"></a>
@@ -252,9 +254,9 @@ cameras:
| Option | Default | Overridable | Description | | Option | Default | Overridable | Description |
| - | - | - | - | | - | - | - | - |
| `motion` | `false` | :heavy_multiplication_x: | Whether to not to trigger the camera 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.| | `motion` | `false` | :white_check_mark: | Whether to not to trigger the camera 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.|
| `occupancy` | `true` | :heavy_multiplication_x: | Whether to not to trigger the camera 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.| | `occupancy` | `true` | :white_check_mark: | Whether to not to trigger the camera 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.|
| `entities` | | :heavy_multiplication_x: | Whether to not to trigger the camera 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.| | `entities` | | :white_check_mark: | Whether to not to trigger the camera 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.|
<a name="camera-ids"></a> <a name="camera-ids"></a>
@@ -266,6 +268,20 @@ Each camera configured in the card has a single identifier (`id`). For a given c
See [the basic cameras configuration example](#basic-cameras-configuration) below. See [the basic cameras configuration example](#basic-cameras-configuration) below.
<a name="camera-global-options"></a>
### Camera Global Options
**Advanced:** The optional `cameras_global` block configures global options that
apply to all cameras from the `cameras` section. For large configs, this can
avoid significant repetition across cameras. The configuration is under:
```yaml
cameras_global:
```
The configuration options are identical to a single [camera entry](#camera-options).
### View Options ### View Options
All configuration is under: All configuration is under:
@@ -1415,6 +1431,61 @@ cameras:
``` ```
</details> </details>
<details>
<summary>Expand: Cameras Global section</summary>
Reference: [Cameras Global Options](#camera-global-options).
```yaml
cameras_global:
live_provider: ha
engine: auto
hide: false
frigate:
url: http://my.frigate.local
client_id: frigate
camera_name: front_door
label: person
zone: steps
dependencies:
all_cameras: false
cameras:
- camera-2
triggers:
motion: false
occupancy: true
entities:
- binary_sensor.front_door_sensor
go2rtc:
modes:
- webrtc
- mse
- mp4
- mjpeg
stream: sitting_room
webrtc_card:
# Arbitrary WebRTC Card options, see https://github.com/AlexxIT/WebRTC#configuration .
entity: camera.sitting_room_rtsp
ui: true
jsmpeg:
options:
audio: false
video: true
pauseWhenHidden: false
disableGl: false
disableWebAssembly: false
preserveDrawingBuffer: false
progressive: true
throttled: true
chunkSize: 1048576
maxAudioLag: 10
videoBufferSize: 524288
audioBufferSize: 131072
image:
refresh_seconds: 1
```
</details>
<a name="config-expanded-view"></a> <a name="config-expanded-view"></a>
<details> <details>
@@ -2118,6 +2189,58 @@ overrides:
state: on state: on
state_not: off state_not: off
overrides: overrides:
cameras:
# As this is an array, we need to carefully ensure we are
# overridding the correct index. We do this by specifying
# earlier indicies as being overridden with an empty object
# (in YAML this is `{}`). In this example, overriddes will
# only apply to the 2nd camera:
- {} # No overrides for camera index 0.
- live_provider: 'ha' # Overrides for camera index 1.
engine: auto
hide: false
frigate:
url: http://my.frigate.local
client_id: frigate
camera_name: front_door
label: person
zone: steps
dependencies:
all_cameras: false
cameras:
- camera-2
triggers:
motion: false
occupancy: true
entities:
- binary_sensor.front_door_sensor
go2rtc:
modes:
- webrtc
- mse
- mp4
- mjpeg
stream: sitting_room
webrtc_card:
# Arbitrary WebRTC Card options, see https://github.com/AlexxIT/WebRTC#configuration .
entity: camera.sitting_room_rtsp
ui: true
jsmpeg:
options:
audio: false
video: true
pauseWhenHidden: false
disableGl: false
disableWebAssembly: false
preserveDrawingBuffer: false
progressive: true
throttled: true
chunkSize: 1048576
maxAudioLag: 10
videoBufferSize: 524288
audioBufferSize: 131072
image:
refresh_seconds: 1
live: live:
webrtc_card: webrtc_card:
ui: true ui: true
+17 -9
View File
@@ -1,9 +1,5 @@
import { HomeAssistant } from 'custom-card-helpers'; import { HomeAssistant } from 'custom-card-helpers';
import { import { CameraConfig, CamerasConfig, CardWideConfig } from '../types.js';
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,
@@ -137,11 +133,11 @@ export class CameraManager {
}; };
const engineTypes = await getEngineTypes(camerasConfig); const engineTypes = await getEngineTypes(camerasConfig);
for (const [index, cameraConfig] of camerasConfig.entries()) { for (const [index, cameraConfig] of camerasConfig.entries()) {
const engineType = engineTypes[index]; 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'),
@@ -180,6 +176,8 @@ export class CameraManager {
entityRegistryManager: EntityRegistryManager, entityRegistryManager: EntityRegistryManager,
camerasConfig: CamerasConfig, camerasConfig: CamerasConfig,
): Promise<void> { ): Promise<void> {
const initializationStartTime = new Date();
const hasAutoTriggers = (config: CameraConfig): boolean => { const hasAutoTriggers = (config: CameraConfig): boolean => {
return config.triggers.motion || config.triggers.occupancy; return config.triggers.motion || config.triggers.occupancy;
}; };
@@ -229,6 +227,16 @@ export class CameraManager {
if (!this._store.getVisibleCameraCount()) { if (!this._store.getVisibleCameraCount()) {
throw new CameraInitializationError(localize('error.no_visible_cameras')); throw new CameraInitializationError(localize('error.no_visible_cameras'));
} }
log(
this._cardWideConfig,
'Frigate Card CameraManager initialized (Cameras: ',
camerasConfig,
`, Duration: ${
(new Date().getTime() - initializationStartTime.getTime()) / 1000
}s,`,
')',
);
} }
public isInitialized(): boolean { public isInitialized(): boolean {
+180 -133
View File
@@ -61,7 +61,7 @@ import {
frigateCardHasAction, frigateCardHasAction,
getActionConfigGivenAction, getActionConfigGivenAction,
} from './utils/action.js'; } from './utils/action.js';
import { contentsChanged, errorToConsole } from './utils/basic.js'; import { errorToConsole } from './utils/basic.js';
import { import {
getEntityIcon, getEntityIcon,
getEntityTitle, getEntityTitle,
@@ -87,6 +87,10 @@ import { EntityRegistryManager } from './utils/ha/entity-registry/index.js';
import { EntityCache } from './utils/ha/entity-registry/cache.js'; import { EntityCache } from './utils/ha/entity-registry/cache.js';
import { Entity, ExtendedEntity } from './utils/ha/entity-registry/types.js'; import { Entity, ExtendedEntity } from './utils/ha/entity-registry/types.js';
import { getAllDependentCameras } from './utils/camera.js'; import { getAllDependentCameras } from './utils/camera.js';
import cloneDeep from 'lodash-es/cloneDeep';
import isEqual from 'lodash-es/isEqual';
import merge from 'lodash-es/merge';
import { FrigateCardInitializer } from './utils/initializer.js';
/** A note on media callbacks: /** A note on media callbacks:
* *
@@ -135,7 +139,12 @@ console.info(
documentationURL: REPO_URL, documentationURL: REPO_URL,
}); });
type InitializedType = 'initialized' | 'initializing'; enum InitializationAspect {
LANGUAGES = 'languages',
SIDE_LOAD_ELEMENTS = 'side-load-elements',
MEDIA_PLAYERS = 'media-players',
CAMERAS = 'cameras',
}
/** /**
* Main FrigateCard class. * Main FrigateCard class.
@@ -202,9 +211,6 @@ class FrigateCard extends LitElement {
// per second for performance reasons. // per second for performance reasons.
protected _boundMouseHandler = throttle(this._mouseHandler.bind(this), 1 * 1000); protected _boundMouseHandler = throttle(this._mouseHandler.bind(this), 1 * 1000);
// Whether the card has been successfully initialized.
protected _initialized?: InitializedType;
protected _triggers: Map<string, Date> = new Map(); protected _triggers: Map<string, Date> = new Map();
protected _untriggerTimerID: number | null = null; protected _untriggerTimerID: number | null = null;
@@ -212,6 +218,8 @@ class FrigateCard extends LitElement {
protected _mediaPlayers?: string[]; protected _mediaPlayers?: string[];
protected _initializer = new FrigateCardInitializer();
constructor() { constructor() {
super(); super();
this._entityRegistryManager = new EntityRegistryManager( this._entityRegistryManager = new EntityRegistryManager(
@@ -315,7 +323,15 @@ class FrigateCard extends LitElement {
// Save on Lit re-rendering costs by only updating the configuration if it // Save on Lit re-rendering costs by only updating the configuration if it
// actually changes. // actually changes.
if (contentsChanged(overriddenConfig, this._overriddenConfig)) { if (!isEqual(overriddenConfig, this._overriddenConfig)) {
if (
!isEqual(overriddenConfig.cameras, this._overriddenConfig?.cameras) ||
!isEqual(overriddenConfig.cameras_global, this._overriddenConfig?.cameras_global)
) {
// Uninitialize the cameras (they will be re-initialized on the render
// cycle triggered by updating the overridden config) below.
this._initializer.uninitialize(InitializationAspect.CAMERAS);
}
this._overriddenConfig = overriddenConfig; this._overriddenConfig = overriddenConfig;
} }
} }
@@ -906,98 +922,6 @@ class FrigateCard extends LitElement {
}); });
} }
protected async _initialize(
hass: HomeAssistant,
config: FrigateCardConfig,
cardWideConfig: CardWideConfig,
): Promise<void> {
// Above arguments are taken (vs usage of `this`) as they must exist prior
// to initialization and this ensures it is the callers responsibility to
// verify that.
await Promise.all([
// Side load Home Assistant elements used in the UI.
sideLoadHomeAssistantElements(),
// Load dynamic language imports.
loadLanguages(),
]);
await this._initializeCameras(hass, config, cardWideConfig);
// Don't reset the message which may be set to an error above. This sets the
// first view using the newly loaded cameras.
this._changeView({ resetMessage: false });
}
protected async _initializeCameras(
hass: HomeAssistant,
config: FrigateCardConfig,
cardWideConfig: CardWideConfig,
): Promise<void> {
this._cameraManager = new CameraManager(
new CameraManagerEngineFactory(this._entityRegistryManager, cardWideConfig),
this._cardWideConfig,
);
try {
await this._cameraManager.initializeCameras(
hass,
this._entityRegistryManager,
config.cameras,
);
} catch (e: unknown) {
if (e instanceof Error) {
errorToConsole(e);
}
if (e instanceof FrigateCardError) {
this._setMessageAndUpdate({
message: e.message,
type: 'error',
context: e.context,
});
}
}
}
protected async _initializeMediaPlayers(hass: HomeAssistant): Promise<void> {
const isValidMediaPlayer = (entityID: string): boolean => {
if (entityID.startsWith('media_player.')) {
const stateObj = this._hass?.states[entityID];
if (
stateObj &&
stateObj.state !== 'unavailable' &&
supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA)
) {
return true;
}
}
return false;
};
const mediaPlayers = Object.keys(this._hass?.states || {}).filter(
isValidMediaPlayer,
);
let mediaPlayerEntities: Map<string, Entity>;
try {
mediaPlayerEntities = await this._entityRegistryManager.getEntities(
hass,
mediaPlayers,
);
} catch (e) {
// Failing to fetch media player information is not considered
// sufficiently serious to block card startup.
errorToConsole(e as Error);
return;
}
// Filter out entities that are marked as hidden (this information is not
// available in the HA state, only in the registry).
this._mediaPlayers = [...mediaPlayerEntities.values()]
.filter((entity) => !entity.hidden_by)
.map((entity) => entity.entity_id);
}
/** /**
* Called before each update. * Called before each update.
*/ */
@@ -1006,25 +930,7 @@ class FrigateCard extends LitElement {
setPerformanceCSSStyles(this, this._cardWideConfig?.performance); setPerformanceCSSStyles(this, this._cardWideConfig?.performance);
} }
if ( this._initializeBackground();
this._hass &&
!this._mediaPlayers &&
this._getConfig().menu.buttons.media_player.enabled
) {
// Media players are initialized outside the main initialization code (the
// `initialize` method) since they may be required depending on an
// overridable configuration value.
// We also want to initialize media players after since the main camera
// initialization since that may have fetched entity information that will
// be cached and re-used here (minor performance optimization). Only do
// this if the media player button is enabled to further limit the
// performance implications.
// Prevent a double initialization.
this._mediaPlayers = [];
this._initializeMediaPlayers(this._hass);
}
if (this._view?.is('live')) { if (this._view?.is('live')) {
import('./components/live/live.js'); import('./components/live/live.js');
@@ -1147,30 +1053,171 @@ class FrigateCard extends LitElement {
} }
} }
protected async _initializeCameras(
hass: HomeAssistant,
config: FrigateCardConfig,
cardWideConfig: CardWideConfig,
): Promise<void> {
this._cameraManager = new CameraManager(
new CameraManagerEngineFactory(this._entityRegistryManager, cardWideConfig),
this._cardWideConfig,
);
// For each camera merge the config (which has no defaults) into the camera
// global config (which does have defaults). The merging must happen in this
// order, to ensure that the defaults in the cameras global config do not
// override the values specified in the per-camera config.
const cameras = config.cameras.map((camera) =>
merge(cloneDeep(config.cameras_global), camera),
);
try {
await this._cameraManager.initializeCameras(
hass,
this._entityRegistryManager,
cameras,
);
} catch (e: unknown) {
if (e instanceof Error) {
errorToConsole(e);
}
if (e instanceof FrigateCardError) {
this._setMessageAndUpdate({
message: e.message,
type: 'error',
context: e.context,
});
}
}
// If there's no view set yet, set one. This will be the case on initial camera load.
if (!this._view) {
// Don't reset the message which may be set to an error above. This sets the
// first view using the newly loaded cameras.
this._changeView({ resetMessage: false });
}
}
protected async _initializeMediaPlayers(hass: HomeAssistant): Promise<void> {
const isValidMediaPlayer = (entityID: string): boolean => {
if (entityID.startsWith('media_player.')) {
const stateObj = this._hass?.states[entityID];
if (
stateObj &&
stateObj.state !== 'unavailable' &&
supportsFeature(stateObj, MEDIA_PLAYER_SUPPORT_BROWSE_MEDIA)
) {
return true;
}
}
return false;
};
const mediaPlayers = Object.keys(this._hass?.states || {}).filter(
isValidMediaPlayer,
);
let mediaPlayerEntities: Map<string, Entity>;
try {
mediaPlayerEntities = await this._entityRegistryManager.getEntities(
hass,
mediaPlayers,
);
} catch (e) {
// Failing to fetch media player information is not considered
// sufficiently serious to block card startup.
errorToConsole(e as Error);
return;
}
// Filter out entities that are marked as hidden (this information is not
// available in the HA state, only in the registry).
this._mediaPlayers = [...mediaPlayerEntities.values()]
.filter((entity) => !entity.hidden_by)
.map((entity) => entity.entity_id);
}
/**
* Initialize the hard requirements for rendering anything.
* @returns `true` if card rendering can continue.
*/
protected _initializeMandatory(): boolean {
if (
this._initializer.isInitializedMultiple([
InitializationAspect.LANGUAGES,
InitializationAspect.SIDE_LOAD_ELEMENTS,
InitializationAspect.CAMERAS,
])
) {
return true;
}
const hass = this._hass;
const config = this._getConfig();
const cardWideConfig = this._cardWideConfig;
if (!hass || !config || !cardWideConfig) {
return false;
}
this._initializer
.initializeMultipleIfNecessary({
// Caution: Ensure nothing in this set of initializers requires
// languages since they will not yet have been initialized.
[InitializationAspect.LANGUAGES]: async () => loadLanguages,
[InitializationAspect.SIDE_LOAD_ELEMENTS]: async () =>
sideLoadHomeAssistantElements,
})
.then(() => {
return this._initializer.initializeIfNecessary(
InitializationAspect.CAMERAS,
async () => this._initializeCameras(hass, config, cardWideConfig),
);
})
.then((initialized) => {
if (initialized) {
return this.requestUpdate();
}
});
return false;
}
/**
* Initialize aspects of the card that can load in the 'background'.
* @returns `true` if card rendering can continue.
*/
protected _initializeBackground(): void {
if (this._initializer.isInitialized(InitializationAspect.MEDIA_PLAYERS)) {
return;
}
const hass = this._hass;
const config = this._getConfig();
if (!hass || !config) {
return;
}
this._initializer
.initializeMultipleIfNecessary({
...(config.menu.buttons.media_player.enabled && {
[InitializationAspect.MEDIA_PLAYERS]: async () =>
this._initializeMediaPlayers(hass),
}),
})
.then((initialized) => {
if (initialized) {
this.requestUpdate();
}
});
return;
}
/** /**
* Determine whether the element should be updated. * Determine whether the element should be updated.
* @param changedProps The changed properties if any. * @param changedProps The changed properties if any.
* @returns `true` if the element should be updated. * @returns `true` if the element should be updated.
*/ */
protected shouldUpdate(changedProps: PropertyValues): boolean { protected shouldUpdate(changedProps: PropertyValues): boolean {
// Load the relevant languages. Cannot do anything until then. if (!this._initializeMandatory()) {
if (this._initialized !== 'initialized') {
const config = this._getConfig();
if (
this._initialized !== 'initializing' &&
this._hass &&
config &&
this._cardWideConfig
) {
this._initialized = 'initializing';
this._initialize(this._hass, config, this._cardWideConfig).then(() => {
this._initialized = 'initialized';
this.requestUpdate();
});
}
return false; return false;
} }
const oldHass = changedProps.get('_hass') as HomeAssistant | undefined; const oldHass = changedProps.get('_hass') as HomeAssistant | undefined;
let shouldUpdate = !oldHass || changedProps.size != 1; let shouldUpdate = !oldHass || changedProps.size != 1;
+6 -99
View File
@@ -4,6 +4,9 @@ import isEqual from 'lodash-es/isEqual';
import set from 'lodash-es/set'; import set from 'lodash-es/set';
import { import {
CONF_CAMERAS, CONF_CAMERAS,
CONF_CAMERAS_GLOBAL_IMAGE,
CONF_CAMERAS_GLOBAL_JSMPEG,
CONF_CAMERAS_GLOBAL_WEBRTC_CARD,
CONF_ELEMENTS, CONF_ELEMENTS,
CONF_LIVE_AUTO_UNMUTE, CONF_LIVE_AUTO_UNMUTE,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE, CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
@@ -28,7 +31,6 @@ import {
import { import {
BUTTON_SIZE_MIN, BUTTON_SIZE_MIN,
RawFrigateCardConfig, RawFrigateCardConfig,
RawFrigateCardConfigArray,
THUMBNAIL_WIDTH_MAX, THUMBNAIL_WIDTH_MAX,
THUMBNAIL_WIDTH_MIN, THUMBNAIL_WIDTH_MIN,
} from './types'; } from './types';
@@ -91,7 +93,6 @@ export const upgradeConfig = function (obj: RawFrigateCardConfig): boolean {
for (let i = 0; i < UPGRADES.length; i++) { for (let i = 0; i < UPGRADES.length; i++) {
upgraded = UPGRADES[i](obj) || upgraded; upgraded = UPGRADES[i](obj) || upgraded;
} }
trimConfig(obj);
return upgraded; return upgraded;
}; };
@@ -104,28 +105,6 @@ export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean
return upgradeConfig(copyConfig(obj)); return upgradeConfig(copyConfig(obj));
}; };
/**
* Remove empty sections from a configuration.
* @param obj Configuration object.
* @returns `true` if the configuration was modified.
*/
const trimConfig = function (obj: RawFrigateCardConfig): boolean {
const keys = Object.keys(obj);
let modified = false;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (typeof obj[key] === 'object' && obj[key] != null) {
modified = trimConfig(obj[key] as RawFrigateCardConfig) || modified;
if (!Object.keys(obj[key] as RawFrigateCardConfig).length) {
delete obj[key];
modified = true;
}
}
}
return modified;
};
/** /**
* Copy a configuration. * Copy a configuration.
* @param obj Configuration to copy. * @param obj Configuration to copy.
@@ -523,80 +502,6 @@ const transformFrigateUIAction = (data: unknown): boolean => {
return false; return false;
}; };
/**
* Move live provider options exclusively into camera configs.
* @returns An upgrade function.
*/
const upgradeCameraOptionsFromLiveToMultipleCameras = (): ((
obj: RawFrigateCardConfig,
) => boolean) => {
return function (obj: RawFrigateCardConfig): boolean {
const cameras = getConfigValue(obj, CONF_CAMERAS) as
| RawFrigateCardConfigArray
| undefined;
if (cameras === undefined) {
return false;
}
const webrtcCardConfig = getConfigValue(obj, 'live.webrtc_card') as
| RawFrigateCardConfigArray
| undefined;
const imageConfig = getConfigValue(obj, 'live.image') as
| RawFrigateCardConfigArray
| undefined;
const jsmpegConfig = getConfigValue(obj, 'live.jsmpeg') as
| RawFrigateCardConfigArray
| undefined;
if (!webrtcCardConfig && !imageConfig && !jsmpegConfig) {
return false;
}
if (webrtcCardConfig) {
cameras.forEach((camera) => {
if (
camera.live_provider === 'webrtc_card' &&
(camera.webrtc_card === undefined || typeof camera.webrtc_card === 'object')
) {
camera.webrtc_card = { ...webrtcCardConfig, ...camera.webrtc_card };
}
});
}
if (imageConfig) {
cameras.forEach((camera) => {
if (
camera.live_provider === 'image' &&
(camera.image === undefined || typeof camera.image === 'object')
) {
camera.image = { ...imageConfig, ...camera.image };
}
});
}
if (jsmpegConfig) {
cameras.forEach((camera) => {
if (
camera.live_provider === 'jsmpeg' &&
(camera.jsmpeg === undefined || typeof camera.jsmpeg === 'object')
) {
camera.jsmpeg = { ...jsmpegConfig, ...camera.jsmpeg };
}
});
}
setConfigValue(obj, CONF_CAMERAS, cameras);
deleteConfigValue(obj, 'live.webrtc_card');
deleteConfigValue(obj, 'live.image');
deleteConfigValue(obj, 'live.jsmpeg');
// Note: This upgrade is imperfect. There could be override conditions being
// set that this upgrade cannot understand, e.g. if in fullscreen mode then
// refresh a live image more frequently. Such functionality is not possible
// after this change, since camera configs cannot be overrided.
return true;
};
};
const UPGRADES = [ const UPGRADES = [
// v3.0.0 -> v4.0.0-rc.1 // v3.0.0 -> v4.0.0-rc.1
upgradeWithOverrides( upgradeWithOverrides(
@@ -680,5 +585,7 @@ const UPGRADES = [
val === 'frigate-jsmpeg' ? 'jsmpeg' : val, val === 'frigate-jsmpeg' ? 'jsmpeg' : val,
), ),
), ),
upgradeCameraOptionsFromLiveToMultipleCameras(), upgradeMoveToWithOverrides('live.image', CONF_CAMERAS_GLOBAL_IMAGE),
upgradeMoveToWithOverrides('live.jsmpeg', CONF_CAMERAS_GLOBAL_JSMPEG),
upgradeMoveToWithOverrides('live.webrtc_card', CONF_CAMERAS_GLOBAL_WEBRTC_CARD),
]; ];
+6
View File
@@ -39,6 +39,12 @@ export const CONF_CAMERAS_ARRAY_TRIGGERS_OCCUPANCY =
export const CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES = export const CONF_CAMERAS_ARRAY_TRIGGERS_ENTITIES =
`${CONF_CAMERAS}.#.triggers.entities` as const; `${CONF_CAMERAS}.#.triggers.entities` as const;
export const CONF_CAMERAS_GLOBAL = 'cameras_global' as const;
export const CONF_CAMERAS_GLOBAL_IMAGE = `${CONF_CAMERAS_GLOBAL}.image` as const;
export const CONF_CAMERAS_GLOBAL_JSMPEG = `${CONF_CAMERAS_GLOBAL}.jsmpeg` as const;
export const CONF_CAMERAS_GLOBAL_WEBRTC_CARD =
`${CONF_CAMERAS_GLOBAL}.webrtc_card` as const;
export const CONF_ELEMENTS = 'elements' as const; export const CONF_ELEMENTS = 'elements' as const;
const CONF_VIEW = 'view' as const; const CONF_VIEW = 'view' as const;
+13 -7
View File
@@ -412,10 +412,8 @@ const go2rtcConfigSchema = z.object({
modes: z.enum(['webrtc', 'mse', 'mp4', 'mjpeg']).array().optional(), modes: z.enum(['webrtc', 'mse', 'mp4', 'mjpeg']).array().optional(),
stream: z.string().optional(), stream: z.string().optional(),
}); });
export type Go2rtcConfig = z.infer<typeof go2rtcConfigSchema>;
const liveImageConfigSchema = imageBaseConfigSchema; const liveImageConfigSchema = imageBaseConfigSchema;
export type LiveImageConfig = z.infer<typeof liveImageConfigSchema>;
const webrtcCardConfigSchema = z const webrtcCardConfigSchema = z
.object({ .object({
@@ -423,7 +421,6 @@ const webrtcCardConfigSchema = z
url: z.string().optional(), url: z.string().optional(),
}) })
.passthrough(); .passthrough();
export type WebRTCCardConfig = z.infer<typeof webrtcCardConfigSchema>;
const jsmpegConfigSchema = z.object({ const jsmpegConfigSchema = z.object({
options: z options: z
@@ -444,7 +441,6 @@ const jsmpegConfigSchema = z.object({
}) })
.optional(), .optional(),
}); });
export type JSMPEGConfig = z.infer<typeof jsmpegConfigSchema>;
/** /**
* Camera configuration section * Camera configuration section
@@ -524,7 +520,9 @@ const cameraConfigSchema = z
.default(cameraConfigDefault); .default(cameraConfigDefault);
export type CameraConfig = z.infer<typeof cameraConfigSchema>; export type CameraConfig = z.infer<typeof cameraConfigSchema>;
const camerasConfigSchema = cameraConfigSchema.array().nonempty(); // Avoid using .nonempty() to avoid changing the inferred type
// (https://github.com/colinhacks/zod#minmaxlength).
const camerasConfigSchema = cameraConfigSchema.array().min(1);
export type CamerasConfig = z.infer<typeof camerasConfigSchema>; export type CamerasConfig = z.infer<typeof camerasConfigSchema>;
/** /**
@@ -1200,6 +1198,8 @@ export type TimelineConfig = z.infer<typeof timelineConfigSchema>;
// Strip all defaults from the override schemas, to ensure values are only what // Strip all defaults from the override schemas, to ensure values are only what
// the user has specified. // the user has specified.
const overrideConfigurationSchema = z.object({ const overrideConfigurationSchema = z.object({
cameras: deepRemoveDefaults(camerasConfigSchema).optional(),
cameras_global: deepRemoveDefaults(cameraConfigSchema).optional(),
live: deepRemoveDefaults(liveOverridableConfigSchema).optional(), live: deepRemoveDefaults(liveOverridableConfigSchema).optional(),
menu: deepRemoveDefaults(menuConfigSchema).optional(), menu: deepRemoveDefaults(menuConfigSchema).optional(),
image: deepRemoveDefaults(imageConfigSchema).optional(), image: deepRemoveDefaults(imageConfigSchema).optional(),
@@ -1282,8 +1282,14 @@ export interface CardWideConfig {
* Main card config. * Main card config.
*/ */
export const frigateCardConfigSchema = z.object({ export const frigateCardConfigSchema = z.object({
// Main configuration sections. // Defaults are stripped out of the individual cameras, since each camera will
cameras: camerasConfigSchema, // be merged with `cameras_global` which *does* have defaults. If we didn't do
// this, the default values of each individual camera would override the
// intentionally specified values in `cameras_global` during camera
// initialization when the two configs are merged.
cameras: deepRemoveDefaults(camerasConfigSchema),
cameras_global: cameraConfigSchema,
view: viewConfigSchema, view: viewConfigSchema,
menu: menuConfigSchema, menu: menuConfigSchema,
live: liveConfigSchema, live: liveConfigSchema,
+69
View File
@@ -0,0 +1,69 @@
import { allPromises } from './basic';
enum InitializationState {
INITIALIZING = 'initializing',
INITIALIZED = 'initialized',
}
type Initializer = () => Promise<unknown>;
/**
* Manages initialization state & calling initializers.
*/
export class FrigateCardInitializer {
protected _state: Map<string, InitializationState>;
constructor() {
this._state = new Map();
}
public async initializeMultipleIfNecessary(
aspects: Record<string, Initializer>,
): Promise<boolean> {
const results = await allPromises(
Object.entries(aspects),
async ([aspect, options]) => this.initializeIfNecessary(aspect, options),
);
return results.every(Boolean);
}
/**
*
* @param aspect The aspect to initialize.
* @param initializer The initializer to call.
* @returns `true` if the state is confirmed as initialized, `false`
* otherwise (i.e. initializing).
*/
public async initializeIfNecessary(
aspect: string,
initializer?: Initializer,
): Promise<boolean> {
const state = this._state.get(aspect);
if (state !== InitializationState.INITIALIZED) {
if (state !== InitializationState.INITIALIZING) {
if (initializer) {
this._state.set(aspect, InitializationState.INITIALIZING);
await initializer();
this._state.set(aspect, InitializationState.INITIALIZED);
} else {
this._state.set(aspect, InitializationState.INITIALIZED);
}
return true;
}
return false;
}
return true;
}
public uninitialize(aspect: string) {
return this._state.delete(aspect);
}
public isInitialized(aspect: string): boolean {
return this._state.get(aspect) == InitializationState.INITIALIZED;
}
public isInitializedMultiple(aspects: string[]): boolean {
return aspects.every((aspect) => this.isInitialized(aspect));
}
}