Add support for global cameras options

This commit is contained in:
Dermot Duffy
2023-03-04 10:03:16 -08:00
parent 072af6941d
commit 6a55cf9007
6 changed files with 369 additions and 266 deletions
+100 -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.
<a name="camera-options"></a>
### 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:
@@ -99,17 +101,17 @@ See the [fully expanded cameras configuration example](#config-expanded-cameras)
| 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. |
| `live_provider` | `auto` | :heavy_multiplication_x: | The choice of live stream provider. See [Live Providers](#live-providers) below.|
| `title` | Autodetected from `camera_entity` if that is specified. | :heavy_multiplication_x: | A friendly name for this camera to use in the card. |
| `icon` | Autodetected from `camera_entity` if that is specified. | :heavy_multiplication_x: | The icon to use for this camera in the camera menu and in the next & previous controls when using the `icon` style. |
| `hide` | `false` | :heavy_multiplication_x: | Whether or not to hide this as an independent camera (e.g. hidden on the live carousel, media filter, camera menu, and triggers cannot trigger this camera). This may be useful if this camera is exclusively used as a dependency of another camera. |
| `id` | `camera_entity`, `webrtc_card.entity` or `frigate.camera_name` if set (in that preference order). | :heavy_multiplication_x: | An optional identifier to use throughout the card configuration to refer unambiguously to this camera. See [camera IDs](#camera-ids). |
| `engine` | `auto` | :heavy_multiplication_x: | Which camera engine to use for this camera. If `auto` the card will attempt to choose the correct engine from the specified options. See [engines](#engines) below for valid options.|
| `frigate` | | :heavy_multiplication_x: | Options for a Frigate camera. See [Frigate configuration](#camera-frigate-configuration) below. |
| `dependencies` | | :heavy_multiplication_x: | 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. |
| `webrtc_card` | | :heavy_multiplication_x: | The WebRTC entity/URL to use for this camera with the `webrtc-card` live provider. See below. |
| `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` | :white_check_mark: | The choice of live stream provider. See [Live Providers](#live-providers) below.|
| `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. | :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` | :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). | :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` | :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` | | :white_check_mark: | Options for a Frigate camera. See [Frigate configuration](#camera-frigate-configuration) below. |
| `dependencies` | | :white_check_mark: | Other cameras that this camera should depend upon. See [camera dependencies](#camera-dependencies-configuration) below. |
| `triggers` | | :white_check_mark: | Define what should cause this camera to update/trigger. See [camera triggers](#camera-trigger-configuration) 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>
@@ -156,11 +158,11 @@ cameras:
| 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.|
| `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. |
| `label` | | :heavy_multiplication_x: | 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`.|
| `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).|
| `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` | | :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` | | :white_check_mark: | A Frigate label / object filter used to filter events (clips & snapshots), e.g. `person`.|
| `zone` | | :white_check_mark: | A Frigate zone used to filter events (clips & snapshots), e.g. `front_door`.|
| `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
@@ -173,8 +175,8 @@ cameras:
| 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. |
| `stream` | Determind by camera engine (e.g. `frigate` camera name). | :heavy_multiplication_x: | A valid `go2rtc` stream name. |
| `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). | :white_check_mark: | A valid `go2rtc` stream name. |
#### Live Provider: Camera WebRTC Card configuration
@@ -187,9 +189,9 @@ cameras:
| Option | Default | Overridable | Description |
| - | - | - | - |
| `entity` | | :heavy_multiplication_x: | 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. |
| `*`| | :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. |
| `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/))| :white_check_mark: | The RTSP url to pass to the WebRTC Card. |
| `*`| | :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.
@@ -205,8 +207,8 @@ cameras:
| Option | Default | Overridable | Description |
| - | - | - | - |
| `refresh_seconds` | 1 | :heavy_multiplication_x: | 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. |
| `refresh_seconds` | 1 | :white_check_mark: | The image will be refreshed at least every `refresh_seconds`. `0` implies no refreshing. |
| `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
@@ -219,7 +221,7 @@ cameras:
| 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>
@@ -236,8 +238,8 @@ cameras:
| Option | Default | Overridable | Description |
| - | - | - | - |
| `cameras` | | :heavy_multiplication_x: | An optional array of other camera identifiers (see [camera IDs](#camera-ids)). If specified the card will fetch 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.|
| `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` | :white_check_mark: | Shortcut to specify all other cameras as dependent cameras.|
<a name="camera-triggers-configuration"></a>
@@ -252,9 +254,9 @@ cameras:
| 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.|
| `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.|
| `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.|
| `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` | :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` | | :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>
@@ -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.
<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
All configuration is under:
@@ -1415,6 +1431,61 @@ cameras:
```
</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>
<details>
+182 -133
View File
@@ -61,7 +61,7 @@ import {
frigateCardHasAction,
getActionConfigGivenAction,
} from './utils/action.js';
import { contentsChanged, errorToConsole } from './utils/basic.js';
import { errorToConsole } from './utils/basic.js';
import {
getEntityIcon,
getEntityTitle,
@@ -87,6 +87,10 @@ import { EntityRegistryManager } from './utils/ha/entity-registry/index.js';
import { EntityCache } from './utils/ha/entity-registry/cache.js';
import { Entity, ExtendedEntity } from './utils/ha/entity-registry/types.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:
*
@@ -135,7 +139,12 @@ console.info(
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.
@@ -202,9 +211,6 @@ class FrigateCard extends LitElement {
// per second for performance reasons.
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 _untriggerTimerID: number | null = null;
@@ -212,6 +218,8 @@ class FrigateCard extends LitElement {
protected _mediaPlayers?: string[];
protected _initializer = new FrigateCardInitializer();
constructor() {
super();
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
// 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;
}
}
@@ -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.
*/
@@ -1006,25 +930,7 @@ class FrigateCard extends LitElement {
setPerformanceCSSStyles(this, this._cardWideConfig?.performance);
}
if (
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);
}
this._initializeBackground();
if (this._view?.is('live')) {
import('./components/live/live.js');
@@ -1147,30 +1053,173 @@ 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 into the camera global config. 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),
);
console.info("MERGED CAMERAS", cameras);
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.
* @param changedProps The changed properties if any.
* @returns `true` if the element should be updated.
*/
protected shouldUpdate(changedProps: PropertyValues): boolean {
// Load the relevant languages. Cannot do anything until then.
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();
});
}
if (!this._initializeMandatory()) {
return false;
}
const oldHass = changedProps.get('_hass') as HomeAssistant | undefined;
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 {
CONF_CAMERAS,
CONF_CAMERAS_GLOBAL_IMAGE,
CONF_CAMERAS_GLOBAL_JSMPEG,
CONF_CAMERAS_GLOBAL_WEBRTC_CARD,
CONF_ELEMENTS,
CONF_LIVE_AUTO_UNMUTE,
CONF_LIVE_CONTROLS_NEXT_PREVIOUS_SIZE,
@@ -28,7 +31,6 @@ import {
import {
BUTTON_SIZE_MIN,
RawFrigateCardConfig,
RawFrigateCardConfigArray,
THUMBNAIL_WIDTH_MAX,
THUMBNAIL_WIDTH_MIN,
} from './types';
@@ -91,7 +93,6 @@ export const upgradeConfig = function (obj: RawFrigateCardConfig): boolean {
for (let i = 0; i < UPGRADES.length; i++) {
upgraded = UPGRADES[i](obj) || upgraded;
}
trimConfig(obj);
return upgraded;
};
@@ -104,28 +105,6 @@ export const isConfigUpgradeable = function (obj: RawFrigateCardConfig): boolean
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.
* @param obj Configuration to copy.
@@ -523,80 +502,6 @@ const transformFrigateUIAction = (data: unknown): boolean => {
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 = [
// v3.0.0 -> v4.0.0-rc.1
upgradeWithOverrides(
@@ -680,5 +585,7 @@ const UPGRADES = [
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 =
`${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;
const CONF_VIEW = 'view' as const;
+6 -5
View File
@@ -412,10 +412,8 @@ const go2rtcConfigSchema = z.object({
modes: z.enum(['webrtc', 'mse', 'mp4', 'mjpeg']).array().optional(),
stream: z.string().optional(),
});
export type Go2rtcConfig = z.infer<typeof go2rtcConfigSchema>;
const liveImageConfigSchema = imageBaseConfigSchema;
export type LiveImageConfig = z.infer<typeof liveImageConfigSchema>;
const webrtcCardConfigSchema = z
.object({
@@ -423,7 +421,6 @@ const webrtcCardConfigSchema = z
url: z.string().optional(),
})
.passthrough();
export type WebRTCCardConfig = z.infer<typeof webrtcCardConfigSchema>;
const jsmpegConfigSchema = z.object({
options: z
@@ -444,7 +441,6 @@ const jsmpegConfigSchema = z.object({
})
.optional(),
});
export type JSMPEGConfig = z.infer<typeof jsmpegConfigSchema>;
/**
* Camera configuration section
@@ -524,7 +520,9 @@ const cameraConfigSchema = z
.default(cameraConfigDefault);
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>;
/**
@@ -1200,6 +1198,8 @@ export type TimelineConfig = z.infer<typeof timelineConfigSchema>;
// Strip all defaults from the override schemas, to ensure values are only what
// the user has specified.
const overrideConfigurationSchema = z.object({
cameras: deepRemoveDefaults(camerasConfigSchema).optional(),
cameras_global: deepRemoveDefaults(cameraConfigSchema).optional(),
live: deepRemoveDefaults(liveOverridableConfigSchema).optional(),
menu: deepRemoveDefaults(menuConfigSchema).optional(),
image: deepRemoveDefaults(imageConfigSchema).optional(),
@@ -1284,6 +1284,7 @@ export interface CardWideConfig {
export const frigateCardConfigSchema = z.object({
// Main configuration sections.
cameras: camerasConfigSchema,
cameras_global: cameraConfigSchema.optional(),
view: viewConfigSchema,
menu: menuConfigSchema,
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));
}
}