diff --git a/src/card-controller/initialization-manager.ts b/src/card-controller/initialization-manager.ts index 1b630fc3..aba58c22 100644 --- a/src/card-controller/initialization-manager.ts +++ b/src/card-controller/initialization-manager.ts @@ -21,11 +21,19 @@ export class InitializationManager { } public isInitializedMandatory(): boolean { + const config = this._api.getConfigManager().getConfig(); + if (!config) { + return false; + } + return ( this._initializer.isInitializedMultiple([ InitializationAspect.LANGUAGES, InitializationAspect.SIDE_LOAD_ELEMENTS, InitializationAspect.CAMERAS, + ...(config.live.microphone.always_connected + ? [InitializationAspect.MICROPHONE_CONNECT] + : []), ]) && // If there's no view, re-initialize (e.g. config changes). this._api.getViewManager().hasView() @@ -54,15 +62,25 @@ export class InitializationManager { return false; } - if (!this._api.getConfigManager().hasConfig()) { + const config = this._api.getConfigManager().getConfig(); + if (!config) { return false; } if ( - !(await this._initializer.initializeIfNecessary( - InitializationAspect.CAMERAS, - async () => await this._api.getCameraManager().initializeCamerasFromConfig(), - )) + !(await this._initializer.initializeMultipleIfNecessary({ + [InitializationAspect.CAMERAS]: async () => + await this._api.getCameraManager().initializeCamerasFromConfig(), + + // Connecting the microphone (if configured) is considered mandatory to + // avoid issues with some cameras that only allow 2-way audio on the + // first stream initialized. See: + // https://github.com/dermotduffy/frigate-hass-card/issues/1235 + ...(config.live.microphone.always_connected && { + [InitializationAspect.MICROPHONE_CONNECT]: async () => + await this._api.getMicrophoneManager().connect(), + }), + })) ) { return false; } @@ -104,9 +122,6 @@ export class InitializationManager { ...(config.menu.buttons.media_player.enabled ? [InitializationAspect.MEDIA_PLAYERS] : []), - ...(config.live.microphone.always_connected - ? [InitializationAspect.MICROPHONE_CONNECT] - : []), ]) ) { return true; @@ -118,10 +133,6 @@ export class InitializationManager { [InitializationAspect.MEDIA_PLAYERS]: async () => await this._api.getMediaPlayerManager().initialize(), }), - ...(config.live.microphone.always_connected && { - [InitializationAspect.MICROPHONE_CONNECT]: async () => - await this._api.getMicrophoneManager().connect(), - }), })) ) { return false; diff --git a/tests/card-controller/initialization-manager.test.ts b/tests/card-controller/initialization-manager.test.ts index 7ee8bd0b..7abc77b0 100644 --- a/tests/card-controller/initialization-manager.test.ts +++ b/tests/card-controller/initialization-manager.test.ts @@ -19,11 +19,20 @@ describe('InitializationManager', () => { }); describe('should correctly determine when mandatory initialization is required', () => { + it('without config', () => { + const api = createCardAPI(); + const initializer = mock(); + const manager = new InitializationManager(api, initializer); + + expect(manager.isInitializedMandatory()).toBeFalsy(); + }); + it('without aspects', () => { const api = createCardAPI(); const initializer = mock(); const manager = new InitializationManager(api, initializer); + vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); initializer.isInitializedMultiple.mockReturnValue(false); expect(manager.isInitializedMandatory()).toBeFalsy(); @@ -34,6 +43,7 @@ describe('InitializationManager', () => { const initializer = mock(); const manager = new InitializationManager(api, initializer); + vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); initializer.isInitializedMultiple.mockReturnValue(true); expect(manager.isInitializedMandatory()).toBeFalsy(); @@ -44,6 +54,27 @@ describe('InitializationManager', () => { const initializer = mock(); const manager = new InitializationManager(api, initializer); + vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); + initializer.isInitializedMultiple.mockReturnValue(true); + vi.mocked(api.getViewManager().hasView).mockReturnValue(true); + + expect(manager.isInitializedMandatory()).toBeTruthy(); + }); + + it('with microphone if configured', () => { + const api = createCardAPI(); + const initializer = mock(); + const manager = new InitializationManager(api, initializer); + + vi.mocked(api.getConfigManager().getConfig).mockReturnValue( + createConfig({ + live: { + microphone: { + always_connected: true, + }, + }, + }), + ); initializer.isInitializedMultiple.mockReturnValue(true); vi.mocked(api.getViewManager().hasView).mockReturnValue(true); @@ -67,7 +98,7 @@ describe('InitializationManager', () => { it('successfully', async () => { const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true); + vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(false); vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue( false, @@ -80,12 +111,31 @@ describe('InitializationManager', () => { expect(sideLoadHomeAssistantElements).toBeCalled(); expect(api.getCameraManager().initializeCamerasFromConfig).toBeCalled(); expect(api.getViewManager().setViewDefault).toBeCalled(); + expect(api.getMicrophoneManager().connect).not.toBeCalled(); + }); + + it('successfully with microphone if configured', async () => { + const api = createCardAPI(); + vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); + vi.mocked(api.getConfigManager().getConfig).mockReturnValue( + createConfig({ + live: { + microphone: { + always_connected: true, + }, + }, + }), + ); + const manager = new InitializationManager(api); + + expect(await manager.initializeMandatory()).toBeTruthy(); + expect(api.getMicrophoneManager().connect).toBeCalled(); }); it('successfully with querystring view', async () => { const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true); + vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(false); vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue(true); const manager = new InitializationManager(api); @@ -98,7 +148,7 @@ describe('InitializationManager', () => { it('with message set during initialization', async () => { const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true); + vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); vi.mocked(api.getMessageManager().hasMessage).mockReturnValue(true); vi.mocked(api.getQueryStringManager().hasViewRelatedActions).mockReturnValue( false, @@ -124,12 +174,13 @@ describe('InitializationManager', () => { it('with cameras in progress', async () => { const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); - vi.mocked(api.getConfigManager().hasConfig).mockReturnValue(true); + vi.mocked(api.getConfigManager().getConfig).mockReturnValue(createConfig()); const initializer = mock(); const manager = new InitializationManager(api, initializer); - initializer.initializeMultipleIfNecessary.mockResolvedValue(true); - initializer.initializeIfNecessary.mockResolvedValue(false); + initializer.initializeMultipleIfNecessary + .mockResolvedValueOnce(true) + .mockResolvedValueOnce(false); expect(await manager.initializeMandatory()).toBeFalsy(); }); @@ -154,17 +205,11 @@ describe('InitializationManager', () => { }, }, }, - live: { - microphone: { - always_connected: false, - }, - }, }), ); expect(await manager.initializeBackgroundIfNecessary()).toBeTruthy(); expect(api.getMediaPlayerManager().initialize).not.toBeCalled(); - expect(api.getMicrophoneManager().connect).not.toBeCalled(); }); it('successfully with all inititalizers', async () => { @@ -180,21 +225,15 @@ describe('InitializationManager', () => { }, }, }, - live: { - microphone: { - always_connected: true, - }, - }, }), ); expect(await manager.initializeBackgroundIfNecessary()).toBeTruthy(); expect(api.getMediaPlayerManager().initialize).toBeCalled(); - expect(api.getMicrophoneManager().connect).toBeCalled(); expect(api.getCardElementManager().update).toBeCalled(); }); - it('with media player and microphone connect in progress', async () => { + it('with media player in progress', async () => { const api = createCardAPI(); vi.mocked(api.getHASSManager().getHASS).mockReturnValue(createHASS()); vi.mocked(api.getConfigManager().getConfig).mockReturnValue( @@ -206,11 +245,6 @@ describe('InitializationManager', () => { }, }, }, - live: { - microphone: { - always_connected: true, - }, - }, }), ); const initializer = mock(); diff --git a/vite.config.ts b/vite.config.ts index 67ec51a5..c9e5ca8e 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -10,10 +10,10 @@ export default defineConfig({ // Thresholds will automatically be updated as coverage improves to avoid // back-sliding. thresholdAutoUpdate: true, - statements: 72.73, - branches: 61.93, + statements: 72.76, + branches: 61.95, functions: 74.01, - lines: 72.63, + lines: 72.66, }, }, });