Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [1.0.7] - 2026-01-24

### Fixed
- Fixed critical memory leak in websocket listener that accumulated handlers over time
- Fixed websocket connection not being detected when closed unexpectedly
- Added connection state monitoring to trigger reconnection when websocket closes
- Added proper cleanup of old event listeners during reconnection

## [1.0.6] - 2026-01-10

### Added
Expand Down
2 changes: 1 addition & 1 deletion config.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Home Assistant Add-on Configuration
name: Eviqo MQTT
version: "1.0.6"
version: "1.0.7"
slug: eviqo-mqtt
description: >-
Bridges Eviqo EV charger data to MQTT with Home Assistant auto-discovery.
Expand Down
513 changes: 249 additions & 264 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eviqo-monorepo",
"version": "1.0.6",
"version": "1.0.7",
"private": true,
"description": "Eviqo EV Charger client API and MQTT gateway monorepo",
"workspaces": [
Expand Down
2 changes: 1 addition & 1 deletion packages/eviqo-client-api/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eviqo-client-api",
"version": "1.0.6",
"version": "1.0.7",
"description": "Node.js/TypeScript client library for Eviqo EV charging stations. Control and monitor your Eviqo devices via cloud WebSocket connection with full TypeScript support.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
43 changes: 36 additions & 7 deletions packages/eviqo-client-api/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,18 +122,30 @@ export class EviqoWebsocketConnection extends EventEmitter {
return;
}

this.ws.on('open', () => {
const connectHandler = () => {
logger.debug('Connected successfully!');
this.ws!.removeListener('error', errorHandler);
resolve(true);
});
};

this.ws.on('error', (error) => {
const errorHandler = (error: Error) => {
logger.error(`WebSocket error: ${error.message}`);
this.ws!.removeListener('open', connectHandler);
reject(error);
};

this.ws.once('open', connectHandler);
this.ws.once('error', errorHandler);

// Set up persistent handlers for ongoing connection monitoring
this.ws.on('close', (code, reason) => {
logger.warn(`WebSocket closed: code=${code} reason=${reason.toString()}`);
this.emit('connectionClosed', { code, reason: reason.toString() });
});

this.ws.on('close', () => {
logger.debug('WebSocket closed');
this.ws.on('error', (error) => {
logger.error(`WebSocket error during operation: ${error.message}`);
this.emit('connectionError', error);
});
});
} catch (error) {
Expand Down Expand Up @@ -462,12 +474,15 @@ export class EviqoWebsocketConnection extends EventEmitter {
}

try {
// Declare handler in outer scope so both promises can access it
let messageHandler: ((message: WebSocket.Data) => void) | null = null;

return await Promise.race([
new Promise<{
header: MessageHeader | null;
payload: Record<string, unknown> | string | null;
}>((resolve) => {
const messageHandler = (message: WebSocket.Data) => {
messageHandler = (message: WebSocket.Data) => {
if (message instanceof Buffer) {
logger.debug(`RECEIVED BINARY (${message.length} bytes):`);
const { header, payload } = parseBinaryMessage(message);
Expand All @@ -476,7 +491,10 @@ export class EviqoWebsocketConnection extends EventEmitter {
this.handleWidgetUpdate(payload as Record<string, unknown>);
}

this.ws?.removeListener('message', messageHandler);
if (messageHandler) {
this.ws?.removeListener('message', messageHandler);
messageHandler = null; // Prevent double removal
}
resolve({ header, payload });
}
};
Expand All @@ -489,6 +507,10 @@ export class EviqoWebsocketConnection extends EventEmitter {
}>((resolve) => {
setTimeout(() => {
logger.debug('Listening period ended');
if (messageHandler) {
this.ws?.removeListener('message', messageHandler);
messageHandler = null; // Prevent double removal
}
resolve({ header: null, payload: null });
}, duration * 1000);
}),
Expand Down Expand Up @@ -637,4 +659,11 @@ export class EviqoWebsocketConnection extends EventEmitter {
getDevicePages(): EviqoDevicePageModel[] {
return this.devicePages;
}

/**
* Check if websocket is connected and ready
*/
isConnected(): boolean {
return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
}
}
2 changes: 1 addition & 1 deletion packages/eviqo-mqtt/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "eviqo-mqtt",
"version": "1.0.6",
"version": "1.0.7",
"description": "MQTT gateway for Eviqo EV charging stations with Home Assistant auto-discovery support",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
28 changes: 28 additions & 0 deletions packages/eviqo-mqtt/src/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,11 @@ export class EviqoMqttGateway extends EventEmitter {
// Track connection time for periodic reconnection
this.lastEviqoConnectTime = Date.now();

// Remove old event listeners if reconnecting
if (this.eviqoClient) {
this.eviqoClient.removeAllListeners();
}

this.eviqoClient = new EviqoWebsocketConnection(
WS_URL,
null,
Expand All @@ -242,6 +247,20 @@ export class EviqoMqttGateway extends EventEmitter {
this.handleCommandSent(command);
});

// Set up connection monitoring handlers
this.eviqoClient.on('connectionClosed', (info: { code: number; reason: string }) => {
if (!this.shutdownRequested) {
logger.warn(`Eviqo websocket closed unexpectedly: code=${info.code} reason=${info.reason}`);
this.scheduleReconnect();
}
});

this.eviqoClient.on('connectionError', (error: Error) => {
if (!this.shutdownRequested) {
logger.error(`Eviqo websocket error: ${error.message}`);
}
});

// Connect and authenticate
if (!(await this.eviqoClient.connect())) {
throw new Error('Failed to connect to Eviqo API');
Expand Down Expand Up @@ -354,6 +373,15 @@ export class EviqoMqttGateway extends EventEmitter {
private async monitorLoop(): Promise<void> {
while (!this.shutdownRequested && this.eviqoClient) {
try {
// Check if websocket is still connected
if (!this.eviqoClient.isConnected()) {
logger.warn('Eviqo websocket is no longer connected');
if (!this.shutdownRequested) {
this.scheduleReconnect();
}
break;
}

// Check if periodic reconnection is needed (to prevent auth timeout)
if (this.shouldReconnect()) {
logger.info('Periodic websocket reconnection triggered to prevent auth timeout');
Expand Down
Loading