homebridge-smartsystem 6.7.4 → 6.8.0

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
@@ -301,6 +301,9 @@ Homebridge UI version
301
301
  - 3: do dimmers/up-downs
302
302
  - 4: better up-down values to and from HB
303
303
 
304
+ ### v6.8.0 - 08/07/2025
305
+ - 0: first version including the proxy
306
+
304
307
  ## Hardware
305
308
  A Raspberry Pi that connects to a Duotecno IP Node
306
309
  and (if configured) to a Smappee Infinity (power and plugs)
package/config.json CHANGED
@@ -480,5 +480,14 @@
480
480
  "gateway2": ""
481
481
  }
482
482
  },
483
- "p1": false
483
+ "p1": false,
484
+ "proxy": {
485
+ "cloudServer": "ws.duotecno.eu",
486
+ "cloudPort": 5098,
487
+ "masterAddress": "192.168.0.97",
488
+ "masterPort": 5001,
489
+ "uniqueId": "",
490
+ "debug": false,
491
+ "kind": "solo"
492
+ }
484
493
  }
package/duotecno/types.js CHANGED
@@ -15,6 +15,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
15
15
  };
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
17
  exports.makeInt = exports.now = exports.datetime = exports.time = exports.date = exports.watt = exports.hex = exports.two = exports.char = exports.ascii = exports.wait = exports.Sanitizers = exports.actionValueStr = exports.actionValue = exports.kEmptyBinding = exports.kEmptyLink = exports.LinkType = exports.kEmptySwitch = exports.SwitchType = exports.kEmptyRule = exports.kEmptyAction = exports.RuleType = exports.Boundaries = exports.kEmptyCommRecord = exports.WriteError = exports.kEmptySettings = exports.kEmptyNetworkConfig = exports.kEmptyGroup = exports.kEmptyScene = exports.kEmptyUnitScene = exports.kEmptyUnit = exports.UnitExtendedType = exports.UnitType = exports.UnitMotorCmd = exports.UnitState = exports.NodeType = void 0;
18
+ const proxy_1 = require("../server/proxy");
18
19
  // Node types
19
20
  var NodeType;
20
21
  (function (NodeType) {
@@ -204,6 +205,7 @@ exports.Sanitizers = {
204
205
  config.shelly = config.shelly || false;
205
206
  config.smartapp = config.smartapp || null;
206
207
  config.system = exports.Sanitizers.system(config.system);
208
+ config.proxy = exports.Sanitizers.proxy(config.proxy, config.system);
207
209
  return config;
208
210
  },
209
211
  homebridge: function (config) {
@@ -358,6 +360,19 @@ exports.Sanitizers = {
358
360
  b.channel = binding.channel || "0";
359
361
  return b;
360
362
  },
363
+ ///////////
364
+ // Proxy //
365
+ ///////////
366
+ proxy: function (config, system) {
367
+ var _a, _b;
368
+ const c = proxy_1.kEmptyProxy;
369
+ c.cloudServer = (config === null || config === void 0 ? void 0 : config.cloudServer) || proxy_1.kEmptyProxy.cloudServer;
370
+ c.cloudPort = (config === null || config === void 0 ? void 0 : config.cloudPort) || proxy_1.kEmptyProxy.cloudPort;
371
+ c.masterAddress = (config === null || config === void 0 ? void 0 : config.masterAddress) || ((_a = system === null || system === void 0 ? void 0 : system.cmasters[0]) === null || _a === void 0 ? void 0 : _a.address) || "";
372
+ c.masterPort = (config === null || config === void 0 ? void 0 : config.masterPort) || ((_b = system === null || system === void 0 ? void 0 : system.cmasters[0]) === null || _b === void 0 ? void 0 : _b.port) || 5001;
373
+ c.uniqueId = (config === null || config === void 0 ? void 0 : config.uniqueId) || proxy_1.kEmptyProxy.uniqueId;
374
+ return c;
375
+ },
361
376
  //////////////
362
377
  // Duotecno //
363
378
  //////////////
package/duotecno/types.ts CHANGED
@@ -4,6 +4,7 @@
4
4
  // v3 - smart server version, Mar 2020
5
5
  // v3.1 - added scenes from app version, May 2020
6
6
 
7
+ import { kEmptyProxy, ProxyConfig } from "../server/proxy";
7
8
  import { Unit } from "./protocol";
8
9
 
9
10
  // for Active=Y/N or New=X
@@ -412,6 +413,7 @@ export const Sanitizers = {
412
413
  config.shelly = config.shelly || false;
413
414
  config.smartapp = config.smartapp || null;
414
415
  config.system = Sanitizers.system(config.system);
416
+ config.proxy = Sanitizers.proxy(config.proxy, config.system);
415
417
 
416
418
  return config;
417
419
  },
@@ -581,6 +583,19 @@ export const Sanitizers = {
581
583
  return b;
582
584
  },
583
585
 
586
+ ///////////
587
+ // Proxy //
588
+ ///////////
589
+ proxy: function(config?: ProxyConfig, system?: SystemConfig): ProxyConfig {
590
+ const c = kEmptyProxy as ProxyConfig;
591
+ c.cloudServer = config?.cloudServer || kEmptyProxy.cloudServer;
592
+ c.cloudPort = config?.cloudPort || kEmptyProxy.cloudPort;
593
+ c.masterAddress = config?.masterAddress || system?.cmasters[0]?.address || "";
594
+ c.masterPort = config?.masterPort || system?.cmasters[0]?.port || 5001;
595
+ c.uniqueId = config?.uniqueId || kEmptyProxy.uniqueId;
596
+ return c;
597
+ },
598
+
584
599
  //////////////
585
600
  // Duotecno //
586
601
  //////////////
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-smartsystem",
3
3
  "displayname": "Duotecno Bridge",
4
- "version": "6.7.4",
4
+ "version": "6.8.0",
5
5
  "description": "SmartServer (Proxy Websockets to TCP sockets, Smappee MQTT, Duotecno IP Nodes, Homekit interface)",
6
6
  "main": "index.js",
7
7
  "author": "Johan Coppieters",
@@ -30,8 +30,10 @@ const p1_1 = require("./p1");
30
30
  const shelly_1 = require("./shelly");
31
31
  const config_1 = require("../duotecno/config");
32
32
  const fs_1 = require("fs");
33
+ const proxy_1 = require("./proxy");
33
34
  class Platform extends base_1.Base {
34
35
  constructor(logger, pConfig, api) {
36
+ var _a;
35
37
  super("homebridge");
36
38
  this.accessoryList = [];
37
39
  this.ready = false;
@@ -109,6 +111,15 @@ class Platform extends base_1.Base {
109
111
  else {
110
112
  (0, logger_1.log)("homebridge", "platform - No SocApp configured.");
111
113
  }
114
+ if (this.config.proxy && ((_a = this.config.proxy) === null || _a === void 0 ? void 0 : _a.uniqueId)) {
115
+ this.config.proxy.kind = "gw";
116
+ (0, proxy_1.setProxyConfig)(this.config.proxy);
117
+ (0, proxy_1.cleanStart)(true);
118
+ (0, logger_1.log)("homebridge", "platform - Proxy configured: " + JSON.stringify(this.config.proxy, null, 2));
119
+ }
120
+ else {
121
+ (0, logger_1.log)("homebridge", "platform - No Proxy configured.");
122
+ }
112
123
  }
113
124
  startPower(type, PowerMgr) {
114
125
  // startup a power handler, if configured
@@ -20,7 +20,8 @@ import { Lock } from "../accessories/lock";
20
20
  import { P1 } from "./p1";
21
21
  import { Shelly } from "./shelly";
22
22
  import { kConfigFiles, setConfig } from "../duotecno/config";
23
- import { fstat, readFileSync } from "fs";
23
+ import { readFileSync } from "fs";
24
+ import { cleanStart, setProxyConfig } from "./proxy";
24
25
 
25
26
 
26
27
  export class Platform extends Base {
@@ -115,6 +116,15 @@ export class Platform extends Base {
115
116
  } else {
116
117
  log("homebridge", "platform - No SocApp configured.");
117
118
  }
119
+
120
+ if (this.config.proxy && this.config.proxy?.uniqueId) {
121
+ this.config.proxy.kind = "gw";
122
+ setProxyConfig(this.config.proxy);
123
+ cleanStart(true);
124
+ log("homebridge", "platform - Proxy configured: " + JSON.stringify(this.config.proxy, null, 2));
125
+ } else {
126
+ log("homebridge", "platform - No Proxy configured.");
127
+ }
118
128
  }
119
129
 
120
130
  startPower(type: string, PowerMgr: any) {
@@ -0,0 +1,257 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Context = exports.cleanStart = exports.makeNewCloudConnection = exports.setProxyConfig = exports.kEmptyProxy = void 0;
4
+ const net = require("net");
5
+ const WebSocket = require("ws");
6
+ const child_process_1 = require("child_process");
7
+ const process_1 = require("process");
8
+ exports.kEmptyProxy = {
9
+ cloudServer: "ws.duotecno.eu",
10
+ cloudPort: 5098,
11
+ masterAddress: "",
12
+ masterPort: 5001,
13
+ uniqueId: "",
14
+ debug: false,
15
+ kind: "solo" // default running under PM2 or other process manager -> don't restart yourself
16
+ };
17
+ function setProxyConfig(c) {
18
+ // set the proxy config
19
+ if (c) {
20
+ config.cloudServer = c.cloudServer || exports.kEmptyProxy.cloudServer;
21
+ config.cloudPort = c.cloudPort || exports.kEmptyProxy.cloudPort;
22
+ config.masterAddress = c.masterAddress || exports.kEmptyProxy.masterAddress;
23
+ config.masterPort = c.masterPort || exports.kEmptyProxy.masterPort;
24
+ config.uniqueId = c.uniqueId || exports.kEmptyProxy.uniqueId;
25
+ config.debug = !!c.debug;
26
+ config.kind = c.kind || exports.kEmptyProxy.kind;
27
+ }
28
+ else {
29
+ config = Object.assign({}, exports.kEmptyProxy);
30
+ }
31
+ return config;
32
+ }
33
+ exports.setProxyConfig = setProxyConfig;
34
+ let config;
35
+ try {
36
+ setProxyConfig(require('../config-proxy.json'));
37
+ }
38
+ catch (e) {
39
+ setProxyConfig();
40
+ console.log("No config-proxy.json found, using default values from homebridge or other.");
41
+ }
42
+ const gCloudConnections = [];
43
+ const kDebug = config.debug || false; // enable debug mode from config
44
+ /////////////
45
+ // Logging //
46
+ /////////////
47
+ function debug(str) {
48
+ if (kDebug) {
49
+ console.log(`DEBUG: ${str}`);
50
+ }
51
+ }
52
+ function warning(str) {
53
+ console.warn(`WARNING: ${str}`);
54
+ }
55
+ function log(str) {
56
+ console.log(`LOG: ${str}`);
57
+ }
58
+ function error(str) {
59
+ console.error(`ERROR: **** ${str} ****`);
60
+ }
61
+ ///////////////////////////////
62
+ // Start up the proxy server //
63
+ ///////////////////////////////
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', () => {
70
+ log(`[PROXY] → [CLOUD] Connected to Cloud at ${server}:${serverPort}`);
71
+ const context = new Context(cloudSocket, master, masterPort, server, serverPort, uniqueId);
72
+ gCloudConnections.push(context);
73
+ log(`[PROXY] → [CLOUD] New free connection #${gCloudConnections.length} to the Cloud at ${server}:${serverPort}`);
74
+ });
75
+ log(`[PROXY] → [CLOUD] Attempting to start a new free connection for ${config.uniqueId} to the Cloud at ${server}:${serverPort}`);
76
+ }
77
+ exports.makeNewCloudConnection = makeNewCloudConnection;
78
+ ////////////////
79
+ // Restarting //
80
+ ////////////////
81
+ 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
+ }
94
+ if (restart) {
95
+ if (config.uniqueId) {
96
+ makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
97
+ }
98
+ else {
99
+ log(`[PROXY] → [CLOUD] Not restarting, no unique ID configured, not starting the proxy.`);
100
+ }
101
+ }
102
+ else {
103
+ log(`[PROXY] → [CLOUD] Not restarting, no new cloud connection, just cleaning up.`);
104
+ }
105
+ }
106
+ exports.cleanStart = cleanStart;
107
+ class Context {
108
+ constructor(cloudSocket, master, masterPort, server, serverPort, uniqueId) {
109
+ this.deviceSocket = null;
110
+ this.master = master;
111
+ this.masterPort = masterPort;
112
+ this.cloudSocket = cloudSocket;
113
+ this.server = server;
114
+ this.serverPort = serverPort;
115
+ this.uniqueId = uniqueId;
116
+ this.setupCloudSocket();
117
+ }
118
+ setupCloudSocket() {
119
+ // set up handlers for the cloud socket
120
+ this.cloudSocket.on('message', message => {
121
+ this.handleDataFromCloud(message);
122
+ });
123
+ this.cloudSocket.on('close', (code, reason) => {
124
+ warning('[CLOUD] Connection closed: ' + code + ' ' + reason);
125
+ if (this.deviceSocket) {
126
+ this.deviceSocket.end();
127
+ this.deviceSocket = null;
128
+ }
129
+ });
130
+ this.cloudSocket.on('error', err => {
131
+ error(`[CLOUD] Error: ${err.message}`);
132
+ if (this.deviceSocket) {
133
+ this.deviceSocket.end();
134
+ this.deviceSocket = null;
135
+ }
136
+ });
137
+ }
138
+ handleDataFromCloud(data) {
139
+ debug(`[CLOUD] → [PROXY]: Data from Cloud: ${data.toString().slice(0, -1)}`);
140
+ if (!this.deviceSocket) {
141
+ // we either have a real client that wants to connect to the device,
142
+ // or we receive a heartbeat from the cloud server
143
+ if (this.isHeartbeatRequest(data)) {
144
+ debug(`[CLOUD] → [PROXY]: Received heartbeat from Cloud, returning response.`);
145
+ // respond to the heartbeat of the cloud server
146
+ this.cloudSocket.send("[72,3]"); // command([Command.HeartbeatStatus, CommandMethod.Heartbeat.server])
147
+ }
148
+ else {
149
+ // There is incomming data, it's a fresh connection, so: a new client wants to connect to the device
150
+ // 1) create a new (free) connection to the cloud server for the next client
151
+ // 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}.`);
153
+ makeNewCloudConnection(this.master, this.masterPort, this.server, this.serverPort, this.uniqueId);
154
+ this.makeDeviceConnection(this.master, this.masterPort, data);
155
+ }
156
+ }
157
+ else if (this.deviceSocket.readyState === 'open') {
158
+ this.deviceSocket.write(data);
159
+ debug(`[PROXY] → [DEVICE] forwarding data to device: ${data.toString().slice(0, -1)}`);
160
+ }
161
+ else {
162
+ warning(`[PROXY] → [DEVICE] Device socket is not open yet, waiting for connection... what to do with the data??`);
163
+ }
164
+ }
165
+ makeDeviceConnection(address, port, data) {
166
+ log(`[PROXY] → [DEVICE] Attempting to connect to local device at ${address}:${port}`);
167
+ if (!this.deviceSocket) {
168
+ this.deviceSocket = new net.Socket();
169
+ // Connect to local device and send the data that came in from the cloud
170
+ this.deviceSocket.connect(port, address, () => {
171
+ log(`[PROXY] → [DEVICE] Connected to local device at ${address}:${port}`);
172
+ this.setUpDeviceSocket();
173
+ log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().slice(0, -1)}`);
174
+ this.deviceSocket.write(data);
175
+ });
176
+ }
177
+ else if (this.deviceSocket.readyState === 'open') {
178
+ warning(`[PROXY] → [DEVICE] Device socket already open, sending data directly -- STRANGE !!`);
179
+ this.deviceSocket.write(data);
180
+ }
181
+ else {
182
+ error(`[PROXY] → [DEVICE] Device socket exists, but is not open yet !!`);
183
+ }
184
+ }
185
+ setUpDeviceSocket() {
186
+ this.deviceSocket.on('data', (data) => {
187
+ const message = data.toString('utf-8');
188
+ debug(`[DEVICE] → [PROXY] forwarding data to Cloud: ${message.slice(0, -1)}`);
189
+ this.cloudSocket.send(message);
190
+ });
191
+ this.deviceSocket.on('close', () => {
192
+ log(`[DEVICE] → [PROXY] Device socket closed`);
193
+ this.removeConnection();
194
+ });
195
+ this.deviceSocket.on('error', (err) => {
196
+ error(`[DEVICE] → [PROXY] Device socket error: ${err.message}`);
197
+ this.removeConnection();
198
+ });
199
+ }
200
+ removeConnection() {
201
+ this.deviceSocket = null;
202
+ // remove this context and close the cloud socket
203
+ const index = gCloudConnections.indexOf(this);
204
+ if (index !== -1) {
205
+ this.cloudSocket.close();
206
+ this.cloudSocket = null;
207
+ gCloudConnections.splice(index, 1);
208
+ log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
209
+ }
210
+ if (gCloudConnections.length === 0) {
211
+ log(`[PROXY] → [CLOUD] No more cloud connections, restarting proxy.`);
212
+ // just to be sure: restart...
213
+ if (config.kind === "pm2") {
214
+ // PM2 should restart us.
215
+ log(`[PROXY] → [CLOUD] PM2 should restart us, exiting now.`);
216
+ process.exit();
217
+ }
218
+ else if (config.kind === "solo") {
219
+ log(`[PROXY] → [CLOUD] Running solo, restarting proxy.`);
220
+ this.restart();
221
+ }
222
+ else {
223
+ log(`[PROXY] → [CLOUD] Running under gateway/homebridge, not restarting... not sure what to do here.`);
224
+ }
225
+ }
226
+ }
227
+ ///////////////
228
+ // Utilities //
229
+ ///////////////
230
+ restart() {
231
+ console.log('🔄 Restarting...');
232
+ (0, child_process_1.spawn)(process_1.execPath, process_1.argv.slice(1), {
233
+ cwd: (0, process_1.cwd)(),
234
+ detached: true,
235
+ stdio: 'inherit',
236
+ });
237
+ process.exit();
238
+ }
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
+ }
254
+ }
255
+ }
256
+ exports.Context = Context;
257
+ //# sourceMappingURL=proxy.js.map
@@ -0,0 +1,315 @@
1
+ import * as net from 'net';
2
+ import * as WebSocket from 'ws';
3
+ import { spawn } from 'child_process';
4
+ import { argv, cwd, execPath } from 'process';
5
+
6
+ //
7
+ // [ device (TCP) ] ←→ [ Proxy = WebSocket client (ws) ] ←→ [ Cloud = 2 x WebSocket server (ws) ] ←→ [ browser client ]
8
+ //
9
+
10
+ export interface ProxyConfig {
11
+ cloudServer: string; // cloud server address
12
+ cloudPort: number; // cloud server port
13
+ masterAddress: string; // local master address
14
+ masterPort: number; // local master port
15
+ uniqueId: string; // unique ID for the device (for now the no-ip address + port)
16
+ debug?: boolean; // debug mode
17
+ kind?: "pm2" | "gw" | "solo";
18
+ }
19
+ export const kEmptyProxy: ProxyConfig = {
20
+ cloudServer: "ws.duotecno.eu",
21
+ cloudPort: 5098,
22
+ masterAddress: "",
23
+ masterPort: 5001,
24
+ uniqueId: "", // no unique ID -> don't start the proxy
25
+ debug: false, // default nodebug mode
26
+ kind: "solo" // default running under PM2 or other process manager -> don't restart yourself
27
+ }
28
+
29
+ export function setProxyConfig(c?: ProxyConfig): ProxyConfig {
30
+ // set the proxy config
31
+ if (c) {
32
+ config.cloudServer = c.cloudServer || kEmptyProxy.cloudServer;
33
+ config.cloudPort = c.cloudPort || kEmptyProxy.cloudPort;
34
+ config.masterAddress = c.masterAddress || kEmptyProxy.masterAddress;
35
+ config.masterPort = c.masterPort || kEmptyProxy.masterPort;
36
+ config.uniqueId = c.uniqueId || kEmptyProxy.uniqueId;
37
+ config.debug = !!c.debug;
38
+ config.kind = c.kind || kEmptyProxy.kind;
39
+ } else {
40
+ config = {...kEmptyProxy};
41
+ }
42
+ return config;
43
+ }
44
+
45
+ let config: ProxyConfig;
46
+ try {
47
+ setProxyConfig(require('../config-proxy.json'))
48
+ } catch (e) {
49
+ setProxyConfig();
50
+ console.log("No config-proxy.json found, using default values from homebridge or other.");
51
+ }
52
+
53
+ const gCloudConnections: Array<Context> = [];
54
+ const kDebug = config.debug || false; // enable debug mode from config
55
+
56
+ /////////////
57
+ // Logging //
58
+ /////////////
59
+ function debug(str: string) {
60
+ if (kDebug) {
61
+ console.log(`DEBUG: ${str}`);
62
+ }
63
+ }
64
+ function warning(str: string) {
65
+ console.warn(`WARNING: ${str}`);
66
+ }
67
+ function log(str: string) {
68
+ console.log(`LOG: ${str}`);
69
+ }
70
+ function error(str: string) {
71
+ console.error(`ERROR: **** ${str} ****`);
72
+ }
73
+
74
+
75
+ ///////////////////////////////
76
+ // Start up the proxy server //
77
+ ///////////////////////////////
78
+
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
+
87
+ cloudSocket.on('open', () => {
88
+ log(`[PROXY] → [CLOUD] Connected to Cloud at ${server}:${serverPort}`);
89
+
90
+ const context = new Context(cloudSocket, master, masterPort, server, serverPort, uniqueId);
91
+ gCloudConnections.push(context);
92
+ log(`[PROXY] → [CLOUD] New free connection #${gCloudConnections.length} to the Cloud at ${server}:${serverPort}`);
93
+ });
94
+
95
+ log(`[PROXY] → [CLOUD] Attempting to start a new free connection for ${config.uniqueId} to the Cloud at ${server}:${serverPort}`);
96
+ }
97
+
98
+ ////////////////
99
+ // Restarting //
100
+ ////////////////
101
+ 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
+ }
114
+
115
+ if (restart) {
116
+ if (config.uniqueId) {
117
+ makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
118
+ } else {
119
+ log(`[PROXY] → [CLOUD] Not restarting, no unique ID configured, not starting the proxy.`);
120
+ }
121
+ } else {
122
+ log(`[PROXY] → [CLOUD] Not restarting, no new cloud connection, just cleaning up.`);
123
+ }
124
+ }
125
+
126
+
127
+ export class Context {
128
+ deviceSocket: net.Socket;
129
+ master: string;
130
+ masterPort: number;
131
+
132
+ cloudSocket: WebSocket;
133
+ server: string;
134
+ serverPort: number;
135
+
136
+ // unique identifier for this device-gateway combination
137
+ uniqueId: string;
138
+
139
+ constructor(cloudSocket: WebSocket, master: string, masterPort: number, server: string, serverPort: number, uniqueId: string) {
140
+ this.deviceSocket = null;
141
+ this.master = master;
142
+ this.masterPort = masterPort;
143
+
144
+ this.cloudSocket = cloudSocket;
145
+ this.server = server;
146
+ this.serverPort = serverPort;
147
+
148
+ this.uniqueId = uniqueId;
149
+
150
+ this.setupCloudSocket();
151
+ }
152
+
153
+ setupCloudSocket() {
154
+ // set up handlers for the cloud socket
155
+ this.cloudSocket.on('message', message => {
156
+ this.handleDataFromCloud(message as Buffer);
157
+ });
158
+
159
+ this.cloudSocket.on('close', (code, reason) => {
160
+ warning('[CLOUD] Connection closed: ' + code + ' ' + reason);
161
+ if (this.deviceSocket) {
162
+ this.deviceSocket.end();
163
+ this.deviceSocket = null;
164
+ }
165
+ });
166
+
167
+ this.cloudSocket.on('error', err => {
168
+ error(`[CLOUD] Error: ${err.message}`);
169
+ if (this.deviceSocket) {
170
+ this.deviceSocket.end();
171
+ this.deviceSocket = null;
172
+ }
173
+ });
174
+ }
175
+
176
+ handleDataFromCloud(data: Buffer) {
177
+ debug(`[CLOUD] → [PROXY]: Data from Cloud: ${data.toString().slice(0, -1)}`);
178
+
179
+ if (!this.deviceSocket) {
180
+ // we either have a real client that wants to connect to the device,
181
+ // or we receive a heartbeat from the cloud server
182
+
183
+ if (this.isHeartbeatRequest(data)) {
184
+ debug(`[CLOUD] → [PROXY]: Received heartbeat from Cloud, returning response.`);
185
+ // respond to the heartbeat of the cloud server
186
+ this.cloudSocket.send("[72,3]"); // command([Command.HeartbeatStatus, CommandMethod.Heartbeat.server])
187
+
188
+ } else {
189
+ // There is incomming data, it's a fresh connection, so: a new client wants to connect to the device
190
+ // 1) create a new (free) connection to the cloud server for the next client
191
+ // 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}.`);
193
+ makeNewCloudConnection(this.master, this.masterPort, this.server, this.serverPort, this.uniqueId);
194
+ this.makeDeviceConnection(this.master, this.masterPort, data);
195
+ }
196
+
197
+ } else if (this.deviceSocket.readyState === 'open') {
198
+ this.deviceSocket.write(data);
199
+ debug(`[PROXY] → [DEVICE] forwarding data to device: ${data.toString().slice(0, -1)}`);
200
+
201
+ } else {
202
+ warning(`[PROXY] → [DEVICE] Device socket is not open yet, waiting for connection... what to do with the data??`);
203
+ }
204
+ }
205
+
206
+
207
+ makeDeviceConnection(address: string, port: number, data: Buffer) {
208
+ log(`[PROXY] → [DEVICE] Attempting to connect to local device at ${address}:${port}`);
209
+
210
+ if (!this.deviceSocket) {
211
+ this.deviceSocket = new net.Socket();
212
+
213
+ // Connect to local device and send the data that came in from the cloud
214
+ this.deviceSocket.connect(port, address, () => {
215
+ log(`[PROXY] → [DEVICE] Connected to local device at ${address}:${port}`);
216
+
217
+ this.setUpDeviceSocket();
218
+
219
+ log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().slice(0, -1)}`);
220
+ this.deviceSocket.write(data);
221
+ });
222
+
223
+ } else if (this.deviceSocket.readyState === 'open') {
224
+ warning(`[PROXY] → [DEVICE] Device socket already open, sending data directly -- STRANGE !!`);
225
+ this.deviceSocket.write(data);
226
+
227
+ } else {
228
+ error(`[PROXY] → [DEVICE] Device socket exists, but is not open yet !!`);
229
+ }
230
+ }
231
+
232
+ setUpDeviceSocket() {
233
+ this.deviceSocket.on('data', (data: Buffer) => {
234
+ const message = data.toString('utf-8');
235
+ debug(`[DEVICE] → [PROXY] forwarding data to Cloud: ${message.slice(0, -1)}`);
236
+ this.cloudSocket.send(message);
237
+ });
238
+
239
+ this.deviceSocket.on('close', () => {
240
+ log(`[DEVICE] → [PROXY] Device socket closed`);
241
+ this.removeConnection()
242
+ });
243
+
244
+ this.deviceSocket.on('error', (err) => {
245
+ error(`[DEVICE] → [PROXY] Device socket error: ${err.message}`);
246
+ this.removeConnection()
247
+ });
248
+ }
249
+
250
+ removeConnection() {
251
+ this.deviceSocket = null;
252
+
253
+ // remove this context and close the cloud socket
254
+ const index = gCloudConnections.indexOf(this);
255
+ if (index !== -1) {
256
+ this.cloudSocket.close();
257
+ this.cloudSocket = null;
258
+ gCloudConnections.splice(index, 1);
259
+ log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
260
+ }
261
+
262
+ if (gCloudConnections.length === 0) {
263
+ log(`[PROXY] → [CLOUD] No more cloud connections, restarting proxy.`);
264
+ // just to be sure: restart...
265
+
266
+ if (config.kind === "pm2") {
267
+ // PM2 should restart us.
268
+ log(`[PROXY] → [CLOUD] PM2 should restart us, exiting now.`);
269
+ process.exit();
270
+
271
+ } else if (config.kind === "solo"){
272
+ log(`[PROXY] → [CLOUD] Running solo, restarting proxy.`);
273
+ this.restart();
274
+
275
+ } else {
276
+ log(`[PROXY] → [CLOUD] Running under gateway/homebridge, not restarting... not sure what to do here.`);
277
+ }
278
+ }
279
+ }
280
+
281
+
282
+ ///////////////
283
+ // Utilities //
284
+ ///////////////
285
+
286
+ restart() {
287
+ console.log('🔄 Restarting...');
288
+
289
+ spawn(execPath, argv.slice(1), {
290
+ cwd: cwd(),
291
+ detached: true,
292
+ stdio: 'inherit',
293
+ });
294
+
295
+ process.exit();
296
+ }
297
+
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
+
302
+ if (typeof data === 'string') {
303
+ return data === "[215,3]";
304
+
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
+ }
314
+ }
315
+ }
@@ -28,6 +28,7 @@ const fs_1 = require("fs");
28
28
  const config_1 = require("../duotecno/config");
29
29
  const HB_1 = require("./HB");
30
30
  const HA_API_1 = require("./HA-API");
31
+ const proxy_1 = require("./proxy");
31
32
  const kMaster = { name: "master", type: "string", default: "0.0.0.0:5001" };
32
33
  const kAddress = { name: "address", type: "string", default: "0.0.0.0" };
33
34
  const kAddresses = { name: "addresses", type: "string", default: "[0.0.0.0]" };
@@ -81,6 +82,7 @@ class SmartApp extends webapp_1.WebApp {
81
82
  this.addFile("materializeCSS", __dirname + "/views/assets/materialize.min.css", "text/css");
82
83
  this.addFile("materializeJS", __dirname + "/views/assets/materialize.min.js", "text/javascript");
83
84
  this.addFile("favicon", __dirname + "/views/assets/favicon.ico", "image/x-icon");
85
+ this.addFile("proxy", __dirname + "/views/proxy.ejs", "text/html");
84
86
  }
85
87
  writeConfig() {
86
88
  // copy switches into config, eliminate the runtime stuff (like unit)
@@ -185,6 +187,9 @@ class SmartApp extends webapp_1.WebApp {
185
187
  else if (context.request === "settings") {
186
188
  return this.doSettings(context);
187
189
  }
190
+ else if (context.request === "proxy") {
191
+ return this.doProxy(context);
192
+ }
188
193
  else {
189
194
  return _super.doRequest.call(this, context);
190
195
  }
@@ -225,6 +230,31 @@ class SmartApp extends webapp_1.WebApp {
225
230
  return { name, value, masterAddress: context["masterAddress"], masterPort: context["masterPort"],
226
231
  logicalAddress, logicalNodeAddress };
227
232
  }
233
+ ///////////
234
+ // Proxy //
235
+ ///////////
236
+ doProxy(context) {
237
+ return __awaiter(this, void 0, void 0, function* () {
238
+ let config = this.config.proxy || proxy_1.kEmptyProxy;
239
+ if (context.action === "save") {
240
+ // save the proxy config
241
+ config.cloudServer = context.getParam({ name: "cloudServer", type: "string" });
242
+ config.cloudPort = context.getParam({ name: "cloudPort", type: "integer" });
243
+ config.masterAddress = context.getParam({ name: "masterAddress", type: "string" });
244
+ config.masterPort = context.getParam({ name: "masterPort", type: "integer" });
245
+ config.uniqueId = context.getParam({ name: "uniqueId", type: "string" });
246
+ this.write("proxy", config);
247
+ (0, proxy_1.setProxyConfig)(config);
248
+ }
249
+ else if (context.action === "restart") {
250
+ (0, proxy_1.cleanStart)(true);
251
+ }
252
+ else {
253
+ // just show the proxy config
254
+ }
255
+ return this.ejs("proxy", context, { config });
256
+ });
257
+ }
228
258
  //////////////
229
259
  // Settings //
230
260
  //////////////
@@ -24,6 +24,7 @@ import { setToken } from "../duotecno/config";
24
24
  import { PowerBase } from "./powerbase";
25
25
  import { dt2hbvalue, hb2dtvalue, hbAccessories, hbGetValue, hbLogin, hbName, hbService, hbSetState, hbSubscribe } from "./HB";
26
26
  import { httpRequest, ContentType } from "./HA-API";
27
+ import { cleanStart, kEmptyProxy, ProxyConfig, setProxyConfig } from "./proxy";
27
28
 
28
29
  const kMaster = {name: "master", type: "string", default: "0.0.0.0:5001"} as const;
29
30
  const kAddress = {name: "address", type: "string", default: "0.0.0.0"} as const;
@@ -59,6 +60,7 @@ export class SmartApp extends WebApp {
59
60
  platform: Platform;
60
61
  switches: Array<Switch>;
61
62
  links: Array<Link>;
63
+ proxy: ProxyConfig;
62
64
 
63
65
 
64
66
  constructor(system: System, power: Power, platform: Platform) {
@@ -100,6 +102,8 @@ export class SmartApp extends WebApp {
100
102
  this.addFile("materializeCSS", __dirname + "/views/assets/materialize.min.css", "text/css");
101
103
  this.addFile("materializeJS", __dirname + "/views/assets/materialize.min.js", "text/javascript");
102
104
  this.addFile("favicon", __dirname + "/views/assets/favicon.ico", "image/x-icon");
105
+
106
+ this.addFile("proxy", __dirname + "/views/proxy.ejs", "text/html");
103
107
  }
104
108
 
105
109
  writeConfig() {
@@ -212,6 +216,9 @@ export class SmartApp extends WebApp {
212
216
  } else if (context.request === "settings") {
213
217
  return this.doSettings(context);
214
218
 
219
+ } else if (context.request === "proxy") {
220
+ return this.doProxy(context);
221
+
215
222
  } else {
216
223
  return super.doRequest(context);
217
224
  }
@@ -254,6 +261,31 @@ export class SmartApp extends WebApp {
254
261
  logicalAddress, logicalNodeAddress };
255
262
  }
256
263
 
264
+ ///////////
265
+ // Proxy //
266
+ ///////////
267
+ async doProxy(context: Context): Promise<HttpResponse> {
268
+ let config: ProxyConfig = this.config.proxy || kEmptyProxy;
269
+
270
+ if (context.action === "save") {
271
+ // save the proxy config
272
+ config.cloudServer = context.getParam({name: "cloudServer", type: "string"});
273
+ config.cloudPort = context.getParam({name: "cloudPort", type: "integer"});
274
+ config.masterAddress = context.getParam({name: "masterAddress", type: "string"});
275
+ config.masterPort = context.getParam({name: "masterPort", type: "integer"});
276
+ config.uniqueId = context.getParam({name: "uniqueId", type: "string"});
277
+ this.write("proxy", config);
278
+ setProxyConfig(config);
279
+
280
+ } else if (context.action === "restart") {
281
+ cleanStart(true);
282
+
283
+ } else {
284
+ // just show the proxy config
285
+ }
286
+ return this.ejs("proxy", context, { config });
287
+ }
288
+
257
289
  //////////////
258
290
  // Settings //
259
291
  //////////////
@@ -4,6 +4,7 @@
4
4
  <ul id="nav-mobile" class="right">
5
5
  <li <%= (request === "masters") ? 'class="active"' : '' %></li><a href="/masters">Masters</a></li>
6
6
  <li <%= (request === "services") ? 'class="active"' : '' %></li><a href="/services">Services</a></li>
7
+ <li <%= (request === "proxy") ? 'class="active"' : '' %></li><a href="/proxy">Proxy</a></li>
7
8
  <li <%= (request === "links") ? 'class="active"' : '' %></li><a href="/links">Links</a></li>
8
9
  <% if (hasSmappee) { %><li <%= ((request === "power") && (type === "smappee")) ? 'class="active"' : '' %></li><a href="/power?type=smappee">Smappee</a></li><% } %>
9
10
  <% if (hasP1) { %><li <%= ((request === "power") && (type === "p1")) ? 'class="active"' : '' %></li><a href="/power?type=p1">P1</a></li><% } %>
@@ -0,0 +1,71 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <link type="text/css" rel="stylesheet" href="/files/min.css" media="screen,projection"/>
5
+
6
+ <!--Let browser know website is optimized for mobile-->
7
+ <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
8
+ <link rel="shortcut icon" href="/files/favicon.ico" type="image/x-icon" />
9
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
10
+
11
+ <%- include('/views/style'); -%>
12
+ </head>
13
+
14
+ <body>
15
+ <%- include('/views/nav', this); -%>
16
+
17
+ <form method="POST" action="/proxy">
18
+ <input type="hidden" name="daction" value="save">
19
+ <h1>
20
+ Proxy Config
21
+ <button class="right btn waves-effect waves-light" type="submit" name="action" value="cancel">Back</button>
22
+ <button class="right btn waves-effect waves-light" type="submit" name="action" value="save">Save</button>
23
+ <button class="right btn waves-effect waves-light red" type="submit" name="action" value="restart">Restart</button>
24
+ </h1>
25
+
26
+ <div class="row">
27
+ <div class="input-field col s12">
28
+ <input placeholder="ws.duotecno.eu" id="cloudServer" name="cloudServer" type="text"
29
+ value="<%= config.cloudServer %>">
30
+ <label for="name">CloudServer</label>
31
+ </div>
32
+ </div>
33
+ <div class="row">
34
+ <div class="input-field col s12">
35
+ <input placeholder="5099" id="cloudPort" name="cloudPort" type="number"
36
+ value="<%= config.cloudPort %>">
37
+ <label for="name">CloudPort</label>
38
+ </div>
39
+ </div>
40
+
41
+ <div class="row">
42
+ <div class="input-field col s12">
43
+ <input placeholder="192.168.0.2" id="masterAddress" name="masterAddress" type="text"
44
+ value="<%= config.masterAddress %>">
45
+ <label for="name">MasterAddress</label>
46
+ </div>
47
+ </div>
48
+
49
+ <div class="row">
50
+ <div class="input-field col s12">
51
+ <input placeholder="5001" id="masterPort" name="masterPort" type="text"
52
+ value="<%= config.masterPort %>">
53
+ <label for="name">MasterPort</label>
54
+ </div>
55
+ </div>
56
+
57
+ <div class="row">
58
+ <div class="input-field col s12">
59
+ <input placeholder="client:5002" id="uniqueId" name="uniqueId" type="text"
60
+ value="<%= config.uniqueId %>">
61
+ <label for="name">UniqueId</label>
62
+ </div>
63
+ </div>
64
+
65
+ </form>
66
+ <%- include('/views/footer'); -%>
67
+
68
+ </body>
69
+
70
+ <%- include('/views/scripts'); -%>
71
+ </html>