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 +2 -0
- package/config.json +2 -2
- package/package.json +2 -2
- package/server/proxy.js +131 -60
- package/server/proxy.ts +159 -69
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":
|
|
464
|
+
"cloudPort": 5097,
|
|
465
465
|
"masterAddress": "192.168.0.97",
|
|
466
466
|
"masterPort": 5001,
|
|
467
467
|
"uniqueId": "",
|
|
468
468
|
"debug": false,
|
|
469
|
-
"kind": "
|
|
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.
|
|
5
|
-
"description": "SmartServer (Proxy
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
function makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId) {
|
|
68
|
-
const cloudSocket = new
|
|
69
|
-
cloudSocket.
|
|
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
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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('
|
|
121
|
-
this.handleDataFromCloud(
|
|
121
|
+
this.cloudSocket.on('data', (data) => {
|
|
122
|
+
this.handleDataFromCloud(data);
|
|
122
123
|
});
|
|
123
|
-
this.cloudSocket.on('close', (
|
|
124
|
-
warning('[CLOUD] Connection closed
|
|
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()
|
|
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.
|
|
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: ${
|
|
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().
|
|
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().
|
|
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.
|
|
189
|
-
this.cloudSocket.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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:
|
|
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:
|
|
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('
|
|
156
|
-
this.handleDataFromCloud(
|
|
162
|
+
this.cloudSocket.on('data', (data: Buffer) => {
|
|
163
|
+
this.handleDataFromCloud(data);
|
|
157
164
|
});
|
|
158
165
|
|
|
159
|
-
this.cloudSocket.on('close', (
|
|
160
|
-
warning('[CLOUD] Connection closed
|
|
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()
|
|
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.
|
|
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: ${
|
|
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().
|
|
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().
|
|
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.
|
|
236
|
-
this.cloudSocket.
|
|
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
|
-
|
|
251
|
-
|
|
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
|
-
//
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
287
|
-
|
|
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
|
-
|
|
303
|
-
|
|
363
|
+
// Graceful shutdown handler
|
|
364
|
+
function handleShutdown(signal: string) {
|
|
365
|
+
log(`[PROXY] → [SYSTEM] Received ${signal}, shutting down gracefully...`);
|
|
304
366
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
+
});
|