Files
advanced-camera-card/src/card-controller/config/config-manager.ts
T

235 lines
7.8 KiB
TypeScript

import { isEqual } from 'lodash-es';
import { copyConfig, isConfigUpgradeable } from '../../config/management.js';
import { setProfiles } from '../../config/profiles/set-profiles.js';
import {
advancedCameraCardConfigSchema,
type AdvancedCameraCardConfig,
type CardWideConfig,
} from '../../config/schema/types.js';
import type { RawAdvancedCameraCardConfig } from '../../config/types.js';
import { localize } from '../../localize/localize.js';
import { getParseError } from '../../utils/zod/parse-errors.js';
import { InitializationAspect } from '../initialization-manager.js';
import type { CardConfigAPI } from '../types.js';
import { ConfigParseError } from './error.js';
import { setAutomationsFromConfig } from './load-automations.js';
import { setRemoteControlEntityFromConfig } from './load-control-entities.js';
import { setFoldersFromConfig } from './load-folders.js';
import { setKeyboardShortcutsFromConfig } from './load-keyboard-shortcuts.js';
import { OverridesManager } from './overrides-manager.js';
export class ConfigManager {
private _api: CardConfigAPI;
// The main base configuration object. For most usecases use getConfig() to
// get the correct configuration (which will return overrides as appropriate).
// This variable must be called `_config` or `config` to be compatible with
// card-mod.
private _config: AdvancedCameraCardConfig | null = null;
private _overriddenConfig: AdvancedCameraCardConfig | null = null;
private _rawConfig: RawAdvancedCameraCardConfig | null = null;
private _cardWideConfig: CardWideConfig | null = null;
private _overridesManager = new OverridesManager(() => this._processOverrideConfig());
constructor(api: CardConfigAPI) {
this._api = api;
}
public hasConfig(): boolean {
return !!this.getConfig();
}
public getConfig(): AdvancedCameraCardConfig | null {
return this._overriddenConfig ?? this._config;
}
public getCardWideConfig(): CardWideConfig | null {
return this._cardWideConfig;
}
public getNonOverriddenConfig(): AdvancedCameraCardConfig | null {
return this._config;
}
public getRawConfig(): RawAdvancedCameraCardConfig | null {
return this._rawConfig;
}
public setConfig(inputConfig?: RawAdvancedCameraCardConfig): void {
if (!inputConfig) {
throw new ConfigParseError(localize('error.invalid_configuration'));
}
const parseResult = advancedCameraCardConfigSchema.safeParse(inputConfig);
if (!parseResult.success) {
const hint = getParseError(parseResult.error);
let upgradeMessage = '';
if (isConfigUpgradeable(inputConfig)) {
upgradeMessage = `${localize('error.upgrade_available')}. `;
}
throw new ConfigParseError(
upgradeMessage +
`${localize('error.invalid_configuration')}: ` +
(hint ?? localize('error.invalid_configuration_no_hint')),
);
}
// The config is cloned here because Zod 4 returns shared constant
// defaults by reference. Since setProfiles() mutates the configuration
// in-place, those mutations would "pollute" the global defaults and break
// test isolation if we didn't use a fresh clone here.
const config = setProfiles(
inputConfig,
copyConfig(parseResult.data),
parseResult.data.profiles,
);
this._rawConfig = inputConfig;
if (isEqual(this._config, config)) {
return;
}
this._config = config;
this._cardWideConfig = {
performance: config.performance,
debug: config.debug,
};
this._overridesManager.set(
this._api.getConditionStateManager(),
this._config.overrides,
);
this._api.getConditionStateManager().setState({
view: undefined,
displayMode: undefined,
camera: undefined,
});
this._api.getMediaLoadedInfoManager().clear();
this._api.getInitializationManager().uninitialize(InitializationAspect.VIEW);
this._api.getViewManager().reset();
this._api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
this._processOverrideConfig();
this._api.getCardElementManager().update();
}
private _processOverrideConfig(): void {
/* istanbul ignore if: No (current) way to reach this code -- @preserve */
if (!this._config) {
return;
}
let overriddenConfig: AdvancedCameraCardConfig;
try {
overriddenConfig = this._overridesManager.getConfig(this._config);
} catch (ev) {
this._api.getIssueManager().trigger('config_error', { error: ev });
return;
}
// Clear any prior config error on success. This method runs both from
// setConfig() and from condition-change-driven override recomputations;
// resetting here ensures a transient override error is cleared when
// conditions change to produce a valid config again.
this._api.getIssueManager().reset('config_error');
// Save on Lit re-rendering costs by only updating the configuration if it
// actually changes.
if (isEqual(overriddenConfig, this._overriddenConfig)) {
return;
}
const previousConfig = this._overriddenConfig;
this._overriddenConfig = overriddenConfig;
setFoldersFromConfig(this._api);
this._api.getStyleManager().updateFromConfig();
// Only re-run side-effect callbacks when their relevant config section
// changed. As an example of why: Automation loaders delete then recreate
// automations, which destroys associated ConditionsManagers. If a condition
// transition (e.g. microphone connect) triggers both a user automation and
// an unrelated override, an unconditional reload would delete the
// automation mid-transition -- the freshly created replacement has no prior
// state, treats the current condition as its baseline, and never fires the
// action.
const runIfChanged = <T>(
extract: (config: AdvancedCameraCardConfig) => T,
callback: () => void,
skipFirst?: boolean,
): void => {
if (!previousConfig) {
if (!skipFirst) {
callback();
}
return;
}
if (!isEqual(extract(previousConfig), extract(overriddenConfig))) {
callback();
}
};
runIfChanged(
(config) => config.view.keyboard_shortcuts,
() => setKeyboardShortcutsFromConfig(this._api),
);
runIfChanged(
(config) => config.remote_control,
() => setRemoteControlEntityFromConfig(this._api),
);
runIfChanged(
(config) => config.automations,
() => setAutomationsFromConfig(this._api),
);
runIfChanged(
(config) => [config.cameras, config.cameras_global],
() => {
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
void this._api.getCameraManager().destroy();
},
true,
);
runIfChanged(
(config) => config.live.microphone.always_connected,
() => {
this._api
.getInitializationManager()
.uninitialize(InitializationAspect.MICROPHONE_CONNECT);
},
true,
);
void this._initializeBackgroundAndUpdate(previousConfig);
}
/**
* Initialize config dependent items in the background. For items that the
* card hard requires, use InitializationManager instead.
*/
private async _initializeBackgroundAndUpdate(
previousConfig: AdvancedCameraCardConfig | null,
): Promise<void> {
await this._api.getDefaultManager().initializeIfNecessary(previousConfig);
await this._api.getMediaPlayerManager().initializeIfNecessary(previousConfig);
// The config is only set in the state if the card is already fully
// initialized. If not, the config will be set post initialization in the
// InitializationManager.
if (
this._overriddenConfig &&
this._api.getInitializationManager().isInitializedMandatory()
) {
this._api.getConditionStateManager().setState({
config: this._overriddenConfig,
});
}
this._api.getCardElementManager().update();
}
}