refactor: Significantly refactor how conditions work internally (#1886)

- Much improved API cleanliness and testability to allow further
extensibility in future
 - Simplified code in `live` view

This technically contains a small change in how overrides work in the
`live` view. Since that change is _closer_ to the documentation, and
since this is likely to be rarely used, this is not considered a
breaking change. Previously, overrides for a given live camera would
always render _as if_ that camera was selected, vs was actually
selected. Now, overrides will only apply in the live view when the
camera is _actually_ selected. If this is an issue for you in practice,
lets discuss.
This commit is contained in:
Dermot Duffy
2025-02-11 20:07:55 -08:00
committed by GitHub
parent a92074bc8d
commit 8d3cf07b43
51 changed files with 2443 additions and 2118 deletions
+36 -19
View File
@@ -1,4 +1,5 @@
import isEqual from 'lodash-es/isEqual';
import { ConditionsManager } from '../../conditions/conditions-manager.js';
import { isConfigUpgradeable } from '../../config/management.js';
import { setProfiles } from '../../config/profiles/index.js';
import {
@@ -9,9 +10,9 @@ import {
} from '../../config/types.js';
import { localize } from '../../localize/localize.js';
import { getParseErrorPaths } from '../../utils/zod.js';
import { getOverriddenConfig } from '../conditions-manager.js';
import { InitializationAspect } from '../initialization-manager.js';
import { CardConfigAPI } from '../types.js';
import { getOverriddenConfig } from './get-overridden-config.js';
import { setAutomationsFromConfig } from './load-automations.js';
import { setKeyboardShortcutsFromConfig } from './load-keyboard-shortcuts.js';
@@ -26,6 +27,7 @@ export class ConfigManager {
protected _overriddenConfig: AdvancedCameraCardConfig | null = null;
protected _rawConfig: RawAdvancedCameraCardConfig | null = null;
protected _cardWideConfig: CardWideConfig | null = null;
protected _overridesConditionsManager: ConditionsManager | null = null;
constructor(api: CardConfigAPI) {
this._api = api;
@@ -85,8 +87,16 @@ export class ConfigManager {
debug: config.debug,
};
this._api.getConditionsManager().setConditionsFromConfig();
this._api.getConditionsManager().setState({
this._overridesConditionsManager?.destroy();
this._overridesConditionsManager = this._config.overrides?.length
? new ConditionsManager(
this._config.overrides.map((override) => override.conditions).flat(),
this._api.getConditionStateManager(),
)
: null;
this._overridesConditionsManager?.addListener(() => this._processOverrideConfig());
this._api.getConditionStateManager().setState({
view: undefined,
displayMode: undefined,
camera: undefined,
@@ -102,31 +112,22 @@ export class ConfigManager {
setKeyboardShortcutsFromConfig(this._api, this);
setAutomationsFromConfig(this._api);
this.computeOverrideConfig();
this._processOverrideConfig();
this._api.getCardElementManager().update();
}
public computeOverrideConfig(): void {
const conditionsManager = this._api.getConditionsManager();
protected _processOverrideConfig(): void {
/* istanbul ignore if: No (current) way to reach this code -- @preserve */
if (!this._config) {
return;
}
let overriddenConfig: AdvancedCameraCardConfig | null = null;
try {
overriddenConfig = getOverriddenConfig(conditionsManager, this._config, {
configOverrides: this._config.overrides,
schema: advancedCameraCardConfigSchema,
});
} catch (ev) {
this._api.getMessageManager().setErrorIfHigherPriority(ev);
return;
}
const overriddenConfig = this._getOverriddenConfig();
// Save on Lit re-rendering costs by only updating the configuration if it
// actually changes.
if (isEqual(overriddenConfig, this._overriddenConfig)) {
if (!overriddenConfig || isEqual(overriddenConfig, this._overriddenConfig)) {
return;
}
@@ -154,14 +155,30 @@ export class ConfigManager {
.uninitialize(InitializationAspect.MICROPHONE_CONNECT);
}
/* async */ this._initializeBackground(previousConfig);
/* async */ this._initializeBackgroundAndUpdate(previousConfig);
}
protected _getOverriddenConfig(): AdvancedCameraCardConfig | null {
if (!this._overridesConditionsManager || !this._config) {
return this._config;
}
try {
return getOverriddenConfig(this._overridesConditionsManager, this._config, {
configOverrides: this._config.overrides,
schema: advancedCameraCardConfigSchema,
});
} catch (ev) {
this._api.getMessageManager().setErrorIfHigherPriority(ev);
return null;
}
}
/**
* Initialize config dependent items in the background. For items that the
* card hard requires, use InitializationManager instead.
*/
protected async _initializeBackground(
protected async _initializeBackgroundAndUpdate(
previousConfig: AdvancedCameraCardConfig | null,
): Promise<void> {
await this._api.getDefaultManager().initializeIfNecessary(previousConfig);
@@ -0,0 +1,76 @@
import { merge } from 'lodash-es';
import { ZodType as ZodSchema } from 'zod';
import { ConditionsManagerReadonlyInterface } from '../../conditions/types';
import {
copyConfig,
deleteConfigValue,
getConfigValue,
setConfigValue,
} from '../../config/management';
import { Overrides, RawAdvancedCameraCardConfig } from '../../config/types';
import { localize } from '../../localize/localize';
import { AdvancedCameraCardError } from '../../types';
import { desparsifyArrays } from '../../utils/basic';
class OverrideConfigurationError extends AdvancedCameraCardError {}
export function getOverriddenConfig<RT extends RawAdvancedCameraCardConfig>(
manager: ConditionsManagerReadonlyInterface,
config: Readonly<RT>,
options?: {
configOverrides?: Readonly<Overrides>;
schema?: ZodSchema;
},
): RT {
if (!options?.configOverrides) {
return config;
}
let output = copyConfig(config);
let overridden = false;
for (const override of options.configOverrides) {
if (manager.getEvaluation()?.result) {
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;
}
}
if (!overridden) {
// 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 (!parseResult.success) {
throw new OverrideConfigurationError(
localize('error.invalid_configuration_override'),
[parseResult.error.errors, output],
);
}
return parseResult.data;
}
return output;
}
@@ -3,8 +3,8 @@ import {
PTZKeyboardShortcutName,
} from '../../config/keyboard-shortcuts';
import { PTZAction } from '../../config/ptz';
import { CardConfigLoaderAPI, TaggedAutomations } from '../types';
import { createPTZMultiAction } from '../../utils/action';
import { CardConfigLoaderAPI, TaggedAutomation } from '../types';
export const setKeyboardShortcutsFromConfig = (
api: CardConfigLoaderAPI,
@@ -47,12 +47,12 @@ const ptzKeyboardShortcutToPTZAction = (
const convertKeyboardShortcutsToAutomations = (
tag: unknown,
shortcuts: KeyboardShortcuts,
): TaggedAutomations => {
): TaggedAutomation[] => {
if (!shortcuts.enabled) {
return [];
}
const automations: TaggedAutomations = [];
const automations: TaggedAutomation[] = [];
for (const name of [
'ptz_down',