fix(connection): re-initialize camera managers and event subscriptions on reconnect (#2389)
This PR fixes an issue where, after running the card for a few hours, new events (such as thumbnails and triggers) would stop showing up. This bug was caused by the Home Assistant connection dropping and reconnecting in the background. Event subscriptions (such as Frigate WebSocket events established via `hass.connection.subscribeMessage`) are bound to the connection they were created on. When the connection drops, those subscriptions are lost, and until now, they were not being automatically recreated when the connection was restored. This PR ensures that when a restored connection is detected, the card properly re-initializes the camera and view components to re-establish these background subscriptions. 1. HA Connection Reconnect Handling (`HASSManager`) - Added logic to detect when the Home Assistant connection transitions from disconnected back to connected. - When restored, the card uninitializes the `CAMERAS`, `VIEW`, and `INITIAL_TRIGGER` initialization aspects. - This forces the system to recreate the camera managers and re-subscribe to the relevant WebSocket event feeds in the next render cycle, fixing the broken thumbnail/trigger feeds. 2. Centralized Camera Teardown (`InitializationManager`) - Improved the teardown logic by moving the `this._api.getCameraManager().destroy()` call directly into `InitializationManager.uninitialize` when the `CAMERAS` aspect is targeted. - Removed duplicate `destroy()` calls scattered across `ConfigManager` and `CardElementManager`. - This ensures that whenever cameras are forced to re-initialize (such as during a reconnection event), the old manager is safely destroyed in a centralized, predictable way without memory leaks. 3. Testing - Added tests in `hass-manager.test.ts` to verify the uninitialization logic fires during a reconnection event. - Updated `initialization-manager.test.ts` to verify that `CameraManager.destroy()` is automatically called when uninitializing cameras. --------- Co-authored-by: dermotduffy <dermot.duffy@gmail.com>
This commit is contained in:
co-authored by
dermotduffy
parent
8223cae501
commit
a81d72375d
@@ -1,6 +1,8 @@
|
|||||||
import { hasHAConnectionStateChanged } from '../../ha/has-hass-connection-changed';
|
import { hasHAConnectionStateChanged } from '../../ha/has-hass-connection-changed';
|
||||||
import { HomeAssistant } from '../../ha/types';
|
import { HomeAssistant } from '../../ha/types';
|
||||||
import { localize } from '../../localize/localize';
|
import { localize } from '../../localize/localize';
|
||||||
|
import { log } from '../../utils/debug';
|
||||||
|
import { InitializationAspect } from '../initialization-manager';
|
||||||
import { CardHASSAPI } from '../types';
|
import { CardHASSAPI } from '../types';
|
||||||
import { StateWatcher, StateWatcherSubscriptionInterface } from './state-watcher';
|
import { StateWatcher, StateWatcherSubscriptionInterface } from './state-watcher';
|
||||||
|
|
||||||
@@ -36,6 +38,28 @@ export class HASSManager {
|
|||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this._api.getMessageManager().resetType('connection');
|
this._api.getMessageManager().resetType('connection');
|
||||||
|
|
||||||
|
// When the HA WebSocket connection is restored after a drop,
|
||||||
|
// reinitialize cameras and the view. This is necessary because
|
||||||
|
// event subscriptions (e.g. Frigate WebSocket subscriptions via
|
||||||
|
// hass.connection.subscribeMessage) are tied to the old connection
|
||||||
|
// and are lost when it drops. Without reinitialization, triggers
|
||||||
|
// and thumbnail updates stop working.
|
||||||
|
if (this._hass) {
|
||||||
|
log(
|
||||||
|
this._api.getConfigManager().getCardWideConfig(),
|
||||||
|
'Advanced Camera Card: HA connection restored, reinitializing...',
|
||||||
|
);
|
||||||
|
|
||||||
|
this._api
|
||||||
|
.getInitializationManager()
|
||||||
|
.uninitialize(InitializationAspect.CAMERAS);
|
||||||
|
this._api.getCameraManager().destroy();
|
||||||
|
this._api.getInitializationManager().uninitialize(InitializationAspect.VIEW);
|
||||||
|
this._api
|
||||||
|
.getInitializationManager()
|
||||||
|
.uninitialize(InitializationAspect.INITIAL_TRIGGER);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ export interface CardHASSAPI {
|
|||||||
getConditionStateManager(): ConditionStateManager;
|
getConditionStateManager(): ConditionStateManager;
|
||||||
getConfigManager(): ConfigManager;
|
getConfigManager(): ConfigManager;
|
||||||
getDefaultManager(): DefaultManager;
|
getDefaultManager(): DefaultManager;
|
||||||
|
getInitializationManager(): InitializationManager;
|
||||||
getInteractionManager(): InteractionManager;
|
getInteractionManager(): InteractionManager;
|
||||||
getMediaPlayerManager(): MediaPlayerManager;
|
getMediaPlayerManager(): MediaPlayerManager;
|
||||||
getMessageManager(): MessageManager;
|
getMessageManager(): MessageManager;
|
||||||
|
|||||||
@@ -114,6 +114,36 @@ describe('HASSManager', () => {
|
|||||||
expect(api.getMessageManager().resetType).toBeCalled();
|
expect(api.getMessageManager().resetType).toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('reconnected reinitializes cameras and view', () => {
|
||||||
|
const api = createCardAPI();
|
||||||
|
const manager = new HASSManager(api);
|
||||||
|
|
||||||
|
// First establish a connected state.
|
||||||
|
const connectedHASS = createHASS();
|
||||||
|
connectedHASS.connected = true;
|
||||||
|
manager.setHASS(connectedHASS);
|
||||||
|
|
||||||
|
// Simulate disconnection.
|
||||||
|
const disconnectedHASS = createHASS();
|
||||||
|
disconnectedHASS.connected = false;
|
||||||
|
manager.setHASS(disconnectedHASS);
|
||||||
|
|
||||||
|
// Simulate reconnection.
|
||||||
|
const reconnectedHASS = createHASS();
|
||||||
|
reconnectedHASS.connected = true;
|
||||||
|
manager.setHASS(reconnectedHASS);
|
||||||
|
|
||||||
|
// Cameras and view should be uninitialized so they get re-subscribed
|
||||||
|
// to event sources (e.g. Frigate WebSocket events) on the next
|
||||||
|
// render cycle.
|
||||||
|
expect(api.getInitializationManager().uninitialize).toBeCalledWith('cameras');
|
||||||
|
expect(api.getCameraManager().destroy).toBeCalled();
|
||||||
|
expect(api.getInitializationManager().uninitialize).toBeCalledWith('view');
|
||||||
|
expect(api.getInitializationManager().uninitialize).toBeCalledWith(
|
||||||
|
'initial-trigger',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
it('hass is null', () => {
|
it('hass is null', () => {
|
||||||
const api = createCardAPI();
|
const api = createCardAPI();
|
||||||
const manager = new HASSManager(api);
|
const manager = new HASSManager(api);
|
||||||
|
|||||||
Reference in New Issue
Block a user