Add support for more advanced forms of overriding
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user