homebridge-smartsystem 7.0.4 → 7.1.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.
Files changed (66) hide show
  1. package/README.md +2 -0
  2. package/duotecno/types.js +1 -1
  3. package/package.json +1 -1
  4. package/server/base.js +4 -0
  5. package/server/mDNS.js +108 -38
  6. package/server/platform.js +7 -0
  7. package/server/proxy.js +99 -44
  8. package/server/smartapp.js +22 -32
  9. package/server/views/head.ejs +1 -0
  10. package/server/views/nav.ejs +16 -8
  11. package/server/views/proxy.ejs +196 -17
  12. package/server/views/style.ejs +2 -1
  13. package/server/webapp.js +7 -0
  14. package/.gitattributes +0 -4
  15. package/MIXINS-GUIDE.md +0 -210
  16. package/SETTINGS-UPDATE.md +0 -195
  17. package/TESTING-GUIDE.md +0 -220
  18. package/WEBSOCKET-API.md +0 -303
  19. package/accessories/accessory.ts +0 -128
  20. package/accessories/bulb.ts +0 -38
  21. package/accessories/dimmer.ts +0 -97
  22. package/accessories/door.ts +0 -88
  23. package/accessories/garagedoor.ts +0 -134
  24. package/accessories/lock.ts +0 -103
  25. package/accessories/mood.ts +0 -68
  26. package/accessories/switch.ts +0 -34
  27. package/accessories/temperature.ts +0 -55
  28. package/accessories/windowcovering.ts +0 -189
  29. package/config.homebridge.json +0 -23
  30. package/config.json +0 -49
  31. package/duotecno/Q.ts +0 -56
  32. package/duotecno/api.ts +0 -63
  33. package/duotecno/colors.ts +0 -96
  34. package/duotecno/config.ts +0 -125
  35. package/duotecno/logger.ts +0 -43
  36. package/duotecno/master.ts +0 -884
  37. package/duotecno/protocol.ts +0 -992
  38. package/duotecno/smartsocket.ts +0 -118
  39. package/duotecno/system.ts +0 -439
  40. package/duotecno/types.ts +0 -900
  41. package/index.ts +0 -9
  42. package/log.txt +0 -74
  43. package/raspberry-pi-setup.sh +0 -80
  44. package/server/HA-API.ts +0 -102
  45. package/server/HB.ts +0 -343
  46. package/server/base.ts +0 -54
  47. package/server/mDNS.ts +0 -223
  48. package/server/p1.ts +0 -226
  49. package/server/platform.ts +0 -305
  50. package/server/powerbase.ts +0 -317
  51. package/server/proxy.ts +0 -482
  52. package/server/shelly.ts +0 -212
  53. package/server/smappee.ts +0 -309
  54. package/server/smartapp.ts +0 -1710
  55. package/server/socapp.ts +0 -182
  56. package/server/somfy.ts +0 -174
  57. package/server/support.ts +0 -142
  58. package/server/webapp.ts +0 -688
  59. package/testHB.js +0 -41
  60. package/testHB.ts +0 -54
  61. package/testProxy.js +0 -7
  62. package/testProxy.ts +0 -6
  63. package/testWS.js +0 -137
  64. package/testWS.ts +0 -174
  65. package/update.sh +0 -16
  66. package/updserv.sh +0 -13
package/README.md CHANGED
@@ -320,6 +320,8 @@ Homebridge UI version
320
320
  ### v7.0.0 - 12/11/2025
321
321
  - 0: added mDNS/Bonjour + always port 80 + don't preload EJS files if system.debug == true
322
322
  - 4: fixed a bug when shelly has empty list of ip addresses
323
+ - 5: better error handling in the proxy.ts + show connection in the /proxy page
324
+
323
325
 
324
326
  ## Hardware
325
327
  A Raspberry Pi that connects to a Duotecno IP Node
package/duotecno/types.js CHANGED
@@ -407,7 +407,7 @@ exports.Sanitizers = {
407
407
  ///////////
408
408
  proxy: function (config, system) {
409
409
  var _a, _b;
410
- const c = proxy_1.kEmptyProxy;
410
+ const c = Object.assign({}, proxy_1.kEmptyProxy);
411
411
  c.cloudServer = (config === null || config === void 0 ? void 0 : config.cloudServer) || proxy_1.kEmptyProxy.cloudServer;
412
412
  c.cloudPort = (config === null || config === void 0 ? void 0 : config.cloudPort) || proxy_1.kEmptyProxy.cloudPort;
413
413
  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) || "";
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-smartsystem",
3
3
  "displayname": "Duotecno Bridge",
4
- "version": "7.0.4",
4
+ "version": "7.1.0",
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",
package/server/base.js CHANGED
@@ -35,7 +35,11 @@ class Base {
35
35
  write(type, config) {
36
36
  (0, config_1.setSubConfig)(type, config);
37
37
  // but better to much than not enough
38
+ // if (os.platform() === "darwin") {
39
+ // writeFileSync(__dirname+"/../config.json", JSON.stringify(gPlatformConfig), null, 2));
40
+ // } else {
38
41
  (0, config_1.setAPIConfig)();
42
+ // }
39
43
  }
40
44
  }
41
45
  exports.Base = Base;
package/server/mDNS.js CHANGED
@@ -12,6 +12,9 @@ const os = require("os");
12
12
  const logger_1 = require("../duotecno/logger");
13
13
  class MDNSService {
14
14
  constructor(config) {
15
+ this.registeredName = null; // Actual name registered (may have suffix)
16
+ this.registrationError = null; // Last error during registration
17
+ this.registrationStatus = 'pending';
15
18
  this.config = config;
16
19
  }
17
20
  /**
@@ -29,65 +32,132 @@ class MDNSService {
29
32
  }
30
33
  /**
31
34
  * Register both HTTP service and hostname (like ESP32 MDNS.begin())
35
+ * Tries alternative names with suffixes if the name is already in use
32
36
  */
33
37
  register() {
34
38
  if (!this.bonjour) {
39
+ this.registrationError = "Service not initialized";
40
+ this.registrationStatus = 'failed';
35
41
  (0, logger_1.err)("mDNS", "Service not initialized, call init() first");
36
42
  return;
37
43
  }
38
- try {
39
- const { name, port, version } = this.config;
40
- const ports = Array.isArray(port) ? port : [port];
41
- const primaryPort = ports[0];
42
- // Register the HTTP service using bonjour
43
- // Wrap in try-catch because bonjour can throw synchronously on conflicts
44
- let service;
45
- try {
46
- service = this.bonjour.publish({
47
- name,
48
- type: 'http',
49
- port: primaryPort,
50
- txt: {
51
- version: version || '1.0.0',
52
- ports: ports.join(','),
53
- path: '/'
54
- }
55
- });
56
- (0, logger_1.log)("mDNS", `HTTP service registered: ${name}.local on port(s) ${ports.join(', ')}`);
44
+ const { name, port, version } = this.config;
45
+ const ports = Array.isArray(port) ? port : [port];
46
+ const primaryPort = ports[0];
47
+ // Set up error handler to prevent crash on "already in use" errors
48
+ // Note: The error message will still be logged by Node.js, but the app won't crash
49
+ // The retry logic with suffixes (.2, .3, etc.) will continue to work
50
+ let handlerActive = true;
51
+ const globalErrorHandler = (error) => {
52
+ if (handlerActive && error.message.includes('already in use')) {
53
+ // Suppress crash - retry logic will handle it
54
+ return;
55
+ }
56
+ };
57
+ process.prependListener('uncaughtException', globalErrorHandler);
58
+ setTimeout(() => {
59
+ handlerActive = false;
60
+ process.removeListener('uncaughtException', globalErrorHandler);
61
+ }, 5000);
62
+ // Try to register with the original name, then try with suffixes .2, .3, .4, .5, .6
63
+ this.tryRegisterWithSuffix(name, primaryPort, ports, version, 0);
64
+ }
65
+ /**
66
+ * Recursively try to register with different suffixes
67
+ */
68
+ tryRegisterWithSuffix(baseName, primaryPort, ports, version, attempt) {
69
+ if (attempt > 6) {
70
+ // Exhausted all attempts
71
+ this.registrationError = `All names from "${baseName}" to "${baseName}.7" are already in use on the network`;
72
+ this.registrationStatus = 'failed';
73
+ (0, logger_1.err)("mDNS", this.registrationError);
74
+ (0, logger_1.err)("mDNS", "Please change the mdnsName in settings or stop other conflicting services");
75
+ return;
76
+ }
77
+ const tryName = attempt === 0 ? baseName : `${baseName}.${attempt + 1}`;
78
+ let service;
79
+ let conflictDetected = false;
80
+ let successTimeout = null;
81
+ // Create a wrapper to handle conflicts
82
+ const handleConflict = () => {
83
+ if (conflictDetected)
84
+ return;
85
+ conflictDetected = true;
86
+ // Clear the success timeout
87
+ if (successTimeout) {
88
+ clearTimeout(successTimeout);
89
+ successTimeout = null;
57
90
  }
58
- catch (publishError) {
59
- if (publishError.message && publishError.message.includes('already in use')) {
60
- (0, logger_1.err)("mDNS", `Service name "${name}" is already in use. This is usually harmless - continuing anyway.`);
61
- (0, logger_1.err)("mDNS", "The old service registration will expire in ~30 seconds.");
91
+ // Unpublish the conflicting service
92
+ if (service) {
93
+ try {
94
+ service.stop();
62
95
  }
63
- else {
64
- (0, logger_1.err)("mDNS", `Service publish error: ${publishError.message}`);
96
+ catch (e) {
97
+ // Ignore errors during stop
65
98
  }
66
- // Continue anyway - service will work once the old registration expires
67
99
  }
68
- // Handle service errors - especially "already in use" errors
100
+ (0, logger_1.err)("mDNS", `Name "${tryName}" is already in use, trying next suffix...`);
101
+ // Try next suffix
102
+ setImmediate(() => {
103
+ this.tryRegisterWithSuffix(baseName, primaryPort, ports, version, attempt + 1);
104
+ });
105
+ };
106
+ try {
107
+ service = this.bonjour.publish({
108
+ name: tryName,
109
+ type: 'http',
110
+ port: primaryPort,
111
+ txt: {
112
+ version: version || '1.0.0',
113
+ ports: ports.join(','),
114
+ path: '/'
115
+ }
116
+ });
117
+ // Set up error handler on the service
69
118
  if (service) {
70
119
  service.on('error', (error) => {
71
120
  if (error.message.includes('already in use')) {
72
- (0, logger_1.err)("mDNS", `Service name "${name}" is already in use. Another instance may be running.`);
73
- (0, logger_1.err)("mDNS", "To fix: 1) Stop other instances, or 2) Change the mdnsName in settings, or 3) Wait 30s for old service to expire");
121
+ handleConflict();
74
122
  }
75
- else {
123
+ else if (!conflictDetected) {
124
+ this.registrationError = error.message;
125
+ this.registrationStatus = 'failed';
76
126
  (0, logger_1.err)("mDNS", `Service error: ${error.message}`);
77
127
  }
78
128
  });
79
129
  }
80
- // Publish the hostname A record (like ESP32 MDNS.begin())
81
- this.publishHostname(name);
130
+ // Wait longer before declaring success - conflicts come from network
131
+ successTimeout = setTimeout(() => {
132
+ if (!conflictDetected) {
133
+ // Success!
134
+ this.registeredName = tryName;
135
+ this.registrationStatus = 'success';
136
+ this.registrationError = null;
137
+ if (attempt > 0) {
138
+ (0, logger_1.log)("mDNS", `⚠️ Original name "${baseName}" was taken, registered as "${tryName}.local" instead`);
139
+ }
140
+ else {
141
+ (0, logger_1.log)("mDNS", `✓ HTTP service registered: ${tryName}.local on port(s) ${ports.join(', ')}`);
142
+ }
143
+ // Publish the hostname A record (like ESP32 MDNS.begin())
144
+ this.publishHostname(tryName);
145
+ }
146
+ }, 2000); // Wait 2 seconds for network conflict responses
82
147
  }
83
- catch (e) {
84
- if (e.message.includes('already in use')) {
85
- (0, logger_1.err)("mDNS", `Service name "${this.config.name}" is already in use on the network`);
86
- (0, logger_1.err)("mDNS", "Possible causes: 1) Another instance is running, 2) Previous instance didn't clean up properly");
87
- (0, logger_1.err)("mDNS", "Solutions: Stop other instances, or restart with a different service name in settings");
148
+ catch (publishError) {
149
+ if (publishError.message && publishError.message.includes('already in use')) {
150
+ // This name is taken, try the next one
151
+ (0, logger_1.err)("mDNS", `Name "${tryName}" is taken (sync error), trying next suffix...`);
152
+ setImmediate(() => {
153
+ this.tryRegisterWithSuffix(baseName, primaryPort, ports, version, attempt + 1);
154
+ });
88
155
  }
89
156
  else {
90
- (0, logger_1.err)("mDNS", `Failed to register service: ${e.message.split('\n')[0]}`);
157
+ // Different error, stop trying
158
+ this.registrationError = publishError.message;
159
+ this.registrationStatus = 'failed';
160
+ (0, logger_1.err)("mDNS", `Service publish error: ${publishError.message}`);
91
161
  }
92
162
  }
93
163
  }
@@ -48,6 +48,13 @@ class Platform extends base_1.Base {
48
48
  (0, logger_1.log)("homebridge", "try doing upgrade from pre v6.2 versions");
49
49
  this.doUpgradeV62();
50
50
  }
51
+ // we're running under HomeBridge/Platform -> always set proxy kind to "gw"
52
+ if (!this.config.proxy) {
53
+ this.config.proxy = Object.assign({}, proxy_1.kEmptyProxy);
54
+ }
55
+ else if (this.config.proxy.kind !== "gw") {
56
+ this.config.proxy.kind = "gw";
57
+ }
51
58
  (0, logger_1.log)("homebridge", "running in " + ((this.config.debug) ? "debug" : "prod") + " mode in directory: " + process.cwd());
52
59
  (0, logger_1.log)("homebridge", "with config:" + JSON.stringify(this.config, null, 2));
53
60
  // save config for other components
package/server/proxy.js CHANGED
@@ -9,18 +9,21 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
9
9
  });
10
10
  };
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
- exports.Context = exports.cleanStart = exports.makeNewCloudConnection = exports.setProxyConfig = exports.setWebApp = exports.kEmptyProxy = void 0;
12
+ exports.Context = exports.cleanStart = exports.makeNewCloudConnection = exports.gCloudConnections = exports.setProxyConfig = exports.setWebApp = exports.kEmptyProxy = void 0;
13
13
  const net = require("net");
14
14
  const child_process_1 = require("child_process");
15
15
  const process_1 = require("process");
16
16
  exports.kEmptyProxy = {
17
17
  cloudServer: "masters.duotecno.eu",
18
18
  cloudPort: 5097,
19
+ configPort: 5099,
19
20
  masterAddress: "",
20
21
  masterPort: 5001,
22
+ masterConfigPort: 8080,
21
23
  uniqueId: "",
24
+ configUniqueId: "",
22
25
  debug: false,
23
- kind: "gw" // default running under PM2 or other process manager -> don't restart yourself
26
+ kind: "gw" // default running under the gateway or other process manager -> don't restart yourself
24
27
  };
25
28
  const config = Object.assign({}, exports.kEmptyProxy);
26
29
  // Store reference to the webapp so we can close servers before restarting
@@ -34,26 +37,20 @@ function setProxyConfig(c) {
34
37
  if (c) {
35
38
  config.cloudServer = c.cloudServer || exports.kEmptyProxy.cloudServer;
36
39
  config.cloudPort = c.cloudPort || exports.kEmptyProxy.cloudPort;
40
+ config.configPort = c.configPort || exports.kEmptyProxy.configPort;
37
41
  config.masterAddress = c.masterAddress || exports.kEmptyProxy.masterAddress;
38
42
  config.masterPort = c.masterPort || exports.kEmptyProxy.masterPort;
43
+ config.masterConfigPort = c.masterConfigPort || exports.kEmptyProxy.masterConfigPort;
39
44
  config.uniqueId = c.uniqueId || exports.kEmptyProxy.uniqueId;
45
+ config.configUniqueId = c.configUniqueId || exports.kEmptyProxy.configUniqueId;
40
46
  config.debug = !!c.debug;
41
47
  config.kind = c.kind || exports.kEmptyProxy.kind;
42
48
  }
43
49
  return config;
44
50
  }
45
51
  exports.setProxyConfig = setProxyConfig;
46
- try {
47
- const configPath = require.resolve('../config-proxy.json');
48
- console.log(`[PROXY] Using config file at: ${configPath}`);
49
- setProxyConfig(require(configPath));
50
- }
51
- catch (e) {
52
- setProxyConfig();
53
- log("[PROXY] No config-proxy.json found, using default values from homebridge or other.");
54
- }
55
52
  let connectionChecker = null;
56
- const gCloudConnections = [];
53
+ exports.gCloudConnections = [];
57
54
  const kDebug = config.debug || false; // enable debug mode from config
58
55
  /////////////
59
56
  // Logging //
@@ -78,13 +75,14 @@ function error(str) {
78
75
  // Only auto-start if we have a config-proxy.json file (standalone mode)
79
76
  // When running under Platform/HomeBridge, let Platform control the startup
80
77
  try {
81
- require.resolve('../config-proxy.json');
78
+ setProxyConfig(require("/../config-proxy.json"));
82
79
  // config-proxy.json exists, we're running standalone -> start the proxy
83
80
  log("[PROXY] Running in standalone mode (config-proxy.json found), starting proxy...");
84
81
  cleanStart(true);
85
82
  }
86
83
  catch (e) {
87
84
  // No config-proxy.json, we're probably running under Platform/HomeBridge
85
+ setProxyConfig();
88
86
  log("[PROXY] No config-proxy.json found, waiting for Platform to start proxy...");
89
87
  }
90
88
  function makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId, count = 1) {
@@ -108,8 +106,8 @@ function makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId
108
106
  cloudSocket.write(`[${uniqueId}]`);
109
107
  debug(`[PROXY] → [CLOUD] Sent unique ID: ${uniqueId}`);
110
108
  const context = new Context(cloudSocket, master, masterPort, server, serverPort, uniqueId);
111
- gCloudConnections.push(context);
112
- log(`[PROXY] → [CLOUD] New free connection #${gCloudConnections.length} to the Cloud at ${server}:${serverPort}`);
109
+ exports.gCloudConnections.push(context);
110
+ log(`[PROXY] → [CLOUD] New free connection #${exports.gCloudConnections.length} to the Cloud at ${server}:${serverPort}`);
113
111
  });
114
112
  }
115
113
  exports.makeNewCloudConnection = makeNewCloudConnection;
@@ -117,23 +115,41 @@ exports.makeNewCloudConnection = makeNewCloudConnection;
117
115
  // Restarting //
118
116
  ////////////////
119
117
  function cleanStart(restart = false) {
120
- if (gCloudConnections === null || gCloudConnections === void 0 ? void 0 : gCloudConnections.length) {
121
- gCloudConnections.forEach(ctx => ctx.cleanupSockets());
122
- log(`[PROXY] → [CLOUD] Cleaned up all ${gCloudConnections.length} cloud connections.`);
123
- gCloudConnections.splice(0, gCloudConnections.length); // clear the array
118
+ if (exports.gCloudConnections === null || exports.gCloudConnections === void 0 ? void 0 : exports.gCloudConnections.length) {
119
+ exports.gCloudConnections.forEach(ctx => ctx.cleanupSockets());
120
+ log(`[PROXY] → [CLOUD] Cleaned up all ${exports.gCloudConnections.length} cloud connections.`);
121
+ exports.gCloudConnections.splice(0, exports.gCloudConnections.length); // clear the array
124
122
  }
125
123
  if (connectionChecker) {
126
124
  clearInterval(connectionChecker);
127
125
  connectionChecker = null;
128
126
  }
129
127
  if (restart) {
130
- log(`[PROXY] → [CLOUD] Restarting proxy... for ${config.uniqueId}.`);
131
- if (config.uniqueId) {
132
- log(`[PROXY] Starting proxy with config: ${JSON.stringify(config, null, 2)}`);
128
+ log(`[PROXY] → [CLOUD] Restarting proxy...`);
129
+ let connectionsStarted = 0;
130
+ // Start connection for the master device if uniqueId is configured
131
+ if (config.uniqueId && config.masterAddress) {
132
+ log(`[PROXY] Starting proxy for master device: ${config.uniqueId}`);
133
133
  makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
134
+ connectionsStarted++;
135
+ }
136
+ else if (config.uniqueId) {
137
+ log(`[PROXY] → [CLOUD] Master uniqueId configured but no masterAddress, skipping master connection.`);
138
+ }
139
+ // Start TCP proxy for Config API if masterConfigPort is configured
140
+ // Use uniqueId:8080 pattern (or configUniqueId if explicitly set for backward compatibility)
141
+ if (config.uniqueId && config.masterAddress && config.masterConfigPort) {
142
+ const configId = config.configUniqueId || `${config.uniqueId}:${config.masterConfigPort}`;
143
+ log(`[PROXY] Starting TCP proxy for Config API: ${configId} -> ${config.masterAddress}:${config.masterConfigPort}`);
144
+ // All connections use the same cloud port for WebSocket multiplexing
145
+ makeNewCloudConnection(config.masterAddress, config.masterConfigPort, config.cloudServer, config.cloudPort, configId);
146
+ connectionsStarted++;
147
+ }
148
+ if (connectionsStarted > 0) {
149
+ log(`[PROXY] Started ${connectionsStarted} proxy connection(s) with config: ${JSON.stringify(config, null, 2)}`);
134
150
  // check every 16 second for a free connection
135
151
  connectionChecker = setInterval(() => {
136
- const freeConnection = gCloudConnections.find((ctx) => (!ctx.deviceSocket) && ctx.cloudSocket && (ctx.cloudSocket.readyState === 'open'));
152
+ const freeConnection = exports.gCloudConnections.find((ctx) => (!ctx.deviceSocket) && ctx.cloudSocket && (ctx.cloudSocket.readyState === 'open'));
137
153
  if (freeConnection) {
138
154
  debug(`[PROXY] → [CLOUD] Found free connection #${freeConnection.uniqueId} to the Cloud -> we're still ok.`);
139
155
  }
@@ -144,7 +160,7 @@ function cleanStart(restart = false) {
144
160
  }, 16 * 1000);
145
161
  }
146
162
  else {
147
- log(`[PROXY] → [CLOUD] Not restarting, no unique ID configured, not starting the proxy.`);
163
+ log(`[PROXY] → [CLOUD] Not restarting, no unique IDs configured, not starting the proxy.`);
148
164
  }
149
165
  }
150
166
  else {
@@ -161,6 +177,15 @@ class Context {
161
177
  this.server = server;
162
178
  this.serverPort = serverPort;
163
179
  this.uniqueId = uniqueId;
180
+ // Gateway connections have no master address (they handle HTTP, not TCP proxy)
181
+ this.isGateway = !master || master === "";
182
+ this.deviceStatus = {
183
+ connected: false,
184
+ lastAttempt: null,
185
+ lastSuccess: null,
186
+ lastError: null,
187
+ retryCount: 0
188
+ };
164
189
  this.setupCloudSocket();
165
190
  }
166
191
  setupCloudSocket() {
@@ -199,12 +224,21 @@ class Context {
199
224
  // Handle connection response from the cloud server
200
225
  }
201
226
  else {
202
- // There is real incomming data, it's a fresh connection, so: a new client wants to connect to the device
203
- // 1) create a new (free) connection to the cloud server for the next client
204
- // 2) connect to the device and send the initial data
205
- log(`[PROXY] → [CLOUD] New client connection, creating new free connection for this proxy/device: ${config.uniqueId}.`);
206
- makeNewCloudConnection(this.master, this.masterPort, this.server, this.serverPort, this.uniqueId);
207
- this.makeDeviceConnection(this.master, this.masterPort, data);
227
+ // There is real incoming data, it's a fresh connection, so: a new client wants to connect
228
+ if (this.isGateway) {
229
+ // Gateway connection - forward HTTP request to local HTTP gateway on port 5002
230
+ log(`[PROXY] → [CLOUD] New client connection for gateway: ${this.uniqueId}.`);
231
+ makeNewCloudConnection(this.master, this.masterPort, this.server, this.serverPort, this.uniqueId);
232
+ this.makeDeviceConnection("localhost", 5002, data);
233
+ }
234
+ else {
235
+ // Device connection - create TCP proxy to master device
236
+ // 1) create a new (free) connection to the cloud server for the next client
237
+ // 2) connect to the device and send the initial data
238
+ log(`[PROXY] → [CLOUD] New client connection, creating new free connection for this proxy/device: ${this.uniqueId}.`);
239
+ makeNewCloudConnection(this.master, this.masterPort, this.server, this.serverPort, this.uniqueId);
240
+ this.makeDeviceConnection(this.master, this.masterPort, data);
241
+ }
208
242
  }
209
243
  }
210
244
  else if (this.deviceSocket.readyState === 'open') {
@@ -217,8 +251,25 @@ class Context {
217
251
  }
218
252
  makeDeviceConnection(address, port, data) {
219
253
  log(`[PROXY] → [DEVICE] Attempting to connect to local device at ${address}:${port}`);
254
+ this.deviceStatus.lastAttempt = new Date();
220
255
  if (!this.deviceSocket) {
221
256
  this.deviceSocket = new net.Socket();
257
+ // Add error handler BEFORE connecting to prevent uncaught exceptions
258
+ this.deviceSocket.on('error', (err) => {
259
+ error(`[DEVICE] → [PROXY] Connection error: ${err.message}`);
260
+ // Update device status
261
+ this.deviceStatus.connected = false;
262
+ this.deviceStatus.lastError = err.message;
263
+ this.deviceStatus.retryCount++;
264
+ log(`[DEVICE] → [PROXY] Connection failed. Will retry on next cloud request.`);
265
+ // Clean up this socket
266
+ if (this.deviceSocket && !this.deviceSocket.destroyed) {
267
+ this.deviceSocket.destroy();
268
+ }
269
+ this.deviceSocket = null;
270
+ // Don't create a new cloud connection - we already have one!
271
+ // The device connection will be retried when the next data comes from the cloud
272
+ });
222
273
  // Connect to local device and send the data that came in from the cloud
223
274
  this.deviceSocket.connect(port, address, () => {
224
275
  // Check if socket is still valid (not cleaned up by removeConnection)
@@ -227,6 +278,11 @@ class Context {
227
278
  return;
228
279
  }
229
280
  log(`[PROXY] → [DEVICE] Connected to local device at ${address}:${port}`);
281
+ // Update device status
282
+ this.deviceStatus.connected = true;
283
+ this.deviceStatus.lastSuccess = new Date();
284
+ this.deviceStatus.lastError = null;
285
+ this.deviceStatus.retryCount = 0;
230
286
  this.setUpDeviceSocket();
231
287
  log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().trim()}`);
232
288
  this.deviceSocket.write(data);
@@ -254,10 +310,8 @@ class Context {
254
310
  log(`[DEVICE] → [PROXY] Device socket closed`);
255
311
  this.removeConnection();
256
312
  });
257
- this.deviceSocket.on('error', (err) => {
258
- error(`[DEVICE] → [PROXY] Device socket error: ${err.message}`);
259
- this.removeConnection();
260
- });
313
+ // Note: error handler is already added in makeDeviceConnection()
314
+ // Don't add duplicate error handler here
261
315
  }
262
316
  cleanupSockets() {
263
317
  // Clean up the device socket
@@ -280,12 +334,12 @@ class Context {
280
334
  }
281
335
  }
282
336
  removeConnection() {
283
- const index = gCloudConnections.indexOf(this);
337
+ const index = exports.gCloudConnections.indexOf(this);
284
338
  if (index !== -1)
285
- gCloudConnections.splice(index, 1);
339
+ exports.gCloudConnections.splice(index, 1);
286
340
  this.cleanupSockets();
287
341
  log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
288
- if (gCloudConnections && gCloudConnections.length === 0) {
342
+ if (exports.gCloudConnections && exports.gCloudConnections.length === 0) {
289
343
  log(`[PROXY] → [CLOUD] No more cloud connections, restarting proxy.`);
290
344
  // just to be sure: restart...
291
345
  if (config.kind === "pm2") {
@@ -346,9 +400,9 @@ function handleShutdown(signal) {
346
400
  connectionChecker = null;
347
401
  }
348
402
  // Close all cloud connections
349
- if (gCloudConnections && gCloudConnections.length > 0) {
350
- gCloudConnections.forEach(ctx => ctx.cleanupSockets());
351
- gCloudConnections.splice(0, gCloudConnections.length);
403
+ if (exports.gCloudConnections && exports.gCloudConnections.length > 0) {
404
+ exports.gCloudConnections.forEach(ctx => ctx.cleanupSockets());
405
+ exports.gCloudConnections.splice(0, exports.gCloudConnections.length);
352
406
  }
353
407
  // Close HTTP servers if available
354
408
  if (gWebApp && typeof gWebApp.closeServers === 'function') {
@@ -360,15 +414,16 @@ function handleShutdown(signal) {
360
414
  error(`[PROXY] → [SYSTEM] Error closing servers: ${e.message}`);
361
415
  }
362
416
  }
363
- log(`[PROXY] → [SYSTEM] All connections closed. Exiting.`);
417
+ log(`[PROXY] → [SYSTEM] All connections closed.`);
364
418
  if (config.kind === "solo") {
365
419
  log(`[PROXY] → [SYSTEM] Restarting proxy in solo mode...`);
366
420
  restart();
367
421
  }
368
422
  else {
369
- log(`[PROXY] → [SYSTEM] Exiting without restart.`);
370
- // Give OS time to release ports before exit (helps prevent EADDRINUSE on restart)
371
- setTimeout(() => process.exit(0), 500);
423
+ log(`[PROXY] → [SYSTEM] Running under HomeBridge/Gateway - NOT calling process.exit() to avoid restarting entire system.`);
424
+ log(`[PROXY] → [SYSTEM] Plugin will remain running. Check /settings page for connection status.`);
425
+ // Don't call process.exit() when running under HomeBridge!
426
+ // This would restart the entire HomeBridge + UI system
372
427
  }
373
428
  });
374
429
  }
@@ -327,11 +327,16 @@ class SmartApp extends webapp_1.WebApp {
327
327
  // save the proxy config
328
328
  config.cloudServer = context.getParam({ name: "cloudServer", type: "string" });
329
329
  config.cloudPort = context.getParam({ name: "cloudPort", type: "integer" });
330
+ config.configPort = context.getParam({ name: "configPort", type: "integer" });
330
331
  config.masterAddress = context.getParam({ name: "masterAddress", type: "string" });
331
332
  config.masterPort = context.getParam({ name: "masterPort", type: "integer" });
333
+ config.masterConfigPort = context.getParam({ name: "masterConfigPort", type: "integer" });
332
334
  config.uniqueId = context.getParam({ name: "uniqueId", type: "string" });
335
+ config.configUniqueId = context.getParam({ name: "configUniqueId", type: "string" });
333
336
  this.write("proxy", config);
334
337
  (0, proxy_1.setProxyConfig)(config);
338
+ // Automatically restart proxy with new config
339
+ (0, proxy_1.cleanStart)(true);
335
340
  }
336
341
  else if (context.action === "restart") {
337
342
  (0, proxy_1.cleanStart)(true);
@@ -339,7 +344,13 @@ class SmartApp extends webapp_1.WebApp {
339
344
  else {
340
345
  // just show the proxy config
341
346
  }
342
- return this.ejs("proxy", context, { config });
347
+ // Get mDNS status for display
348
+ const mdnsStatus = this.mdnsService ? {
349
+ registeredName: this.mdnsService.registeredName,
350
+ registrationError: this.mdnsService.registrationError,
351
+ registrationStatus: this.mdnsService.registrationStatus
352
+ } : null;
353
+ return this.ejs("proxy", context, { config, connections: proxy_1.gCloudConnections, mdnsStatus });
343
354
  });
344
355
  }
345
356
  //////////////
@@ -363,36 +374,6 @@ class SmartApp extends webapp_1.WebApp {
363
374
  context.request = "restart";
364
375
  return this.doRestart(false);
365
376
  }
366
- else if (context.action === "install") {
367
- // Network IP configuration - temporarily disabled
368
- // TODO: Re-implement DHCP configuration
369
- message = "Network IP configuration is temporarily disabled. Use 'Save Settings' for mDNS configuration.";
370
- /* Commented out until DHCP implementation is fixed
371
- config = this.scrapeSettings(context);
372
- context.request = "install";
373
- this.write("settings", config);
374
- this.writeDHCP(context, <SettingsConfig>config);
375
- */
376
- }
377
- else if (context.action === "reset") {
378
- // Reset to DHCP - temporarily disabled
379
- // TODO: Re-implement DHCP reset
380
- message = "DHCP reset is temporarily disabled.";
381
- /* Commented out until DHCP implementation is fixed
382
- config = { ...kEmptySettings };
383
- this.resetDHCP(context);
384
- this.write("settings", config);
385
- return this.doReboot(context, false);
386
- */
387
- }
388
- else if (context.action === "reboot") {
389
- // System reboot - temporarily disabled
390
- // TODO: Re-enable after testing
391
- message = "System reboot is temporarily disabled.";
392
- /* Commented out until we want to enable system reboot
393
- return this.doReboot(context, false);
394
- */
395
- }
396
377
  else {
397
378
  // Just display the settings form
398
379
  }
@@ -1389,7 +1370,7 @@ class SmartApp extends webapp_1.WebApp {
1389
1370
  //////////////////////////////
1390
1371
  doUnits(context) {
1391
1372
  return __awaiter(this, void 0, void 0, function* () {
1392
- // get masterAddress and Port
1373
+ // get master Address and Port into "masterAddress" & "masterPort"
1393
1374
  context.getMaster("action", "node");
1394
1375
  const master = this.system.findMaster(context["masterAddress"], context["masterPort"]);
1395
1376
  if (context.action === "save") {
@@ -1405,6 +1386,15 @@ class SmartApp extends webapp_1.WebApp {
1405
1386
  context.action = "edit";
1406
1387
  return this.doRequest(context);
1407
1388
  }
1389
+ else if (context.action === "register") {
1390
+ // master=...&port=...&register=...&value=...
1391
+ if (!master)
1392
+ return this.error(context, "master not found", true);
1393
+ const register = context.getParam({ name: "register", type: "integer", default: 0 });
1394
+ const value = context.getParam(kValue);
1395
+ yield master.setRegisterValue(register, value);
1396
+ return this.json({ register, value });
1397
+ }
1408
1398
  else if (context.action === "set") {
1409
1399
  if (!master)
1410
1400
  return this.error(context, "master not found", true);
@@ -1,5 +1,6 @@
1
1
  <meta charset="UTF-8">
2
2
  <link type="text/css" rel="stylesheet" href="/files/min.css" media="screen,projection"/>
3
+ <title>Duotecno Gateway</title>
3
4
 
4
5
  <!--Let browser know website is optimized for mobile-->
5
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>