Add support for more advanced forms of overriding
This commit is contained in:
+150
-23
@@ -1,39 +1,166 @@
|
||||
# `overrides`
|
||||
|
||||
Various parts of card configuration may [conditionally](conditions.md) be
|
||||
overridden (e.g. to hide the menu in fullscreen mode).
|
||||
The card configuration may [conditionally](conditions.md) be overridden (e.g. to
|
||||
hide the menu in fullscreen mode).
|
||||
|
||||
```yaml
|
||||
overrides:
|
||||
- conditions:
|
||||
[condition]
|
||||
overrides:
|
||||
[override]
|
||||
[...]
|
||||
```
|
||||
|
||||
Not all configuration parameters are overriddable, some because it doesn't make
|
||||
sense for that parameter to vary, and many because of the extra complexity of
|
||||
supporting overriding given the lack of compelling usecases ([please request new
|
||||
overridable parameters
|
||||
here!](https://github.com/dermotduffy/frigate-hass-card/issues/new/choose)).
|
||||
!> Whilst all configuration parameters are theoretically overridable, in some instances a configuration variable may only be consulted on startup or changing its value may negatively impact behavior -- override results may vary!
|
||||
|
||||
Each entry under the top-level `overrides` configuration block should be a list
|
||||
item, that has both of the following parameters set:
|
||||
The top-level `overrides` configuration block expects a list, with each list
|
||||
item containing `conditions` and at least one of `merge`, `delete` or `set` specified.
|
||||
|
||||
| Option | Default | Description |
|
||||
| - | - | - |
|
||||
| `conditions` | | A list of [conditions](conditions.md) that must evaluate to `true` in order for the overrides to be applied. |
|
||||
| `overrides` | | Configuration overrides to be applied. Any configuration parameter matching [Overrideable parameters](overrides.md?id=overrideable-parameters) can be overridden. |
|
||||
| `delete` | | An array of configuration paths to delete. See below. |
|
||||
| `merge` | | A dictionary of configuration paths to merge. See below. |
|
||||
| `set` | | A dictionary of configuration paths to set. See below. |
|
||||
|
||||
## Overrideable parameters
|
||||
## Configuration Paths
|
||||
|
||||
| Configuration Key | Overrideable |
|
||||
| - | - |
|
||||
| [`cameras.*`](cameras/README.md) | :white_check_mark: |
|
||||
| [`cameras_global.*`](cameras/README.md) | :white_check_mark: |
|
||||
| [`dimensions.*`](dimensions.md) | :white_check_mark: |
|
||||
| [`image.*`](image.md) | :white_check_mark: |
|
||||
| [`live.controls.*`](live.md?id=controls), [`live.display.*`](live.md?id=display), [`live.microphone.*`](live.md?id=microphone), [`live.show_image_during_load`](live.md), [`live.zoomable`](live.md) | :white_check_mark: |
|
||||
| [`menu.*`](menu.md) | :white_check_mark: |
|
||||
| [`view.*`](view.md) | :white_check_mark: |
|
||||
| *(Everything else)* | :heavy_multiplication_x: |
|
||||
The `delete`, `merge` and `set` parameters take configuration paths. Paths are dot-separated references to particular configuration parameters. To refer to list elements use `[n]` notation.
|
||||
|
||||
For example the path `cameras[1].dimensions.aspect_ratio` refers to the `aspect_ratio` parameter below:
|
||||
|
||||
```yaml
|
||||
cameras:
|
||||
- camera_entity: camera.other
|
||||
- camera_entity: camera.relevant
|
||||
dimensions:
|
||||
aspect_ratio: '16:9'
|
||||
```
|
||||
|
||||
## `delete`
|
||||
|
||||
An array of configuration paths to delete.
|
||||
|
||||
```yaml
|
||||
overrides:
|
||||
- conditions:
|
||||
[condition]
|
||||
delete:
|
||||
- [path_1]
|
||||
- [path_2]
|
||||
```
|
||||
|
||||
### Examples
|
||||
|
||||
Delete the 2nd camera:
|
||||
|
||||
```yaml
|
||||
overrides:
|
||||
- conditions:
|
||||
[condition]
|
||||
delete:
|
||||
'cameras[2]'
|
||||
```
|
||||
|
||||
Delete the menu style parameter, thus falling back to the default:
|
||||
|
||||
```yaml
|
||||
overrides:
|
||||
- conditions:
|
||||
[condition]
|
||||
delete:
|
||||
'menu.style'
|
||||
```
|
||||
|
||||
## `merge`
|
||||
|
||||
Specifies an object to recursively merge into existing configuration.
|
||||
|
||||
| Option | Default | Description |
|
||||
| - | - | - |
|
||||
| [configuration path] | | Arbitrary configuration object to merge. Must be an object (i.e. not a literal value). |
|
||||
|
||||
### Examples
|
||||
|
||||
Hide the menu when a given condition is met:
|
||||
|
||||
```yaml
|
||||
overrides:
|
||||
- conditions:
|
||||
[condition]
|
||||
merge:
|
||||
menu: {
|
||||
style: 'hidden'
|
||||
}
|
||||
```
|
||||
|
||||
Enable thumbnails below the `live` feed:
|
||||
|
||||
```yaml
|
||||
overrides:
|
||||
- conditions:
|
||||
[condition]
|
||||
merge:
|
||||
'live.controls.thumbnails': {
|
||||
mode: 'below'
|
||||
}
|
||||
```
|
||||
|
||||
Also enables thumbnails below the `live` feed, but without using the dot-separated notation:
|
||||
|
||||
```yaml
|
||||
overrides:
|
||||
- conditions:
|
||||
[condition]
|
||||
merge:
|
||||
live: {
|
||||
controls: {
|
||||
thumbnails: {
|
||||
mode: 'below'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## `set`
|
||||
|
||||
Specifies a value to set in the configuration. This differs from `merge` in that the existing value is entirely replaced.
|
||||
|
||||
| Option | Default | Description |
|
||||
| - | - | - |
|
||||
| [configuration path] | | Arbitrary configuration value / object / list to set. |
|
||||
|
||||
### Examples
|
||||
|
||||
Set the entire menu configuration to defaults with the exception of the `style` which is set to `overlay`.
|
||||
|
||||
```yaml
|
||||
overrides:
|
||||
- conditions:
|
||||
[condition]
|
||||
set:
|
||||
menu: {
|
||||
style: 'overlay'
|
||||
}
|
||||
```
|
||||
|
||||
Set the menu style but without touching the other `menu` parameters:
|
||||
|
||||
```yaml
|
||||
overrides:
|
||||
- conditions:
|
||||
[condition]
|
||||
set:
|
||||
'menu.style': 'overlay'
|
||||
```
|
||||
|
||||
That is equivalent to merging the following:
|
||||
|
||||
```yaml
|
||||
overrides:
|
||||
- conditions:
|
||||
[condition]
|
||||
merge:
|
||||
menu: {
|
||||
style: 'overlay'
|
||||
}
|
||||
```
|
||||
+25
-7
@@ -424,7 +424,7 @@ overrides:
|
||||
- condition: state
|
||||
entity: light.office_main_lights
|
||||
state: 'on'
|
||||
overrides:
|
||||
merge:
|
||||
menu:
|
||||
position: bottom
|
||||
```
|
||||
@@ -451,7 +451,7 @@ overrides:
|
||||
- condition: state
|
||||
entity: binary_sensor.alarm_armed
|
||||
state: 'off'
|
||||
overrides:
|
||||
merge:
|
||||
view:
|
||||
default: image
|
||||
```
|
||||
@@ -471,7 +471,7 @@ overrides:
|
||||
fullscreen: true
|
||||
- condition: display_mode
|
||||
display_mode: grid
|
||||
overrides:
|
||||
merge:
|
||||
live:
|
||||
display:
|
||||
grid_columns: 5
|
||||
@@ -497,7 +497,7 @@ overrides:
|
||||
- conditions:
|
||||
- condition: expand
|
||||
expand: true
|
||||
overrides:
|
||||
merge:
|
||||
menu:
|
||||
style: overlay
|
||||
```
|
||||
@@ -520,11 +520,29 @@ overrides:
|
||||
- conditions:
|
||||
- condition: fullscreen
|
||||
fullscreen: true
|
||||
overrides:
|
||||
merge:
|
||||
menu:
|
||||
style: none
|
||||
```
|
||||
|
||||
### Remove a camera when an entity state changes
|
||||
|
||||
This example removes a camera from the card when an entity is disabled (e.g. a switch controlling power to the camera).
|
||||
|
||||
```yaml
|
||||
type: custom:frigate-card
|
||||
cameras:
|
||||
- camera_entity: camera.office
|
||||
- camera_entity: camera.kitchen
|
||||
overrides:
|
||||
- conditions:
|
||||
- condition: state
|
||||
entity: switch.kitchen_camera_power
|
||||
state: off
|
||||
delete:
|
||||
- 'cameras[1]'
|
||||
```
|
||||
|
||||
## PTZ control
|
||||
|
||||
The card supports using PTZ controls to conveniently control pan, tilt and zoom for cameras. This example shows the PTZ controls on the `live` view. Note that if your camera engine supports it (e.g. `frigate`) this will just work out of the box with no configuration at all.
|
||||
@@ -568,7 +586,7 @@ overrides:
|
||||
- conditions:
|
||||
- condition: screen
|
||||
media_query: '(orientation: landscape)'
|
||||
overrides:
|
||||
merge:
|
||||
menu:
|
||||
position: left
|
||||
```
|
||||
@@ -584,7 +602,7 @@ overrides:
|
||||
- conditions:
|
||||
- condition: screen
|
||||
media_query: '(max-width: 300px)'
|
||||
overrides:
|
||||
merge:
|
||||
menu:
|
||||
style: none
|
||||
live:
|
||||
|
||||
+2
-2
@@ -61,7 +61,7 @@
|
||||
"@types/masonry-layout": "^4.2.5",
|
||||
"@typescript-eslint/eslint-plugin": "^5.36.2",
|
||||
"@typescript-eslint/parser": "^5.36.2",
|
||||
"@vitest/coverage-istanbul": "^1.3.1",
|
||||
"@vitest/coverage-istanbul": "^1.5.0",
|
||||
"docsify-cli": "^4.4.4",
|
||||
"eslint": "^8.23.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
@@ -81,7 +81,7 @@
|
||||
"sass": "^1.54.9",
|
||||
"ts-prune": "^0.10.3",
|
||||
"typescript": "^4.9.5",
|
||||
"vitest": "^1.3.1",
|
||||
"vitest": "^1.5.0",
|
||||
"vitest-mock-extended": "^1.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
||||
@@ -145,7 +145,7 @@ export class CameraManager {
|
||||
// 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) =>
|
||||
recursivelyMergeObjectsNotArrays(cloneDeep(config?.cameras_global), camera),
|
||||
recursivelyMergeObjectsNotArrays({}, cloneDeep(config?.cameras_global), camera),
|
||||
);
|
||||
|
||||
try {
|
||||
|
||||
@@ -1,14 +1,21 @@
|
||||
import { CurrentUser } from '@dermotduffy/custom-card-helpers';
|
||||
import { HassEntities } from 'home-assistant-js-websocket';
|
||||
import merge from 'lodash-es/merge';
|
||||
import { copyConfig } from '../config/management';
|
||||
import { ZodSchema } from 'zod';
|
||||
import {
|
||||
copyConfig,
|
||||
deleteConfigValue,
|
||||
getConfigValue,
|
||||
setConfigValue,
|
||||
} from '../config/management';
|
||||
import {
|
||||
FrigateCardCondition,
|
||||
frigateConditionalSchema,
|
||||
OverrideConfigurationKey,
|
||||
RawFrigateCardConfig,
|
||||
ViewDisplayMode,
|
||||
frigateConditionalSchema,
|
||||
Overrides,
|
||||
} from '../config/types';
|
||||
import { desparsifyArrays } from '../utils/basic';
|
||||
import { CardConditionAPI } from './types';
|
||||
|
||||
interface MicrophoneConditionState {
|
||||
@@ -73,44 +80,66 @@ export function evaluateConditionViaEvent(
|
||||
return evaluateEvent.evaluation ?? false;
|
||||
}
|
||||
|
||||
type RawOverrides = {
|
||||
conditions: FrigateCardCondition[];
|
||||
overrides: RawFrigateCardConfig;
|
||||
}[];
|
||||
|
||||
export function getOverriddenConfig(
|
||||
manager: Readonly<ConditionsManager>,
|
||||
config: Readonly<RawFrigateCardConfig>,
|
||||
configOverrides?: Readonly<RawOverrides>,
|
||||
stateOverrides?: Partial<ConditionState>,
|
||||
options?: {
|
||||
configOverrides?: Readonly<Overrides>;
|
||||
stateOverrides?: Partial<ConditionState>;
|
||||
schema?: ZodSchema;
|
||||
logOnParseError?: boolean;
|
||||
},
|
||||
): RawFrigateCardConfig {
|
||||
const output = copyConfig(config);
|
||||
let output = copyConfig(config);
|
||||
let overridden = false;
|
||||
if (configOverrides) {
|
||||
for (const override of configOverrides) {
|
||||
if (manager.evaluateConditions(override.conditions, stateOverrides)) {
|
||||
merge(output, override.overrides);
|
||||
if (options?.configOverrides) {
|
||||
for (const override of options.configOverrides) {
|
||||
if (manager.evaluateConditions(override.conditions, options?.stateOverrides)) {
|
||||
override.delete?.forEach((deletionKey) => {
|
||||
deleteConfigValue(output, deletionKey);
|
||||
});
|
||||
|
||||
Object.keys(override.set ?? {}).forEach((setKey) => {
|
||||
setConfigValue(output, setKey, override.set?.[setKey]);
|
||||
});
|
||||
|
||||
Object.keys(override.merge ?? {}).forEach((mergeKey) => {
|
||||
setConfigValue(
|
||||
output,
|
||||
mergeKey,
|
||||
merge({}, getConfigValue(output, mergeKey), override.merge?.[mergeKey]),
|
||||
);
|
||||
});
|
||||
|
||||
overridden = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Attempt to return the same configuration object if it has not been
|
||||
// overridden (to reduce re-renders for a configuration that has not changed).
|
||||
return overridden ? output : config;
|
||||
}
|
||||
|
||||
export function getOverridesByKey(
|
||||
key: OverrideConfigurationKey,
|
||||
overrides?: Readonly<RawOverrides>,
|
||||
): RawOverrides {
|
||||
return (
|
||||
overrides
|
||||
?.filter((o) => key in o.overrides)
|
||||
.map((o) => ({
|
||||
conditions: o.conditions,
|
||||
overrides: o.overrides[key] as RawFrigateCardConfig,
|
||||
})) ?? []
|
||||
);
|
||||
if (!overridden) {
|
||||
// Attempt to return the same configuration object if it has not been
|
||||
// overridden (to reduce re-renders for a configuration that has not changed).
|
||||
return config;
|
||||
}
|
||||
|
||||
if (options?.configOverrides?.some((override) => override.delete?.length)) {
|
||||
// If anything was deleted during this override, empty undefined slots may
|
||||
// be left in arrays where values were unset. Desparsify them.
|
||||
output = desparsifyArrays(output);
|
||||
}
|
||||
|
||||
if (options?.schema) {
|
||||
const parseResult = options.schema.safeParse(output);
|
||||
if (options.logOnParseError && !parseResult.success) {
|
||||
console.warn(
|
||||
`Cannot parse overridden configuration`,
|
||||
output,
|
||||
parseResult.error.message,
|
||||
);
|
||||
}
|
||||
return parseResult.success ? parseResult.data : config;
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
// A tiny wrapper interface to allow the same manager to be passed around
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
CardWideConfig,
|
||||
FrigateCardConfig,
|
||||
frigateCardConfigSchema,
|
||||
RawFrigateCardConfig
|
||||
RawFrigateCardConfig,
|
||||
} from '../config/types';
|
||||
import { localize } from '../localize/localize';
|
||||
import { setProfiles } from '../config/profiles';
|
||||
@@ -104,11 +104,11 @@ export class ConfigManager {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const overriddenConfig = getOverriddenConfig(
|
||||
conditionsManager,
|
||||
this._config,
|
||||
this._config.overrides,
|
||||
) as FrigateCardConfig;
|
||||
const overriddenConfig = getOverriddenConfig(conditionsManager, this._config, {
|
||||
configOverrides: this._config.overrides,
|
||||
schema: frigateCardConfigSchema,
|
||||
logOnParseError: !!this.getCardWideConfig()?.debug?.logging,
|
||||
}) as FrigateCardConfig;
|
||||
|
||||
// Save on Lit re-rendering costs by only updating the configuration if it
|
||||
// actually changes.
|
||||
|
||||
+18
-13
@@ -26,8 +26,9 @@ import {
|
||||
CardWideConfig,
|
||||
frigateCardConfigDefaults,
|
||||
LiveConfig,
|
||||
LiveOverrides,
|
||||
liveConfigAbsoluteRootSchema,
|
||||
LiveProvider,
|
||||
Overrides,
|
||||
TransitionEffect,
|
||||
} from '../../config/types.js';
|
||||
import { localize } from '../../localize/localize.js';
|
||||
@@ -80,7 +81,7 @@ export class FrigateCardLive extends LitElement {
|
||||
public overriddenLiveConfig?: LiveConfig;
|
||||
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public liveOverrides?: LiveOverrides;
|
||||
public overrides?: Overrides;
|
||||
|
||||
@property({ attribute: false })
|
||||
public cameraManager?: CameraManager;
|
||||
@@ -151,7 +152,7 @@ export class FrigateCardLive extends LitElement {
|
||||
.overriddenLiveConfig=${this.overriddenLiveConfig}
|
||||
.inBackground=${this._controller.isInBackground()}
|
||||
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
||||
.liveOverrides=${this.liveOverrides}
|
||||
.overrides=${this.overrides}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.microphoneManager=${this.microphoneManager}
|
||||
@@ -182,7 +183,7 @@ export class FrigateCardLiveGrid extends LitElement {
|
||||
public overriddenLiveConfig?: LiveConfig;
|
||||
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public liveOverrides?: LiveOverrides;
|
||||
public overrides?: Overrides;
|
||||
|
||||
@property({ attribute: false })
|
||||
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
||||
@@ -211,7 +212,7 @@ export class FrigateCardLiveGrid extends LitElement {
|
||||
.nonOverriddenLiveConfig=${this.nonOverriddenLiveConfig}
|
||||
.overriddenLiveConfig=${this.overriddenLiveConfig}
|
||||
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
||||
.liveOverrides=${this.liveOverrides}
|
||||
.overrides=${this.overrides}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.microphoneManager=${this.microphoneManager}
|
||||
@@ -290,7 +291,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
public overriddenLiveConfig?: LiveConfig;
|
||||
|
||||
@property({ attribute: false, hasChanged: contentsChanged })
|
||||
public liveOverrides?: LiveOverrides;
|
||||
public overrides?: Overrides;
|
||||
|
||||
@property({ attribute: false })
|
||||
public conditionsManagerEpoch?: ConditionsManagerEpoch;
|
||||
@@ -471,19 +472,23 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
// (in the carousel for example) is not necessarily the live camera *this*
|
||||
// <frigate-card-live-provider> is rendering right now, so we provide a
|
||||
// stateOverride to evaluate the condition in that context.
|
||||
const config = getOverriddenConfig(
|
||||
const liveConfig = getOverriddenConfig(
|
||||
this.conditionsManagerEpoch.manager,
|
||||
this.nonOverriddenLiveConfig,
|
||||
this.liveOverrides,
|
||||
{ camera: cameraID },
|
||||
) as LiveConfig;
|
||||
{ live: this.nonOverriddenLiveConfig },
|
||||
{
|
||||
configOverrides: this.overrides,
|
||||
stateOverrides: { camera: cameraID },
|
||||
schema: liveConfigAbsoluteRootSchema,
|
||||
logOnParseError: !!this.cardWideConfig?.debug?.logging,
|
||||
},
|
||||
).live as LiveConfig;
|
||||
|
||||
const cameraMetadata = this.cameraManager.getCameraMetadata(cameraID);
|
||||
|
||||
return html`
|
||||
<div class="embla__slide">
|
||||
<frigate-card-live-provider
|
||||
?load=${!config.lazy_load}
|
||||
?load=${!liveConfig.lazy_load}
|
||||
.microphoneStream=${this.view?.camera === cameraID
|
||||
? this.microphoneManager?.getStream()
|
||||
: undefined}
|
||||
@@ -493,7 +498,7 @@ export class FrigateCardLiveCarousel extends LitElement {
|
||||
() => this.cameraManager?.getCameraEndpoints(cameraID) ?? undefined,
|
||||
)}
|
||||
.label=${cameraMetadata?.title ?? ''}
|
||||
.liveConfig=${config}
|
||||
.liveConfig=${liveConfig}
|
||||
.hass=${this.hass}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
>
|
||||
|
||||
+2
-12
@@ -9,10 +9,7 @@ import {
|
||||
import { customElement, property } from 'lit/decorators.js';
|
||||
import { classMap } from 'lit/directives/class-map.js';
|
||||
import { CameraManager } from '../camera-manager/manager.js';
|
||||
import {
|
||||
ConditionsManagerEpoch,
|
||||
getOverridesByKey,
|
||||
} from '../card-controller/conditions-manager.js';
|
||||
import { ConditionsManagerEpoch } from '../card-controller/conditions-manager.js';
|
||||
import { ReadonlyMicrophoneManager } from '../card-controller/microphone-manager.js';
|
||||
import {
|
||||
CardWideConfig,
|
||||
@@ -233,10 +230,7 @@ export class FrigateCardViews extends LitElement {
|
||||
.nonOverriddenLiveConfig=${this.nonOverriddenConfig.live}
|
||||
.overriddenLiveConfig=${this.overriddenConfig.live}
|
||||
.conditionsManagerEpoch=${this.conditionsManagerEpoch}
|
||||
.liveOverrides=${getOverridesByKey(
|
||||
'live',
|
||||
this.overriddenConfig.overrides,
|
||||
)}
|
||||
.overrides=${this.overriddenConfig.overrides}
|
||||
.cameraManager=${this.cameraManager}
|
||||
.cardWideConfig=${this.cardWideConfig}
|
||||
.microphoneManager=${this.microphoneManager}
|
||||
@@ -248,10 +242,6 @@ export class FrigateCardViews extends LitElement {
|
||||
: ``
|
||||
}
|
||||
</frigate-card-surround>`;
|
||||
|
||||
// .fetchMediaType=${this.view?.is('live') ? this.overriddenConfig.live.controls.thumbnails.media_type : undefined}
|
||||
// .fetchEventsMediaType=${this.view?.is('live') ? this.overriddenConfig.live.controls.thumbnails.events_media_type : undefined}
|
||||
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -42,7 +42,6 @@ import { arrayify } from '../utils/basic';
|
||||
* @param keys The key to the property to set.
|
||||
* @param value The value to set.
|
||||
*/
|
||||
|
||||
export const setConfigValue = (
|
||||
obj: RawFrigateCardConfig,
|
||||
keys: string | (string | number)[],
|
||||
@@ -689,4 +688,5 @@ const UPGRADES = [
|
||||
// Delete the value if it's set to the default.
|
||||
transform: (val) => (val === 'low' ? ['low-performance'] : null),
|
||||
}),
|
||||
upgradeArrayOfObjects(CONF_OVERRIDES, upgradeMoveTo('overrides', 'merge')),
|
||||
];
|
||||
|
||||
+38
-51
@@ -1008,8 +1008,24 @@ const livethumbnailsControlSchema = thumbnailsControlSchema.extend({
|
||||
),
|
||||
});
|
||||
|
||||
const liveOverridableConfigSchema = z
|
||||
const liveConfigSchema = z
|
||||
.object({
|
||||
auto_pause: z
|
||||
.enum(MEDIA_ACTION_NEGATIVE_CONDITIONS)
|
||||
.array()
|
||||
.default(liveConfigDefault.auto_pause),
|
||||
auto_play: z
|
||||
.enum(MEDIA_ACTION_POSITIVE_CONDITIONS)
|
||||
.array()
|
||||
.default(liveConfigDefault.auto_play),
|
||||
auto_mute: z
|
||||
.enum(MEDIA_MUTE_CONDITIONS)
|
||||
.array()
|
||||
.default(liveConfigDefault.auto_mute),
|
||||
auto_unmute: z
|
||||
.enum(MEDIA_UNMUTE_CONDITIONS)
|
||||
.array()
|
||||
.default(liveConfigDefault.auto_unmute),
|
||||
controls: z
|
||||
.object({
|
||||
builtin: z.boolean().default(liveConfigDefault.controls.builtin),
|
||||
@@ -1032,55 +1048,36 @@ const liveOverridableConfigSchema = z
|
||||
title: titleControlConfigSchema.optional(),
|
||||
})
|
||||
.default(liveConfigDefault.controls),
|
||||
show_image_during_load: z
|
||||
.boolean()
|
||||
.default(liveConfigDefault.show_image_during_load),
|
||||
microphone: microphoneConfigSchema.default(liveConfigDefault.microphone),
|
||||
zoomable: z.boolean().default(liveConfigDefault.zoomable),
|
||||
display: viewDisplaySchema,
|
||||
})
|
||||
.merge(actionsSchema);
|
||||
|
||||
const liveConfigSchema = liveOverridableConfigSchema
|
||||
.extend({
|
||||
auto_play: z
|
||||
.enum(MEDIA_ACTION_POSITIVE_CONDITIONS)
|
||||
.array()
|
||||
.default(liveConfigDefault.auto_play),
|
||||
auto_pause: z
|
||||
.enum(MEDIA_ACTION_NEGATIVE_CONDITIONS)
|
||||
.array()
|
||||
.default(liveConfigDefault.auto_pause),
|
||||
auto_mute: z
|
||||
.enum(MEDIA_MUTE_CONDITIONS)
|
||||
.array()
|
||||
.default(liveConfigDefault.auto_mute),
|
||||
auto_unmute: z
|
||||
.enum(MEDIA_UNMUTE_CONDITIONS)
|
||||
.array()
|
||||
.default(liveConfigDefault.auto_unmute),
|
||||
preload: z.boolean().default(liveConfigDefault.preload),
|
||||
draggable: z.boolean().default(liveConfigDefault.draggable),
|
||||
lazy_load: z.boolean().default(liveConfigDefault.lazy_load),
|
||||
lazy_unload: z
|
||||
.enum(MEDIA_ACTION_NEGATIVE_CONDITIONS)
|
||||
.array()
|
||||
.default(liveConfigDefault.lazy_unload),
|
||||
draggable: z.boolean().default(liveConfigDefault.draggable),
|
||||
microphone: microphoneConfigSchema.default(liveConfigDefault.microphone),
|
||||
preload: z.boolean().default(liveConfigDefault.preload),
|
||||
show_image_during_load: z
|
||||
.boolean()
|
||||
.default(liveConfigDefault.show_image_during_load),
|
||||
transition_effect: transitionEffectConfigSchema.default(
|
||||
liveConfigDefault.transition_effect,
|
||||
),
|
||||
zoomable: z.boolean().default(liveConfigDefault.zoomable),
|
||||
})
|
||||
.merge(actionsSchema)
|
||||
.default(liveConfigDefault);
|
||||
export type LiveConfig = z.infer<typeof liveConfigSchema>;
|
||||
|
||||
const liveOverridesSchema = z
|
||||
.object({
|
||||
conditions: frigateCardConditionSchema.array(),
|
||||
overrides: liveOverridableConfigSchema,
|
||||
})
|
||||
.array()
|
||||
.optional();
|
||||
export type LiveOverrides = z.infer<typeof liveOverridesSchema>;
|
||||
// This schema is used when the live config needs to be overridden (see
|
||||
// `live.ts`). Overrides will always be "relative" to the config root, so this
|
||||
// schema maintains that 'depth' from the root but without the other
|
||||
// requirements that frigateCardConfigSchema has. Without this, overrides
|
||||
// calculated in `live.ts` would fail since cameras/type are not provided (as
|
||||
// these are mandatory parameters in the full config).
|
||||
export const liveConfigAbsoluteRootSchema = z.object({
|
||||
live: liveConfigSchema,
|
||||
});
|
||||
|
||||
// *************************************************************************
|
||||
// Cast Configuration
|
||||
@@ -1580,26 +1577,16 @@ export const dimensionsConfigSchema = z
|
||||
// Override Configuration
|
||||
// *************************************************************************
|
||||
|
||||
// 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(),
|
||||
view: deepRemoveDefaults(viewConfigSchema).optional(),
|
||||
dimensions: deepRemoveDefaults(dimensionsConfigSchema).optional(),
|
||||
});
|
||||
export type OverrideConfigurationKey = keyof z.infer<typeof overrideConfigurationSchema>;
|
||||
|
||||
const overridesSchema = z
|
||||
.object({
|
||||
conditions: frigateCardConditionSchema.array(),
|
||||
overrides: overrideConfigurationSchema,
|
||||
merge: z.object({}).passthrough().optional(),
|
||||
set: z.object({}).passthrough().optional(),
|
||||
delete: z.string().array().optional(),
|
||||
})
|
||||
.array()
|
||||
.optional();
|
||||
export type Overrides = z.infer<typeof overridesSchema>;
|
||||
|
||||
// *************************************************************************
|
||||
// Automation Configuration
|
||||
|
||||
+20
-2
@@ -243,8 +243,8 @@ export const getChildrenFromElement = (parent: HTMLElement): HTMLElement[] => {
|
||||
return children.filter(isHTMLElement);
|
||||
};
|
||||
|
||||
export const recursivelyMergeObjectsNotArrays = <T>(src1: T, src2: T): T => {
|
||||
return mergeWith({}, src1, src2, (_a, b) => (Array.isArray(b) ? b : undefined));
|
||||
export const recursivelyMergeObjectsNotArrays = <T>(target: T, src1: T, src2: T): T => {
|
||||
return mergeWith(target, src1, src2, (_a, b) => (Array.isArray(b) ? b : undefined));
|
||||
};
|
||||
|
||||
export const aspectRatioToString = (options?: {
|
||||
@@ -268,3 +268,21 @@ export const aspectRatioToStyle = (options?: {
|
||||
'aspect-ratio': aspectRatioToString(options),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove empty slots from nested arrays.
|
||||
*/
|
||||
export const desparsifyArrays = <T>(data: T): T => {
|
||||
if (Array.isArray(data)) {
|
||||
return <T>(
|
||||
data.filter((item) => item !== undefined).map((item) => desparsifyArrays(item))
|
||||
);
|
||||
} else if (typeof data === 'object' && data !== null) {
|
||||
const result: Record<string | number | symbol, unknown> = {};
|
||||
for (const key in data) {
|
||||
result[key] = desparsifyArrays(data[key]);
|
||||
}
|
||||
return <T>result;
|
||||
}
|
||||
return data;
|
||||
};
|
||||
|
||||
+4
-1
@@ -28,7 +28,10 @@ export function deepRemoveDefaults<T extends z.ZodTypeAny>(schema: T): any {
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodArray) {
|
||||
return z.ZodArray.create(deepRemoveDefaults(schema.element));
|
||||
return z.ZodArray.create(deepRemoveDefaults(schema.element))
|
||||
.min(schema._def.minLength?.value, schema._def.minLength?.message)
|
||||
.max(schema._def.maxLength?.value, schema._def.maxLength?.message)
|
||||
.length(schema._def.exactLength?.value, schema._def.exactLength?.message);
|
||||
}
|
||||
|
||||
if (schema instanceof z.ZodOptional) {
|
||||
|
||||
@@ -457,11 +457,11 @@ describe('CameraManager', async () => {
|
||||
},
|
||||
);
|
||||
|
||||
it('without cameras', async () => {
|
||||
it('without camera', async () => {
|
||||
const api = createCardAPI();
|
||||
vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS());
|
||||
|
||||
const manager = createCameraManager(api, mock<CameraManagerEngine>(), []);
|
||||
const manager = createCameraManager(api, mock<CameraManagerEngine>());
|
||||
|
||||
expect(manager.generateDefaultEventQueries('not_a_camera')).toBeNull();
|
||||
});
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
ConditionsEvaluateRequestEvent,
|
||||
ConditionsManager,
|
||||
evaluateConditionViaEvent,
|
||||
getOverriddenConfig,
|
||||
getOverridesByKey,
|
||||
} from '../../src/card-controller/conditions-manager';
|
||||
import { FrigateCardCondition } from '../../src/config/types';
|
||||
import {
|
||||
@@ -77,77 +77,350 @@ describe('getOverriddenConfig', () => {
|
||||
style: 'none',
|
||||
},
|
||||
};
|
||||
const overrides = [
|
||||
{
|
||||
overrides: {
|
||||
menu: {
|
||||
style: 'above',
|
||||
},
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
it('should not override config', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
expect(getOverriddenConfig(manager, config, overrides)).toBe(config);
|
||||
});
|
||||
|
||||
it('should override config', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.setState({ fullscreen: true });
|
||||
|
||||
expect(getOverriddenConfig(manager, config, overrides)).toEqual({
|
||||
menu: {
|
||||
style: 'above',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should do nothing without overrides', () => {
|
||||
it('should not override without overrides', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.setState({ fullscreen: true });
|
||||
|
||||
expect(getOverriddenConfig(manager, config)).toBe(config);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOverridesByKey', () => {
|
||||
const conditions = [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
];
|
||||
const override = {
|
||||
menu: {
|
||||
style: 'above',
|
||||
},
|
||||
};
|
||||
const overrides = [
|
||||
{
|
||||
overrides: override,
|
||||
conditions: conditions,
|
||||
},
|
||||
];
|
||||
|
||||
it('should get overrides', () => {
|
||||
expect(getOverridesByKey('menu', overrides)).toEqual([
|
||||
{ conditions: conditions, overrides: { style: 'above' } },
|
||||
]);
|
||||
it('should not override when condition does not match', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
expect(
|
||||
getOverriddenConfig(manager, config, {
|
||||
configOverrides: [
|
||||
{
|
||||
merge: {
|
||||
menu: {
|
||||
style: 'hidden',
|
||||
},
|
||||
},
|
||||
delete: ['menu.style'],
|
||||
set: {
|
||||
'menu.style': 'overlay',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toBe(config);
|
||||
});
|
||||
|
||||
it('should get no overrides', () => {
|
||||
expect(getOverridesByKey('live', overrides)).toEqual([]);
|
||||
describe('should merge', () => {
|
||||
it('with path', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.setState({ fullscreen: true });
|
||||
|
||||
expect(
|
||||
getOverriddenConfig(manager, config, {
|
||||
configOverrides: [
|
||||
{
|
||||
merge: {
|
||||
'live.controls.thumbnails': {
|
||||
mode: 'none',
|
||||
},
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
menu: {
|
||||
style: 'none',
|
||||
},
|
||||
live: {
|
||||
controls: {
|
||||
thumbnails: {
|
||||
mode: 'none',
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('without path', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.setState({ fullscreen: true });
|
||||
|
||||
expect(
|
||||
getOverriddenConfig(manager, config, {
|
||||
configOverrides: [
|
||||
{
|
||||
merge: {
|
||||
menu: {
|
||||
style: 'hidden',
|
||||
},
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
menu: {
|
||||
style: 'hidden',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('with invalid merge', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.setState({ fullscreen: true });
|
||||
|
||||
expect(
|
||||
getOverriddenConfig(manager, config, {
|
||||
configOverrides: [
|
||||
{
|
||||
merge: 6 as unknown as Record<string, unknown>,
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
menu: {
|
||||
style: 'none',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should get no overrides when undefined', () => {
|
||||
expect(getOverridesByKey('live')).toEqual([]);
|
||||
describe('should set', () => {
|
||||
it('leaf node', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.setState({ fullscreen: true });
|
||||
|
||||
expect(
|
||||
getOverriddenConfig(manager, config, {
|
||||
configOverrides: [
|
||||
{
|
||||
set: {
|
||||
'menu.style': 'hidden',
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
menu: {
|
||||
style: 'hidden',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('root node', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.setState({ fullscreen: true });
|
||||
|
||||
expect(
|
||||
getOverriddenConfig(manager, config, {
|
||||
configOverrides: [
|
||||
{
|
||||
set: {
|
||||
menu: {
|
||||
style: 'hidden',
|
||||
},
|
||||
},
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
menu: {
|
||||
style: 'hidden',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('should delete', () => {
|
||||
it('leaf node', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.setState({ fullscreen: true });
|
||||
|
||||
expect(
|
||||
getOverriddenConfig(manager, config, {
|
||||
configOverrides: [
|
||||
{
|
||||
delete: ['menu.style' as const],
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({
|
||||
menu: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('root node', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.setState({ fullscreen: true });
|
||||
|
||||
expect(
|
||||
getOverriddenConfig(manager, config, {
|
||||
configOverrides: [
|
||||
{
|
||||
delete: ['menu' as const],
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual({});
|
||||
});
|
||||
|
||||
// it('with empty value and object', () => {
|
||||
// const manager = new ConditionsManager(createCardAPI());
|
||||
// manager.setState({ fullscreen: true });
|
||||
|
||||
// expect(
|
||||
// getOverriddenConfig(manager, config, {
|
||||
// configOverrides: [
|
||||
// {
|
||||
// delete: [''],
|
||||
// conditions: [
|
||||
// {
|
||||
// condition: 'fullscreen' as const,
|
||||
// fullscreen: true,
|
||||
// },
|
||||
// ],
|
||||
// },
|
||||
// ],
|
||||
// emptyKeyReplaces: true,
|
||||
// }),
|
||||
// ).toEqual({});
|
||||
// });
|
||||
});
|
||||
|
||||
describe('should validate schema', () => {
|
||||
const testSchema = z.object({
|
||||
menu: z.object({
|
||||
style: z.enum(['none', 'hidden']),
|
||||
}),
|
||||
});
|
||||
|
||||
it('passing', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.setState({ fullscreen: true });
|
||||
|
||||
expect(
|
||||
getOverriddenConfig(manager, config, {
|
||||
configOverrides: [
|
||||
{
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
],
|
||||
set: {
|
||||
'menu.style': 'hidden',
|
||||
},
|
||||
},
|
||||
],
|
||||
schema: testSchema,
|
||||
}),
|
||||
).toEqual({
|
||||
menu: {
|
||||
style: 'hidden',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('failing', () => {
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.setState({ fullscreen: true });
|
||||
|
||||
expect(
|
||||
getOverriddenConfig(manager, config, {
|
||||
configOverrides: [
|
||||
{
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
],
|
||||
set: {
|
||||
'menu.style': 'NOT_A_STYLE',
|
||||
},
|
||||
},
|
||||
],
|
||||
schema: testSchema,
|
||||
}),
|
||||
).toEqual(config);
|
||||
});
|
||||
|
||||
it('failing and logging', () => {
|
||||
const consoleSpy = vi.spyOn(global.console, 'warn').mockReturnValue(undefined);
|
||||
|
||||
const manager = new ConditionsManager(createCardAPI());
|
||||
manager.setState({ fullscreen: true });
|
||||
|
||||
expect(
|
||||
getOverriddenConfig(manager, config, {
|
||||
configOverrides: [
|
||||
{
|
||||
conditions: [
|
||||
{
|
||||
condition: 'fullscreen' as const,
|
||||
fullscreen: true,
|
||||
},
|
||||
],
|
||||
set: {
|
||||
'menu.style': 'NOT_A_STYLE',
|
||||
},
|
||||
},
|
||||
],
|
||||
schema: testSchema,
|
||||
logOnParseError: true,
|
||||
}),
|
||||
).toEqual(config);
|
||||
|
||||
expect(consoleSpy).toBeCalledWith(
|
||||
'Cannot parse overridden configuration',
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -194,7 +467,7 @@ describe('ConditionsManager', () => {
|
||||
return createConfig({
|
||||
overrides: [
|
||||
{
|
||||
overrides: {},
|
||||
merge: {},
|
||||
conditions: conditions,
|
||||
},
|
||||
],
|
||||
@@ -455,7 +728,7 @@ describe('ConditionsManager', () => {
|
||||
describe('with screen condition', () => {
|
||||
const mediaQueryConfig = {
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [],
|
||||
cameras: [{}],
|
||||
elements: [
|
||||
{
|
||||
type: 'custom:frigate-card-conditional',
|
||||
|
||||
@@ -420,7 +420,7 @@ describe('should handle version specific upgrades', () => {
|
||||
media_loaded: true,
|
||||
},
|
||||
],
|
||||
overrides: {
|
||||
merge: {
|
||||
view: {
|
||||
default: 'clips',
|
||||
},
|
||||
@@ -489,7 +489,7 @@ describe('should handle version specific upgrades', () => {
|
||||
overrides: [
|
||||
{
|
||||
conditions: {},
|
||||
overrides: {
|
||||
merge: {
|
||||
menu: {
|
||||
buttons: {
|
||||
camera_ui: {
|
||||
@@ -1872,7 +1872,7 @@ describe('should handle version specific upgrades', () => {
|
||||
views: ['clips', 'snapshots'],
|
||||
},
|
||||
],
|
||||
overrides: {
|
||||
merge: {
|
||||
view: {
|
||||
default: 'clips',
|
||||
},
|
||||
@@ -1987,7 +1987,7 @@ describe('should handle version specific upgrades', () => {
|
||||
cameras: ['camera_1', 'camera_2'],
|
||||
},
|
||||
],
|
||||
overrides: {
|
||||
merge: {
|
||||
view: {
|
||||
default: 'clips',
|
||||
},
|
||||
@@ -2107,7 +2107,7 @@ describe('should handle version specific upgrades', () => {
|
||||
[condition]: true,
|
||||
},
|
||||
],
|
||||
overrides: {
|
||||
merge: {
|
||||
view: {
|
||||
default: 'clips',
|
||||
},
|
||||
@@ -2255,7 +2255,7 @@ describe('should handle version specific upgrades', () => {
|
||||
state_not: 'off',
|
||||
},
|
||||
],
|
||||
overrides: {
|
||||
merge: {
|
||||
view: {
|
||||
default: 'clips',
|
||||
},
|
||||
@@ -2387,7 +2387,7 @@ describe('should handle version specific upgrades', () => {
|
||||
media_query: 'query',
|
||||
},
|
||||
],
|
||||
overrides: {
|
||||
merge: {
|
||||
view: {
|
||||
default: 'clips',
|
||||
},
|
||||
@@ -2497,5 +2497,48 @@ describe('should handle version specific upgrades', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('from overrides to merge', () => {
|
||||
const config = {
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [],
|
||||
overrides: [
|
||||
{
|
||||
conditions: [
|
||||
{
|
||||
condition: 'view',
|
||||
view: ['clips'],
|
||||
},
|
||||
],
|
||||
overrides: {
|
||||
menu: {
|
||||
style: 'hidden',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
expect(upgradeConfig(config)).toBeTruthy();
|
||||
expect(config).toEqual({
|
||||
type: 'custom:frigate-card',
|
||||
cameras: [],
|
||||
overrides: [
|
||||
{
|
||||
conditions: [
|
||||
{
|
||||
condition: 'view',
|
||||
view: ['clips'],
|
||||
},
|
||||
],
|
||||
merge: {
|
||||
menu: {
|
||||
style: 'hidden',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ describe('setProfiles', () => {
|
||||
it('should handle profiles', () => {
|
||||
const input = {
|
||||
type: 'frigate-hass-card',
|
||||
cameras: [],
|
||||
cameras: [{}],
|
||||
live: {
|
||||
controls: {
|
||||
timeline: {
|
||||
|
||||
@@ -12,7 +12,7 @@ import { createConfig } from '../test-utils';
|
||||
describe('config defaults', () => {
|
||||
it('should be as expected', () => {
|
||||
expect(createConfig()).toEqual({
|
||||
cameras: [],
|
||||
cameras: [{}],
|
||||
cameras_global: {
|
||||
dependencies: {
|
||||
all_cameras: false,
|
||||
@@ -453,7 +453,7 @@ describe('should handle custom frigate elements', () => {
|
||||
it('should not require title controls to specify all options', () => {
|
||||
expect(
|
||||
createConfig({
|
||||
cameras: [],
|
||||
cameras: [{}],
|
||||
live: {
|
||||
controls: {
|
||||
title: {
|
||||
|
||||
+1
-1
@@ -67,7 +67,7 @@ export const createCondition = (
|
||||
export const createConfig = (config?: RawFrigateCardConfig): FrigateCardConfig => {
|
||||
return frigateCardConfigSchema.parse({
|
||||
type: 'frigate-hass-card',
|
||||
cameras: [],
|
||||
cameras: [{}],
|
||||
...config,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
aspectRatioToStyle,
|
||||
contentsChanged,
|
||||
dayToDate,
|
||||
desparsifyArrays,
|
||||
dispatchFrigateCardEvent,
|
||||
errorToConsole,
|
||||
formatDate,
|
||||
@@ -158,6 +159,7 @@ describe('runWhenIdleIfSupported', () => {
|
||||
});
|
||||
|
||||
it('should run directly when not supported', () => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(window as any).requestIdleCallback = undefined;
|
||||
const func = vi.fn();
|
||||
runWhenIdleIfSupported(func);
|
||||
@@ -166,7 +168,7 @@ describe('runWhenIdleIfSupported', () => {
|
||||
|
||||
it('should run idle when supported', () => {
|
||||
const requestIdle = vi.fn();
|
||||
(window as any).requestIdleCallback = requestIdle;
|
||||
window.requestIdleCallback = requestIdle;
|
||||
const func = vi.fn();
|
||||
runWhenIdleIfSupported(func);
|
||||
expect(requestIdle).toBeCalledWith(func, {});
|
||||
@@ -174,7 +176,7 @@ describe('runWhenIdleIfSupported', () => {
|
||||
|
||||
it('should run idle with timeout when supported', () => {
|
||||
const requestIdle = vi.fn();
|
||||
(window as any).requestIdleCallback = requestIdle;
|
||||
window.requestIdleCallback = requestIdle;
|
||||
const func = vi.fn();
|
||||
runWhenIdleIfSupported(func, 10);
|
||||
expect(requestIdle).toBeCalledWith(func, { timeout: 10 });
|
||||
@@ -298,6 +300,7 @@ describe('recursivelyMergeObjectsNotArrays', () => {
|
||||
it('should recursively merge objects but replace arrays', () => {
|
||||
expect(
|
||||
recursivelyMergeObjectsNotArrays(
|
||||
{},
|
||||
{
|
||||
a: {
|
||||
b: {
|
||||
@@ -356,3 +359,50 @@ describe('aspectRatioToStyle', () => {
|
||||
expect(aspectRatioToStyle({ ratio: [4] })).toEqual({ 'aspect-ratio': 'auto' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('desparsifyArrays', () => {
|
||||
it('number', () => {
|
||||
expect(desparsifyArrays(1)).toBe(1);
|
||||
});
|
||||
it('string', () => {
|
||||
expect(desparsifyArrays('foo')).toBe('foo');
|
||||
});
|
||||
describe('array', () => {
|
||||
it('simple', () => {
|
||||
expect(desparsifyArrays([1, 2, undefined, 3])).toEqual([1, 2, 3]);
|
||||
});
|
||||
it('nested', () => {
|
||||
expect(
|
||||
desparsifyArrays([
|
||||
1,
|
||||
2,
|
||||
undefined,
|
||||
{
|
||||
subArray: [undefined, 3],
|
||||
},
|
||||
4,
|
||||
]),
|
||||
).toEqual([1, 2, { subArray: [3] }, 4]);
|
||||
});
|
||||
});
|
||||
describe('object', () => {
|
||||
it('simple', () => {
|
||||
expect(
|
||||
desparsifyArrays({ foo: [1, 2, undefined, 3], bar: [undefined, 4] }),
|
||||
).toEqual({
|
||||
foo: [1, 2, 3],
|
||||
bar: [4],
|
||||
});
|
||||
});
|
||||
it('nested', () => {
|
||||
expect(
|
||||
desparsifyArrays({ foo: { bar: [1, undefined, 2], empty: [undefined] } }),
|
||||
).toEqual({
|
||||
foo: {
|
||||
bar: [1, 2],
|
||||
empty: [],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -49,6 +49,23 @@ describe('deepRemoveDefaults', () => {
|
||||
const result = deepRemoveDefaults(schema).parse({ string: 'moo' });
|
||||
expect(result.string).toBe('moo');
|
||||
});
|
||||
describe('should still enforce array length', () => {
|
||||
it('min', () => {
|
||||
const schema = z.number().array().min(1);
|
||||
const result = deepRemoveDefaults(schema).safeParse([]);
|
||||
expect(result.success).toBeFalsy();
|
||||
});
|
||||
it('max', () => {
|
||||
const schema = z.number().array().max(1);
|
||||
const result = deepRemoveDefaults(schema).safeParse([1, 2]);
|
||||
expect(result.success).toBeFalsy();
|
||||
});
|
||||
it('exact', () => {
|
||||
const schema = z.number().array().length(1);
|
||||
const result = deepRemoveDefaults(schema).safeParse([]);
|
||||
expect(result.success).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getParseErrorKeys', () => {
|
||||
|
||||
@@ -800,6 +800,16 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@jridgewell/trace-mapping@npm:^0.3.23":
|
||||
version: 0.3.25
|
||||
resolution: "@jridgewell/trace-mapping@npm:0.3.25"
|
||||
dependencies:
|
||||
"@jridgewell/resolve-uri": "npm:^3.1.0"
|
||||
"@jridgewell/sourcemap-codec": "npm:^1.4.14"
|
||||
checksum: 10c0/3d1ce6ebc69df9682a5a8896b414c6537e428a1d68b02fcc8363b04284a8ca0df04d0ee3013132252ab14f2527bc13bea6526a912ecb5658f0e39fd2860b4df4
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@lit-labs/scoped-registry-mixin@npm:^1.0.3":
|
||||
version: 1.0.3
|
||||
resolution: "@lit-labs/scoped-registry-mixin@npm:1.0.3"
|
||||
@@ -1467,76 +1477,76 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/coverage-istanbul@npm:^1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "@vitest/coverage-istanbul@npm:1.3.1"
|
||||
"@vitest/coverage-istanbul@npm:^1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "@vitest/coverage-istanbul@npm:1.5.0"
|
||||
dependencies:
|
||||
debug: "npm:^4.3.4"
|
||||
istanbul-lib-coverage: "npm:^3.2.2"
|
||||
istanbul-lib-instrument: "npm:^6.0.1"
|
||||
istanbul-lib-report: "npm:^3.0.1"
|
||||
istanbul-lib-source-maps: "npm:^4.0.1"
|
||||
istanbul-lib-source-maps: "npm:^5.0.4"
|
||||
istanbul-reports: "npm:^3.1.6"
|
||||
magicast: "npm:^0.3.3"
|
||||
picocolors: "npm:^1.0.0"
|
||||
test-exclude: "npm:^6.0.0"
|
||||
peerDependencies:
|
||||
vitest: 1.3.1
|
||||
checksum: 10c0/40efacde51e5998bde8032dd45d64fe214674f1efb97c094c4ae9ef1893afc4e0ef97f3fa4cc4923561b67b1196c27a877a0fa5bc71a049b8e48e3ef1f5c48db
|
||||
vitest: 1.5.0
|
||||
checksum: 10c0/12abbaf8925c797e1fafef05c8b7d554c9fc61d13eb70c3f8532ade72db305a3cb1fec9bb7642763de27471e3f3da3d684d6533aa954756c5edf2707d2fc25cd
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/expect@npm:1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "@vitest/expect@npm:1.3.1"
|
||||
"@vitest/expect@npm:1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "@vitest/expect@npm:1.5.0"
|
||||
dependencies:
|
||||
"@vitest/spy": "npm:1.3.1"
|
||||
"@vitest/utils": "npm:1.3.1"
|
||||
"@vitest/spy": "npm:1.5.0"
|
||||
"@vitest/utils": "npm:1.5.0"
|
||||
chai: "npm:^4.3.10"
|
||||
checksum: 10c0/ea66a1e912d896a481a27631b68089b885af7e8ed62ba8aaa119c37a9beafe6c094fd672775a20e6e23460af66e294f9ca259e6e0562708d1b7724eaaf53c7bb
|
||||
checksum: 10c0/12138caf0831a9bcdf475750fdff27588f03f11ee5101124f9d39951bbcdec39aa6a19a1a30f3ab0ca17374ddfc7399b861e2fcb1548bba302943e8e88512a9c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/runner@npm:1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "@vitest/runner@npm:1.3.1"
|
||||
"@vitest/runner@npm:1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "@vitest/runner@npm:1.5.0"
|
||||
dependencies:
|
||||
"@vitest/utils": "npm:1.3.1"
|
||||
"@vitest/utils": "npm:1.5.0"
|
||||
p-limit: "npm:^5.0.0"
|
||||
pathe: "npm:^1.1.1"
|
||||
checksum: 10c0/d732de2368d2bc32cbc27f0bbc5477f6e36088ddfb873c036935a45b1b252ebc529b932cf5cd944eed9b692243acebef828f6d3218583cb8a6817a8270712050
|
||||
checksum: 10c0/a7b693e4121b6159a77f6cbffc03c228cd18c3734e27a968fe26579777f6d7a4d55a02fba396d564df996a91881ee76de80ee5a526d27a4b508f44a9af62d95a
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/snapshot@npm:1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "@vitest/snapshot@npm:1.3.1"
|
||||
"@vitest/snapshot@npm:1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "@vitest/snapshot@npm:1.5.0"
|
||||
dependencies:
|
||||
magic-string: "npm:^0.30.5"
|
||||
pathe: "npm:^1.1.1"
|
||||
pretty-format: "npm:^29.7.0"
|
||||
checksum: 10c0/cad0844270852c6d53c1ca6b7ca279034880d2140837ff245d5bd2376f4356cc924929c58dc69bcf9fad83ba934d4a06000c908971cc24b5d7a9ec2656b72d29
|
||||
checksum: 10c0/d897070b3a7c008eb58d44ca0dc324c69dfc1f787335e59fb930ae2e8d02a148e6b3c13497a588083a0654bdc430281fdd84276ead4888230314a2984dd9f93d
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/spy@npm:1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "@vitest/spy@npm:1.3.1"
|
||||
"@vitest/spy@npm:1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "@vitest/spy@npm:1.5.0"
|
||||
dependencies:
|
||||
tinyspy: "npm:^2.2.0"
|
||||
checksum: 10c0/efc42f679d2a51fc6583ca3136ccd47581cb27c923ed3cb0500f5dee9aac99b681bfdd400c16ef108f2e0761daa642bc190816a6411931a2aba99ebf8b213dd4
|
||||
checksum: 10c0/0206f1e8431f543474dc6d252b553227d0f286b27226b987b63babb18865e8320e6c3d822c67782aae68555728a57ecdeb7c4e16283dbad49791608a691f26d1
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@vitest/utils@npm:1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "@vitest/utils@npm:1.3.1"
|
||||
"@vitest/utils@npm:1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "@vitest/utils@npm:1.5.0"
|
||||
dependencies:
|
||||
diff-sequences: "npm:^29.6.3"
|
||||
estree-walker: "npm:^3.0.3"
|
||||
loupe: "npm:^2.3.7"
|
||||
pretty-format: "npm:^29.7.0"
|
||||
checksum: 10c0/d604c8ad3b1aee30d4dcd889098f591407bfe18547ff96485b1d1ed54eff58219c756a9544a7fbd4e37886863abacd7a89a76334cb3ea7f84c3d496bb757db23
|
||||
checksum: 10c0/b9d779ea5c1a5759df4f59e3ba3f3a94816d6c600afe7a2d14b963ea114ce1acedbfe678cbfacb0a20d33cabcce890a08b2ce3fd52e3465f2e0969cc39f7686b
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -3855,7 +3865,7 @@ __metadata:
|
||||
"@types/masonry-layout": "npm:^4.2.5"
|
||||
"@typescript-eslint/eslint-plugin": "npm:^5.36.2"
|
||||
"@typescript-eslint/parser": "npm:^5.36.2"
|
||||
"@vitest/coverage-istanbul": "npm:^1.3.1"
|
||||
"@vitest/coverage-istanbul": "npm:^1.5.0"
|
||||
component-emitter: "npm:^1.3.0"
|
||||
crypto: "npm:^1.0.1"
|
||||
date-fns: "npm:^2.29.2"
|
||||
@@ -3895,7 +3905,7 @@ __metadata:
|
||||
vis-data: "npm:^7.1.9"
|
||||
vis-timeline: "npm:^7.7.3"
|
||||
vis-util: "npm:^5.0.2"
|
||||
vitest: "npm:^1.3.1"
|
||||
vitest: "npm:^1.5.0"
|
||||
vitest-mock-extended: "npm:^1.3.1"
|
||||
web-dialog: "npm:^0.0.11"
|
||||
xss: "npm:^1.0.14"
|
||||
@@ -4938,14 +4948,14 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"istanbul-lib-source-maps@npm:^4.0.1":
|
||||
version: 4.0.1
|
||||
resolution: "istanbul-lib-source-maps@npm:4.0.1"
|
||||
"istanbul-lib-source-maps@npm:^5.0.4":
|
||||
version: 5.0.4
|
||||
resolution: "istanbul-lib-source-maps@npm:5.0.4"
|
||||
dependencies:
|
||||
"@jridgewell/trace-mapping": "npm:^0.3.23"
|
||||
debug: "npm:^4.1.1"
|
||||
istanbul-lib-coverage: "npm:^3.0.0"
|
||||
source-map: "npm:^0.6.1"
|
||||
checksum: 10c0/19e4cc405016f2c906dff271a76715b3e881fa9faeb3f09a86cb99b8512b3a5ed19cadfe0b54c17ca0e54c1142c9c6de9330d65506e35873994e06634eebeb66
|
||||
checksum: 10c0/48b48294590675005ba439888a53157fc71a99d78321428f3ce5f64e28cdfb6bc6eb45871333f448437118ef56a0ef371f4958163e2c2d066d3a703415a71b2e
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8011,10 +8021,10 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tinypool@npm:^0.8.2":
|
||||
version: 0.8.2
|
||||
resolution: "tinypool@npm:0.8.2"
|
||||
checksum: 10c0/8998626614172fc37c394e9a14e701dc437727fc6525488a4d4fd42044a4b2b59d6f076d750cbf5c699f79c58dd4e40599ab09e2f1ae0df4b23516b98c9c3055
|
||||
"tinypool@npm:^0.8.3":
|
||||
version: 0.8.3
|
||||
resolution: "tinypool@npm:0.8.3"
|
||||
checksum: 10c0/c219d0cfb69de8e3cf17403034a508d773f2fccaad79a13cdbad68600c4fb10186ad814d2320bcaa8f6e774fff5666d2a3d3b241dc8a7ad9d970ee63fe620a32
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8487,9 +8497,9 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vite-node@npm:1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "vite-node@npm:1.3.1"
|
||||
"vite-node@npm:1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "vite-node@npm:1.5.0"
|
||||
dependencies:
|
||||
cac: "npm:^6.7.14"
|
||||
debug: "npm:^4.3.4"
|
||||
@@ -8498,7 +8508,7 @@ __metadata:
|
||||
vite: "npm:^5.0.0"
|
||||
bin:
|
||||
vite-node: vite-node.mjs
|
||||
checksum: 10c0/b50665ef224f3527f856ab88a0cfabab36dd6e2dd1e3edca8f8f25d5d33754e1050495472c2c82147d0dcf7c5280971dae2f37a531c10f3941d8d3344e34ce0b
|
||||
checksum: 10c0/85f9e8616f3612c71193a2bbdfb00f610685108a65eaa7ac535655ca1bd65dda5436ee577465436f715966f34e4b25b54140bbd584fe2c95ea0726acf4ecd351
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
@@ -8554,15 +8564,15 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"vitest@npm:^1.3.1":
|
||||
version: 1.3.1
|
||||
resolution: "vitest@npm:1.3.1"
|
||||
"vitest@npm:^1.5.0":
|
||||
version: 1.5.0
|
||||
resolution: "vitest@npm:1.5.0"
|
||||
dependencies:
|
||||
"@vitest/expect": "npm:1.3.1"
|
||||
"@vitest/runner": "npm:1.3.1"
|
||||
"@vitest/snapshot": "npm:1.3.1"
|
||||
"@vitest/spy": "npm:1.3.1"
|
||||
"@vitest/utils": "npm:1.3.1"
|
||||
"@vitest/expect": "npm:1.5.0"
|
||||
"@vitest/runner": "npm:1.5.0"
|
||||
"@vitest/snapshot": "npm:1.5.0"
|
||||
"@vitest/spy": "npm:1.5.0"
|
||||
"@vitest/utils": "npm:1.5.0"
|
||||
acorn-walk: "npm:^8.3.2"
|
||||
chai: "npm:^4.3.10"
|
||||
debug: "npm:^4.3.4"
|
||||
@@ -8574,15 +8584,15 @@ __metadata:
|
||||
std-env: "npm:^3.5.0"
|
||||
strip-literal: "npm:^2.0.0"
|
||||
tinybench: "npm:^2.5.1"
|
||||
tinypool: "npm:^0.8.2"
|
||||
tinypool: "npm:^0.8.3"
|
||||
vite: "npm:^5.0.0"
|
||||
vite-node: "npm:1.3.1"
|
||||
vite-node: "npm:1.5.0"
|
||||
why-is-node-running: "npm:^2.2.2"
|
||||
peerDependencies:
|
||||
"@edge-runtime/vm": "*"
|
||||
"@types/node": ^18.0.0 || >=20.0.0
|
||||
"@vitest/browser": 1.3.1
|
||||
"@vitest/ui": 1.3.1
|
||||
"@vitest/browser": 1.5.0
|
||||
"@vitest/ui": 1.5.0
|
||||
happy-dom: "*"
|
||||
jsdom: "*"
|
||||
peerDependenciesMeta:
|
||||
@@ -8600,7 +8610,7 @@ __metadata:
|
||||
optional: true
|
||||
bin:
|
||||
vitest: vitest.mjs
|
||||
checksum: 10c0/66d312a3dc12e67bba22d31332d939e89cd17d38531893c7b13b8826704564031c1dde795df2799b855660572c19a595301e920710c7775d072ee6332502efc5
|
||||
checksum: 10c0/87f6666cccd52678a0e1dbf32e7492e4003580a168114221e4c2d404d53097d886a8be4c5c379e347d0e749affa0ea5416e4e3a0356aeca228a77ee54891ca3c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
|
||||
Reference in New Issue
Block a user