235 lines
7.8 KiB
TypeScript
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();
|
|
}
|
|
}
|