fix: automations on microphone connect not working (#2403)
- Closes #2314 I used GPT-5.4 to investigate and generate this fix. I'm not sure if the code is good, but I can confirm it works. --------- Co-authored-by: dermotduffy <dermot.duffy@gmail.com>
This commit is contained in:
co-authored by
dermotduffy
parent
4fceade37c
commit
ee23cb2b9d
@@ -138,32 +138,59 @@ export class ConfigManager {
|
|||||||
setFoldersFromConfig(this._api);
|
setFoldersFromConfig(this._api);
|
||||||
this._api.getStyleManager().updateFromConfig();
|
this._api.getStyleManager().updateFromConfig();
|
||||||
|
|
||||||
// Ensure features that register automations or other side-effects from
|
// Only re-run side-effect callbacks when their relevant config section
|
||||||
// configuration are updated when overrides change (e.g. remote_control).
|
// changed. As an example of why: Automation loaders delete then recreate
|
||||||
// Re-run loaders that may add/remove automations based on the current
|
// automations, which destroys associated ConditionsManagers. If a condition
|
||||||
// effective configuration.
|
// transition (e.g. microphone connect) triggers both a user automation and
|
||||||
setKeyboardShortcutsFromConfig(this._api);
|
// an unrelated override, an unconditional reload would delete the
|
||||||
setRemoteControlEntityFromConfig(this._api);
|
// automation mid-transition — the freshly created replacement has no prior
|
||||||
setAutomationsFromConfig(this._api);
|
// 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (
|
runIfChanged(
|
||||||
previousConfig &&
|
(config) => config.view.keyboard_shortcuts,
|
||||||
(!isEqual(previousConfig?.cameras, this._overriddenConfig?.cameras) ||
|
() => setKeyboardShortcutsFromConfig(this._api),
|
||||||
!isEqual(previousConfig?.cameras_global, this._overriddenConfig?.cameras_global))
|
);
|
||||||
) {
|
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);
|
this._api.getInitializationManager().uninitialize(InitializationAspect.CAMERAS);
|
||||||
this._api.getCameraManager().destroy();
|
this._api.getCameraManager().destroy();
|
||||||
}
|
},
|
||||||
|
true,
|
||||||
if (
|
);
|
||||||
previousConfig &&
|
runIfChanged(
|
||||||
previousConfig?.live.microphone.always_connected !==
|
(config) => config.live.microphone.always_connected,
|
||||||
this._overriddenConfig?.live.microphone.always_connected
|
() => {
|
||||||
) {
|
|
||||||
this._api
|
this._api
|
||||||
.getInitializationManager()
|
.getInitializationManager()
|
||||||
.uninitialize(InitializationAspect.MICROPHONE_CONNECT);
|
.uninitialize(InitializationAspect.MICROPHONE_CONNECT);
|
||||||
}
|
},
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
|
||||||
/* async */ this._initializeBackgroundAndUpdate(previousConfig);
|
/* async */ this._initializeBackgroundAndUpdate(previousConfig);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,5 +4,5 @@ export const setAutomationsFromConfig = (api: CardConfigLoaderAPI): void => {
|
|||||||
api.getAutomationsManager().deleteAutomations();
|
api.getAutomationsManager().deleteAutomations();
|
||||||
api
|
api
|
||||||
.getAutomationsManager()
|
.getAutomationsManager()
|
||||||
.addAutomations(api.getConfigManager().getNonOverriddenConfig()?.automations ?? []);
|
.addAutomations(api.getConfigManager().getConfig()?.automations ?? []);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
import { ZodError, z } from 'zod';
|
||||||
import { AutomationsManager } from '../../../src/card-controller/automations-manager';
|
import { AutomationsManager } from '../../../src/card-controller/automations-manager';
|
||||||
import { ConfigManager } from '../../../src/card-controller/config/config-manager';
|
import { ConfigManager } from '../../../src/card-controller/config/config-manager';
|
||||||
|
import { setRemoteControlEntityFromConfig } from '../../../src/card-controller/config/load-control-entities';
|
||||||
|
import { setKeyboardShortcutsFromConfig } from '../../../src/card-controller/config/load-keyboard-shortcuts';
|
||||||
import { InitializationAspect } from '../../../src/card-controller/initialization-manager';
|
import { InitializationAspect } from '../../../src/card-controller/initialization-manager';
|
||||||
import { ConditionStateManager } from '../../../src/conditions/state-manager';
|
import { ConditionStateManager } from '../../../src/conditions/state-manager';
|
||||||
import { Automation } from '../../../src/config/schema/automations';
|
import { Automation } from '../../../src/config/schema/automations';
|
||||||
@@ -8,7 +11,6 @@ import { AdvancedCameraCardCondition } from '../../../src/config/schema/conditio
|
|||||||
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
|
import { advancedCameraCardConfigSchema } from '../../../src/config/schema/types';
|
||||||
import { createGeneralAction } from '../../../src/utils/action';
|
import { createGeneralAction } from '../../../src/utils/action';
|
||||||
import { createCardAPI, createConfig, flushPromises } from '../../test-utils';
|
import { createCardAPI, createConfig, flushPromises } from '../../test-utils';
|
||||||
import { ZodError, z } from 'zod';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a ConfigManager test setup with real AutomationsManager and ConditionStateManager.
|
* Create a ConfigManager test setup with real AutomationsManager and ConditionStateManager.
|
||||||
@@ -679,6 +681,46 @@ describe('ConfigManager', () => {
|
|||||||
// The automation should still not execute because it was deleted by the override
|
// The automation should still not execute because it was deleted by the override
|
||||||
expect(executeActionsMock).not.toHaveBeenCalled();
|
expect(executeActionsMock).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not reload user automations for unrelated override changes', async () => {
|
||||||
|
const { manager, stateManager, api, addAutomationsSpy, deleteAutomationsSpy } =
|
||||||
|
createConfigManagerTestSetup();
|
||||||
|
|
||||||
|
const executeActionsMock = vi.fn();
|
||||||
|
vi.mocked(api.getActionsManager().executeActions).mockImplementation(
|
||||||
|
executeActionsMock,
|
||||||
|
);
|
||||||
|
|
||||||
|
const automation = {
|
||||||
|
conditions: [TEST_CONDITIONS.FULLSCREEN_ON],
|
||||||
|
actions: [createGeneralAction('screenshot')],
|
||||||
|
};
|
||||||
|
const config = createConfig({
|
||||||
|
automations: [automation],
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
conditions: [TEST_CONDITIONS.FULLSCREEN_ON],
|
||||||
|
set: {
|
||||||
|
'menu.buttons.microphone.enabled': false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.setConfig(config);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
addAutomationsSpy.mockClear();
|
||||||
|
deleteAutomationsSpy.mockClear();
|
||||||
|
executeActionsMock.mockClear();
|
||||||
|
|
||||||
|
stateManager.setState({ fullscreen: true });
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(deleteAutomationsSpy).not.toHaveBeenCalled();
|
||||||
|
expect(addAutomationsSpy).not.toHaveBeenCalled();
|
||||||
|
expect(executeActionsMock).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('remote-control loader with overrides', () => {
|
describe('remote-control loader with overrides', () => {
|
||||||
@@ -759,6 +801,73 @@ describe('ConfigManager', () => {
|
|||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should not reload remote-control automations for unrelated override changes', async () => {
|
||||||
|
const { manager, stateManager, deleteAutomationsSpy } =
|
||||||
|
createConfigManagerTestSetup();
|
||||||
|
|
||||||
|
const config = createConfig({
|
||||||
|
remote_control: {
|
||||||
|
entities: { camera: 'input_select.camera' },
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
conditions: [TEST_CONDITIONS.FULLSCREEN_ON],
|
||||||
|
set: {
|
||||||
|
'menu.buttons.microphone.enabled': false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.setConfig(config);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
deleteAutomationsSpy.mockClear();
|
||||||
|
|
||||||
|
stateManager.setState({ fullscreen: true });
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(deleteAutomationsSpy).not.toHaveBeenCalledWith(
|
||||||
|
setRemoteControlEntityFromConfig,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('keyboard-shortcuts loader with overrides', () => {
|
||||||
|
it('should not reload keyboard-shortcut automations for unrelated override changes', async () => {
|
||||||
|
const { manager, stateManager, deleteAutomationsSpy } =
|
||||||
|
createConfigManagerTestSetup();
|
||||||
|
|
||||||
|
const config = createConfig({
|
||||||
|
view: {
|
||||||
|
keyboard_shortcuts: {
|
||||||
|
enabled: true,
|
||||||
|
ptz_left: { key: 'ArrowLeft' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
overrides: [
|
||||||
|
{
|
||||||
|
conditions: [TEST_CONDITIONS.FULLSCREEN_ON],
|
||||||
|
set: {
|
||||||
|
'menu.buttons.microphone.enabled': false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
manager.setConfig(config);
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
deleteAutomationsSpy.mockClear();
|
||||||
|
|
||||||
|
stateManager.setState({ fullscreen: true });
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(deleteAutomationsSpy).not.toHaveBeenCalledWith(
|
||||||
|
setKeyboardShortcutsFromConfig,
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ describe('setAutomationsFromConfig', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
vi.mocked(api.getConfigManager().getNonOverriddenConfig).mockReturnValue(
|
vi.mocked(api.getConfigManager().getConfig).mockReturnValue(
|
||||||
createConfig({
|
createConfig({
|
||||||
automations: automations,
|
automations: automations,
|
||||||
}),
|
}),
|
||||||
|
|||||||
Reference in New Issue
Block a user