homebridge-smartsystem 6.8.3 → 6.8.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -307,6 +307,8 @@ Homebridge UI version
307
307
  - 1: max/min values for hbValues
308
308
  - 2: HB up/down -> don't respond to motor unit's, only macro
309
309
  - 3: remember request before the login kicked in (and redirect to that one, once the login was succesful)
310
+ - 4: now using only TCP sockets to the server
311
+ - 5: making it more robust / inline with repo-service version
310
312
 
311
313
  ## Hardware
312
314
  A Raspberry Pi that connects to a Duotecno IP Node
package/config.json CHANGED
@@ -461,11 +461,11 @@
461
461
  "p1": false,
462
462
  "proxy": {
463
463
  "cloudServer": "ws.duotecno.eu",
464
- "cloudPort": 5098,
464
+ "cloudPort": 5097,
465
465
  "masterAddress": "192.168.0.97",
466
466
  "masterPort": 5001,
467
467
  "uniqueId": "",
468
468
  "debug": false,
469
- "kind": "solo"
469
+ "kind": "gw"
470
470
  }
471
471
  }
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "homebridge-smartsystem",
3
3
  "displayname": "Duotecno Bridge",
4
- "version": "6.8.3",
5
- "description": "SmartServer (Proxy Websockets to TCP sockets, Smappee MQTT, Duotecno IP Nodes, Homekit interface)",
4
+ "version": "6.8.5",
5
+ "description": "SmartServer (Proxy TCP sockets to the cloud, Smappee MQTT, Duotecno IP Nodes, Homekit interface)",
6
6
  "main": "index.js",
7
7
  "author": "Johan Coppieters",
8
8
  "homepage": "http://www.duotecno.be",
package/server/proxy.js CHANGED
@@ -2,7 +2,6 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.Context = exports.cleanStart = exports.makeNewCloudConnection = exports.setProxyConfig = exports.kEmptyProxy = void 0;
4
4
  const net = require("net");
5
- const WebSocket = require("ws");
6
5
  const child_process_1 = require("child_process");
7
6
  const process_1 = require("process");
8
7
  exports.kEmptyProxy = {
@@ -61,36 +60,38 @@ function error(str) {
61
60
  ///////////////////////////////
62
61
  // Start up the proxy server //
63
62
  ///////////////////////////////
64
- // if (config.kind != "gw") {
65
- // makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
66
- // }
67
- function makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId) {
68
- const cloudSocket = new WebSocket('ws://' + server + ':' + serverPort + "/" + uniqueId);
69
- cloudSocket.on('open', () => {
63
+ if (config.uniqueId) {
64
+ makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
65
+ }
66
+ function makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId, count = 0) {
67
+ const cloudSocket = new net.Socket();
68
+ cloudSocket.connect(serverPort, server, () => {
70
69
  log(`[PROXY] → [CLOUD] Connected to Cloud at ${server}:${serverPort}`);
70
+ // Send unique ID as the first message to identify this proxy/device
71
+ cloudSocket.write(`[${uniqueId}]`);
72
+ debug(`[PROXY] → [CLOUD] Sent unique ID: ${uniqueId}`);
71
73
  const context = new Context(cloudSocket, master, masterPort, server, serverPort, uniqueId);
72
74
  gCloudConnections.push(context);
73
75
  log(`[PROXY] → [CLOUD] New free connection #${gCloudConnections.length} to the Cloud at ${server}:${serverPort}`);
74
76
  });
75
- log(`[PROXY] → [CLOUD] Attempting to start a new free connection for ${config.uniqueId} to the Cloud at ${server}:${serverPort}`);
77
+ cloudSocket.on('error', (err) => {
78
+ error(`[PROXY] → [CLOUD] Failed to connect ${count + 1} times to the Cloud server: ${err.message}`);
79
+ if (count < 3) {
80
+ setTimeout(() => {
81
+ makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId, count + 1);
82
+ }, 1000);
83
+ }
84
+ });
85
+ log(`[PROXY] → [CLOUD] Attempt ${count + 1} to start a new free connection for ${config.uniqueId} to the Cloud at ${server}:${serverPort}`);
76
86
  }
77
87
  exports.makeNewCloudConnection = makeNewCloudConnection;
78
88
  ////////////////
79
89
  // Restarting //
80
90
  ////////////////
81
91
  function cleanStart(restart = false) {
82
- if (gCloudConnections.length) {
83
- gCloudConnections.forEach(context => {
84
- if (context.deviceSocket) {
85
- context.deviceSocket.end();
86
- context.deviceSocket = null;
87
- }
88
- context.cloudSocket.close();
89
- context.cloudSocket = null;
90
- });
91
- gCloudConnections.splice(0, gCloudConnections.length); // clear the array
92
- log(`[PROXY] → [CLOUD] Cleaned up all cloud connections.`);
93
- }
92
+ gCloudConnections.forEach(ctx => ctx.cleanupSockets());
93
+ log(`[PROXY] → [CLOUD] Cleaned up all ${gCloudConnections.length} cloud connections.`);
94
+ gCloudConnections.splice(0, gCloudConnections.length); // clear the array
94
95
  if (restart) {
95
96
  if (config.uniqueId) {
96
97
  makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
@@ -117,11 +118,11 @@ class Context {
117
118
  }
118
119
  setupCloudSocket() {
119
120
  // set up handlers for the cloud socket
120
- this.cloudSocket.on('message', message => {
121
- this.handleDataFromCloud(message);
121
+ this.cloudSocket.on('data', (data) => {
122
+ this.handleDataFromCloud(data);
122
123
  });
123
- this.cloudSocket.on('close', (code, reason) => {
124
- warning('[CLOUD] Connection closed: ' + code + ' ' + reason);
124
+ this.cloudSocket.on('close', () => {
125
+ warning('[CLOUD] Connection closed');
125
126
  if (this.deviceSocket) {
126
127
  this.deviceSocket.end();
127
128
  this.deviceSocket = null;
@@ -136,27 +137,32 @@ class Context {
136
137
  });
137
138
  }
138
139
  handleDataFromCloud(data) {
139
- debug(`[CLOUD] → [PROXY]: Data from Cloud: ${data.toString().slice(0, -1)}`);
140
+ debug(`[CLOUD] → [PROXY]: Data from Cloud: ${data.toString()}`);
140
141
  if (!this.deviceSocket) {
141
142
  // we either have a real client that wants to connect to the device,
142
143
  // or we receive a heartbeat from the cloud server
144
+ // or we receive a connection response [OK], [OK-=2], [ERROR-...]
143
145
  if (this.isHeartbeatRequest(data)) {
144
146
  debug(`[CLOUD] → [PROXY]: Received heartbeat from Cloud, returning response.`);
145
147
  // respond to the heartbeat of the cloud server
146
- this.cloudSocket.send("[72,3]"); // command([Command.HeartbeatStatus, CommandMethod.Heartbeat.server])
148
+ this.cloudSocket.write("[72,3]"); // command([Command.HeartbeatStatus, CommandMethod.Heartbeat.server])
149
+ }
150
+ else if (this.isConnectionResponse(data)) {
151
+ debug(`[CLOUD] → [PROXY]: Received connection response from Cloud: ${data.toString()}`);
152
+ // Handle connection response from the cloud server
147
153
  }
148
154
  else {
149
- // There is incomming data, it's a fresh connection, so: a new client wants to connect to the device
155
+ // There is real incomming data, it's a fresh connection, so: a new client wants to connect to the device
150
156
  // 1) create a new (free) connection to the cloud server for the next client
151
157
  // 2) connect to the device and send the initial data
152
- log(`[PROXY] → [CLOUD] New client connection, creating new free connection for this proxy/device: ${this.uniqueId}.`);
158
+ log(`[PROXY] → [CLOUD] New client connection, creating new free connection for this proxy/device: ${config.uniqueId}.`);
153
159
  makeNewCloudConnection(this.master, this.masterPort, this.server, this.serverPort, this.uniqueId);
154
160
  this.makeDeviceConnection(this.master, this.masterPort, data);
155
161
  }
156
162
  }
157
163
  else if (this.deviceSocket.readyState === 'open') {
158
164
  this.deviceSocket.write(data);
159
- debug(`[PROXY] → [DEVICE] forwarding data to device: ${data.toString().slice(0, -1)}`);
165
+ debug(`[PROXY] → [DEVICE] forwarding data to device: ${data.toString().trim()}`);
160
166
  }
161
167
  else {
162
168
  warning(`[PROXY] → [DEVICE] Device socket is not open yet, waiting for connection... what to do with the data??`);
@@ -168,9 +174,14 @@ class Context {
168
174
  this.deviceSocket = new net.Socket();
169
175
  // Connect to local device and send the data that came in from the cloud
170
176
  this.deviceSocket.connect(port, address, () => {
177
+ // Check if socket is still valid (not cleaned up by removeConnection)
178
+ if (!this.deviceSocket) {
179
+ warning(`[PROXY] → [DEVICE] Device socket was cleaned up before connection completed`);
180
+ return;
181
+ }
171
182
  log(`[PROXY] → [DEVICE] Connected to local device at ${address}:${port}`);
172
183
  this.setUpDeviceSocket();
173
- log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().slice(0, -1)}`);
184
+ log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().trim()}`);
174
185
  this.deviceSocket.write(data);
175
186
  });
176
187
  }
@@ -183,10 +194,14 @@ class Context {
183
194
  }
184
195
  }
185
196
  setUpDeviceSocket() {
197
+ if (!this.deviceSocket) {
198
+ error(`[PROXY] → [DEVICE] Cannot set up device socket - socket is null`);
199
+ return;
200
+ }
186
201
  this.deviceSocket.on('data', (data) => {
187
202
  const message = data.toString('utf-8');
188
- debug(`[DEVICE] → [PROXY] forwarding data to Cloud: ${message.slice(0, -1)}`);
189
- this.cloudSocket.send(message);
203
+ debug(`[DEVICE] → [PROXY] forwarding data to Cloud: ${message.trim()}`);
204
+ this.cloudSocket.write(data);
190
205
  });
191
206
  this.deviceSocket.on('close', () => {
192
207
  log(`[DEVICE] → [PROXY] Device socket closed`);
@@ -197,27 +212,43 @@ class Context {
197
212
  this.removeConnection();
198
213
  });
199
214
  }
215
+ cleanupSockets() {
216
+ // Clean up the device socket
217
+ if (this.deviceSocket) {
218
+ if (!this.deviceSocket.destroyed) {
219
+ this.deviceSocket.removeAllListeners();
220
+ this.deviceSocket.end();
221
+ this.deviceSocket.destroy();
222
+ }
223
+ this.deviceSocket = null;
224
+ }
225
+ // Clean up the cloud socket
226
+ if (this.cloudSocket) {
227
+ if (!this.cloudSocket.destroyed) {
228
+ this.cloudSocket.removeAllListeners();
229
+ this.cloudSocket.end();
230
+ this.cloudSocket.destroy();
231
+ }
232
+ this.cloudSocket = null;
233
+ }
234
+ }
200
235
  removeConnection() {
201
- this.deviceSocket = null;
202
- // remove this context and close the cloud socket
203
236
  const index = gCloudConnections.indexOf(this);
204
- if (index !== -1) {
205
- this.cloudSocket.close();
206
- this.cloudSocket = null;
237
+ if (index !== -1)
207
238
  gCloudConnections.splice(index, 1);
208
- log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
209
- }
239
+ this.cleanupSockets();
240
+ log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
210
241
  if (gCloudConnections.length === 0) {
211
242
  log(`[PROXY] → [CLOUD] No more cloud connections, restarting proxy.`);
212
243
  // just to be sure: restart...
213
244
  if (config.kind === "pm2") {
214
245
  // PM2 should restart us.
215
246
  log(`[PROXY] → [CLOUD] PM2 should restart us, exiting now.`);
216
- process.exit();
247
+ setTimeout(() => process.exit(0), 100);
217
248
  }
218
249
  else if (config.kind === "solo") {
219
250
  log(`[PROXY] → [CLOUD] Running solo, restarting proxy.`);
220
- this.restart();
251
+ restart();
221
252
  }
222
253
  else {
223
254
  log(`[PROXY] → [CLOUD] Running under gateway/homebridge, not restarting... not sure what to do here.`);
@@ -227,31 +258,71 @@ class Context {
227
258
  ///////////////
228
259
  // Utilities //
229
260
  ///////////////
230
- restart() {
231
- console.log('🔄 Restarting...');
261
+ // check if it's a heartbeat request from the server
262
+ isHeartbeatRequest(data) {
263
+ const msg = data.toString('utf-8').trim();
264
+ return msg === '[215,3]';
265
+ // hearbeatKind = (poll = 1, poll_old = 2, serve = 3)
266
+ // return ((data[0] === 91) && // [
267
+ // (data[1] === 50) && // 2
268
+ // (data[2] === 49) && // 1
269
+ // (data[3] === 53) && // 5
270
+ // (data[4] === 44) && // ,
271
+ // (data[5] === 51) && // 3
272
+ // (data[6] === 93)); // ]
273
+ }
274
+ // check if it's a server response after connecting and sending our uniqueID
275
+ isConnectionResponse(data) {
276
+ const message = data.toString();
277
+ // Check for various server response patterns
278
+ return (message.startsWith('[OK')) || (message.startsWith('[ERROR'));
279
+ }
280
+ }
281
+ exports.Context = Context;
282
+ function restart() {
283
+ console.log('🔄 Restarting...');
284
+ setTimeout(() => {
232
285
  (0, child_process_1.spawn)(process_1.execPath, process_1.argv.slice(1), {
233
286
  cwd: (0, process_1.cwd)(),
234
287
  detached: true,
235
288
  stdio: 'inherit',
236
289
  });
237
- process.exit();
290
+ process.exit(0);
291
+ }, 100);
292
+ }
293
+ // Graceful shutdown handler
294
+ function handleShutdown(signal) {
295
+ log(`[PROXY] → [SYSTEM] Received ${signal}, shutting down gracefully...`);
296
+ // Close all cloud connections
297
+ gCloudConnections.forEach(ctx => ctx.cleanupSockets());
298
+ gCloudConnections.splice(0, gCloudConnections.length);
299
+ log(`[PROXY] → [SYSTEM] All connections closed. Exiting.`);
300
+ if (config.kind === "solo") {
301
+ log(`[PROXY] → [SYSTEM] Restarting proxy in solo mode...`);
302
+ restart();
238
303
  }
239
- // check if it's a heartbeat request from the server
240
- isHeartbeatRequest(data) {
241
- // heartbeatKind = (poll = 1, poll_old = 2, serve = 3)
242
- if (typeof data === 'string') {
243
- return data === "[215,3]";
244
- }
245
- else {
246
- return ((data[0] === 91) && // [
247
- (data[1] === 50) && // 2
248
- (data[2] === 49) && // 1
249
- (data[3] === 53) && // 5
250
- (data[4] === 44) && // ,
251
- (data[5] === 51) && // 3
252
- (data[6] === 93)); // ]
253
- }
304
+ else {
305
+ log(`[PROXY] → [SYSTEM] Exiting without restart.`);
306
+ setTimeout(() => process.exit(0), 100);
254
307
  }
255
308
  }
256
- exports.Context = Context;
309
+ // Listen for termination signals
310
+ process.on('SIGINT', () => handleShutdown('SIGINT'));
311
+ process.on('SIGTERM', () => handleShutdown('SIGTERM'));
312
+ process.on('SIGINT', (err) => {
313
+ error(`[SYSTEM] received SIGINT`);
314
+ handleShutdown('SIGINT');
315
+ });
316
+ process.on('SIGTERM', (err) => {
317
+ error(`[SYSTEM] received SIGTERM`);
318
+ handleShutdown('SIGTERM');
319
+ });
320
+ process.on('uncaughtException', (err) => {
321
+ error(`[SYSTEM] Uncaught Exception: ${err.message}`);
322
+ handleShutdown('uncaughtException');
323
+ });
324
+ process.on('unhandledRejection', (reason) => {
325
+ error(`[SYSTEM] Unhandled Rejection: ${reason}`);
326
+ handleShutdown('unhandledRejection');
327
+ });
257
328
  //# sourceMappingURL=proxy.js.map
package/server/proxy.ts CHANGED
@@ -1,10 +1,9 @@
1
1
  import * as net from 'net';
2
- import * as WebSocket from 'ws';
3
2
  import { spawn } from 'child_process';
4
3
  import { argv, cwd, execPath } from 'process';
5
4
 
6
5
  //
7
- // [ device (TCP) ] ←→ [ Proxy = WebSocket client (ws) ] ←→ [ Cloud = 2 x WebSocket server (ws) ] ←→ [ browser client ]
6
+ // [ device (TCP) ] ←→ [ Proxy = TCP client ] ←→ [ Cloud = TCP server ] ←→ [ browser client ]
8
7
  //
9
8
 
10
9
  export interface ProxyConfig {
@@ -16,6 +15,7 @@ export interface ProxyConfig {
16
15
  debug?: boolean; // debug mode
17
16
  kind?: "pm2" | "gw" | "solo";
18
17
  }
18
+
19
19
  export const kEmptyProxy: ProxyConfig = {
20
20
  cloudServer: "ws.duotecno.eu",
21
21
  cloudPort: 5098,
@@ -44,12 +44,14 @@ export function setProxyConfig(c?: ProxyConfig): ProxyConfig {
44
44
 
45
45
  let config: ProxyConfig;
46
46
  try {
47
- setProxyConfig(require('../config-proxy.json'))
47
+ setProxyConfig(require('../config-proxy.json'));
48
48
  } catch (e) {
49
49
  setProxyConfig();
50
50
  console.log("No config-proxy.json found, using default values from homebridge or other.");
51
51
  }
52
52
 
53
+
54
+
53
55
  const gCloudConnections: Array<Context> = [];
54
56
  const kDebug = config.debug || false; // enable debug mode from config
55
57
 
@@ -75,42 +77,47 @@ function error(str: string) {
75
77
  ///////////////////////////////
76
78
  // Start up the proxy server //
77
79
  ///////////////////////////////
80
+ if (config.uniqueId) {
81
+ makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
82
+ }
78
83
 
79
- // if (config.kind != "gw") {
80
- // makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
81
- // }
82
-
83
-
84
- export function makeNewCloudConnection(master: string, masterPort: number, server: string, serverPort: number, uniqueId: string) {
85
- const cloudSocket = new WebSocket('ws://'+server+':'+serverPort+"/"+uniqueId);
86
84
 
87
- cloudSocket.on('open', () => {
85
+ export function makeNewCloudConnection(master: string, masterPort: number, server: string, serverPort: number, uniqueId: string, count = 0) {
86
+ const cloudSocket = new net.Socket();
87
+
88
+ cloudSocket.connect(serverPort, server, () => {
88
89
  log(`[PROXY] → [CLOUD] Connected to Cloud at ${server}:${serverPort}`);
90
+
91
+ // Send unique ID as the first message to identify this proxy/device
92
+ cloudSocket.write(`[${uniqueId}]`);
93
+ debug(`[PROXY] → [CLOUD] Sent unique ID: ${uniqueId}`);
89
94
 
90
95
  const context = new Context(cloudSocket, master, masterPort, server, serverPort, uniqueId);
91
96
  gCloudConnections.push(context);
92
97
  log(`[PROXY] → [CLOUD] New free connection #${gCloudConnections.length} to the Cloud at ${server}:${serverPort}`);
93
98
  });
94
99
 
95
- log(`[PROXY] → [CLOUD] Attempting to start a new free connection for ${config.uniqueId} to the Cloud at ${server}:${serverPort}`);
100
+ cloudSocket.on('error', (err) => {
101
+ error(`[PROXY] → [CLOUD] Failed to connect ${count+1} times to the Cloud server: ${err.message}`);
102
+ if (count < 3) {
103
+ setTimeout(() => {
104
+ makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId, count + 1);
105
+ }, 1000);
106
+ }
107
+ });
108
+
109
+ log(`[PROXY] → [CLOUD] Attempt ${count+1} to start a new free connection for ${config.uniqueId} to the Cloud at ${server}:${serverPort}`);
96
110
  }
97
111
 
112
+
98
113
  ////////////////
99
114
  // Restarting //
100
115
  ////////////////
101
116
  export function cleanStart(restart: boolean = false) {
102
- if (gCloudConnections.length) {
103
- gCloudConnections.forEach(context => {
104
- if (context.deviceSocket) {
105
- context.deviceSocket.end();
106
- context.deviceSocket = null;
107
- }
108
- context.cloudSocket.close();
109
- context.cloudSocket = null;
110
- });
111
- gCloudConnections.splice(0, gCloudConnections.length); // clear the array
112
- log(`[PROXY] → [CLOUD] Cleaned up all cloud connections.`);
113
- }
117
+ gCloudConnections.forEach(ctx => ctx.cleanupSockets());
118
+ log(`[PROXY] → [CLOUD] Cleaned up all ${gCloudConnections.length} cloud connections.`);
119
+
120
+ gCloudConnections.splice(0, gCloudConnections.length); // clear the array
114
121
 
115
122
  if (restart) {
116
123
  if (config.uniqueId) {
@@ -125,18 +132,18 @@ export function cleanStart(restart: boolean = false) {
125
132
 
126
133
 
127
134
  export class Context {
128
- deviceSocket: net.Socket;
135
+ deviceSocket: net.Socket | null;
129
136
  master: string;
130
137
  masterPort: number;
131
138
 
132
- cloudSocket: WebSocket;
139
+ cloudSocket: net.Socket | null;
133
140
  server: string;
134
141
  serverPort: number;
135
142
 
136
143
  // unique identifier for this device-gateway combination
137
144
  uniqueId: string;
138
145
 
139
- constructor(cloudSocket: WebSocket, master: string, masterPort: number, server: string, serverPort: number, uniqueId: string) {
146
+ constructor(cloudSocket: net.Socket, master: string, masterPort: number, server: string, serverPort: number, uniqueId: string) {
140
147
  this.deviceSocket = null;
141
148
  this.master = master;
142
149
  this.masterPort = masterPort;
@@ -152,12 +159,12 @@ export class Context {
152
159
 
153
160
  setupCloudSocket() {
154
161
  // set up handlers for the cloud socket
155
- this.cloudSocket.on('message', message => {
156
- this.handleDataFromCloud(message as Buffer);
162
+ this.cloudSocket.on('data', (data: Buffer) => {
163
+ this.handleDataFromCloud(data);
157
164
  });
158
165
 
159
- this.cloudSocket.on('close', (code, reason) => {
160
- warning('[CLOUD] Connection closed: ' + code + ' ' + reason);
166
+ this.cloudSocket.on('close', () => {
167
+ warning('[CLOUD] Connection closed');
161
168
  if (this.deviceSocket) {
162
169
  this.deviceSocket.end();
163
170
  this.deviceSocket = null;
@@ -174,29 +181,34 @@ export class Context {
174
181
  }
175
182
 
176
183
  handleDataFromCloud(data: Buffer) {
177
- debug(`[CLOUD] → [PROXY]: Data from Cloud: ${data.toString().slice(0, -1)}`);
184
+ debug(`[CLOUD] → [PROXY]: Data from Cloud: ${data.toString()}`);
178
185
 
179
186
  if (!this.deviceSocket) {
180
187
  // we either have a real client that wants to connect to the device,
181
188
  // or we receive a heartbeat from the cloud server
189
+ // or we receive a connection response [OK], [OK-=2], [ERROR-...]
182
190
 
183
191
  if (this.isHeartbeatRequest(data)) {
184
192
  debug(`[CLOUD] → [PROXY]: Received heartbeat from Cloud, returning response.`);
185
193
  // respond to the heartbeat of the cloud server
186
- this.cloudSocket.send("[72,3]"); // command([Command.HeartbeatStatus, CommandMethod.Heartbeat.server])
194
+ this.cloudSocket.write("[72,3]"); // command([Command.HeartbeatStatus, CommandMethod.Heartbeat.server])
187
195
 
196
+ } else if (this.isConnectionResponse(data)) {
197
+ debug(`[CLOUD] → [PROXY]: Received connection response from Cloud: ${data.toString()}`);
198
+ // Handle connection response from the cloud server
199
+
188
200
  } else {
189
- // There is incomming data, it's a fresh connection, so: a new client wants to connect to the device
201
+ // There is real incomming data, it's a fresh connection, so: a new client wants to connect to the device
190
202
  // 1) create a new (free) connection to the cloud server for the next client
191
203
  // 2) connect to the device and send the initial data
192
- log(`[PROXY] → [CLOUD] New client connection, creating new free connection for this proxy/device: ${this.uniqueId}.`);
204
+ log(`[PROXY] → [CLOUD] New client connection, creating new free connection for this proxy/device: ${config.uniqueId}.`);
193
205
  makeNewCloudConnection(this.master, this.masterPort, this.server, this.serverPort, this.uniqueId);
194
206
  this.makeDeviceConnection(this.master, this.masterPort, data);
195
207
  }
196
208
 
197
209
  } else if (this.deviceSocket.readyState === 'open') {
198
210
  this.deviceSocket.write(data);
199
- debug(`[PROXY] → [DEVICE] forwarding data to device: ${data.toString().slice(0, -1)}`);
211
+ debug(`[PROXY] → [DEVICE] forwarding data to device: ${data.toString().trim()}`);
200
212
 
201
213
  } else {
202
214
  warning(`[PROXY] → [DEVICE] Device socket is not open yet, waiting for connection... what to do with the data??`);
@@ -212,11 +224,17 @@ export class Context {
212
224
 
213
225
  // Connect to local device and send the data that came in from the cloud
214
226
  this.deviceSocket.connect(port, address, () => {
227
+ // Check if socket is still valid (not cleaned up by removeConnection)
228
+ if (!this.deviceSocket) {
229
+ warning(`[PROXY] → [DEVICE] Device socket was cleaned up before connection completed`);
230
+ return;
231
+ }
232
+
215
233
  log(`[PROXY] → [DEVICE] Connected to local device at ${address}:${port}`);
216
234
 
217
235
  this.setUpDeviceSocket();
218
-
219
- log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().slice(0, -1)}`);
236
+
237
+ log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().trim()}`);
220
238
  this.deviceSocket.write(data);
221
239
  });
222
240
 
@@ -230,10 +248,15 @@ export class Context {
230
248
  }
231
249
 
232
250
  setUpDeviceSocket() {
251
+ if (!this.deviceSocket) {
252
+ error(`[PROXY] → [DEVICE] Cannot set up device socket - socket is null`);
253
+ return;
254
+ }
255
+
233
256
  this.deviceSocket.on('data', (data: Buffer) => {
234
257
  const message = data.toString('utf-8');
235
- debug(`[DEVICE] → [PROXY] forwarding data to Cloud: ${message.slice(0, -1)}`);
236
- this.cloudSocket.send(message);
258
+ debug(`[DEVICE] → [PROXY] forwarding data to Cloud: ${message.trim()}`);
259
+ this.cloudSocket.write(data);
237
260
  });
238
261
 
239
262
  this.deviceSocket.on('close', () => {
@@ -247,30 +270,46 @@ export class Context {
247
270
  });
248
271
  }
249
272
 
250
- removeConnection() {
251
- this.deviceSocket = null;
273
+ cleanupSockets() {
274
+ // Clean up the device socket
275
+ if (this.deviceSocket) {
276
+ if (!this.deviceSocket.destroyed) {
277
+ this.deviceSocket.removeAllListeners();
278
+ this.deviceSocket.end();
279
+ this.deviceSocket.destroy();
280
+ }
281
+ this.deviceSocket = null;
282
+ }
252
283
 
253
- // remove this context and close the cloud socket
254
- const index = gCloudConnections.indexOf(this);
255
- if (index !== -1) {
256
- this.cloudSocket.close();
284
+ // Clean up the cloud socket
285
+ if (this.cloudSocket) {
286
+ if (!this.cloudSocket.destroyed) {
287
+ this.cloudSocket.removeAllListeners();
288
+ this.cloudSocket.end();
289
+ this.cloudSocket.destroy();
290
+ }
257
291
  this.cloudSocket = null;
258
- gCloudConnections.splice(index, 1);
259
- log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
260
292
  }
293
+ }
294
+
295
+ removeConnection() {
296
+ const index = gCloudConnections.indexOf(this);
297
+ if (index !== -1) gCloudConnections.splice(index, 1);
298
+
299
+ this.cleanupSockets();
300
+ log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
261
301
 
262
302
  if (gCloudConnections.length === 0) {
263
303
  log(`[PROXY] → [CLOUD] No more cloud connections, restarting proxy.`);
264
304
  // just to be sure: restart...
265
-
266
305
  if (config.kind === "pm2") {
267
306
  // PM2 should restart us.
268
307
  log(`[PROXY] → [CLOUD] PM2 should restart us, exiting now.`);
269
- process.exit();
308
+ setTimeout(() => process.exit(0), 100);
270
309
 
271
310
  } else if (config.kind === "solo"){
272
311
  log(`[PROXY] → [CLOUD] Running solo, restarting proxy.`);
273
- this.restart();
312
+ restart();
274
313
 
275
314
  } else {
276
315
  log(`[PROXY] → [CLOUD] Running under gateway/homebridge, not restarting... not sure what to do here.`);
@@ -283,33 +322,84 @@ export class Context {
283
322
  // Utilities //
284
323
  ///////////////
285
324
 
286
- restart() {
287
- console.log('🔄 Restarting...');
325
+ // check if it's a heartbeat request from the server
326
+ isHeartbeatRequest(data: Buffer): boolean {
327
+ const msg = data.toString('utf-8').trim();
328
+ return msg === '[215,3]';
329
+ // hearbeatKind = (poll = 1, poll_old = 2, serve = 3)
330
+ // return ((data[0] === 91) && // [
331
+ // (data[1] === 50) && // 2
332
+ // (data[2] === 49) && // 1
333
+ // (data[3] === 53) && // 5
334
+ // (data[4] === 44) && // ,
335
+ // (data[5] === 51) && // 3
336
+ // (data[6] === 93)); // ]
337
+ }
338
+
339
+ // check if it's a server response after connecting and sending our uniqueID
340
+ isConnectionResponse(data: Buffer): boolean {
341
+ const message = data.toString();
342
+
343
+ // Check for various server response patterns
344
+ return (message.startsWith('[OK')) || (message.startsWith('[ERROR'));
345
+ }
346
+ }
347
+
348
+ function restart() {
349
+ console.log('🔄 Restarting...');
288
350
 
351
+ setTimeout(() => {
289
352
  spawn(execPath, argv.slice(1), {
290
353
  cwd: cwd(),
291
354
  detached: true,
292
355
  stdio: 'inherit',
293
356
  });
294
357
 
295
- process.exit();
296
- }
358
+ process.exit(0);
359
+ }, 100);
360
+ }
297
361
 
298
- // check if it's a heartbeat request from the server
299
- isHeartbeatRequest(data: Buffer): boolean {
300
- // heartbeatKind = (poll = 1, poll_old = 2, serve = 3)
301
362
 
302
- if (typeof data === 'string') {
303
- return data === "[215,3]";
363
+ // Graceful shutdown handler
364
+ function handleShutdown(signal: string) {
365
+ log(`[PROXY] → [SYSTEM] Received ${signal}, shutting down gracefully...`);
304
366
 
305
- } else {
306
- return ((data[0] === 91) && // [
307
- (data[1] === 50) && // 2
308
- (data[2] === 49) && // 1
309
- (data[3] === 53) && // 5
310
- (data[4] === 44) && // ,
311
- (data[5] === 51) && // 3
312
- (data[6] === 93)); // ]
313
- }
367
+ // Close all cloud connections
368
+ gCloudConnections.forEach(ctx => ctx.cleanupSockets());
369
+ gCloudConnections.splice(0, gCloudConnections.length);
370
+
371
+ log(`[PROXY] → [SYSTEM] All connections closed. Exiting.`);
372
+
373
+ if (config.kind === "solo") {
374
+ log(`[PROXY] → [SYSTEM] Restarting proxy in solo mode...`);
375
+ restart();
376
+ } else {
377
+ log(`[PROXY] → [SYSTEM] Exiting without restart.`);
378
+ setTimeout(() => process.exit(0), 100);
314
379
  }
315
380
  }
381
+
382
+ // Listen for termination signals
383
+ process.on('SIGINT', () => handleShutdown('SIGINT'));
384
+ process.on('SIGTERM', () => handleShutdown('SIGTERM'));
385
+
386
+
387
+ process.on('SIGINT', (err) => {
388
+ error(`[SYSTEM] received SIGINT`);
389
+ handleShutdown('SIGINT');
390
+ });
391
+
392
+ process.on('SIGTERM', (err) => {
393
+ error(`[SYSTEM] received SIGTERM`);
394
+ handleShutdown('SIGTERM');
395
+ });
396
+
397
+ process.on('uncaughtException', (err) => {
398
+ error(`[SYSTEM] Uncaught Exception: ${err.message}`);
399
+ handleShutdown('uncaughtException');
400
+ });
401
+
402
+ process.on('unhandledRejection', (reason) => {
403
+ error(`[SYSTEM] Unhandled Rejection: ${reason}`);
404
+ handleShutdown('unhandledRejection');
405
+ });