homebridge-smartsystem 6.8.13 → 7.0.1

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 (75) hide show
  1. package/.vscode/launch.json +15 -4
  2. package/MIXINS-GUIDE.md +210 -0
  3. package/README.md +32 -0
  4. package/SETTINGS-UPDATE.md +195 -0
  5. package/TESTING-GUIDE.md +220 -0
  6. package/WEBSOCKET-API.md +303 -0
  7. package/accessories/accessory.js +20 -4
  8. package/accessories/accessory.ts +19 -4
  9. package/accessories/bulb.js +0 -5
  10. package/accessories/bulb.ts +1 -5
  11. package/accessories/dimmer.js +11 -3
  12. package/accessories/dimmer.ts +11 -4
  13. package/accessories/garagedoor.js +19 -9
  14. package/accessories/garagedoor.ts +21 -9
  15. package/accessories/lock.js +11 -3
  16. package/accessories/lock.ts +10 -3
  17. package/accessories/temperature.js +10 -2
  18. package/accessories/temperature.ts +9 -2
  19. package/accessories/windowcovering.js +30 -14
  20. package/accessories/windowcovering.ts +29 -15
  21. package/config.json +57 -19
  22. package/duotecno/types.js +48 -2
  23. package/duotecno/types.ts +75 -1
  24. package/package.json +12 -4
  25. package/raspberry-pi-setup.sh +80 -0
  26. package/server/HA-API.js +4 -1
  27. package/server/HA-API.ts +4 -1
  28. package/server/HB.js +6 -1
  29. package/server/HB.ts +5 -1
  30. package/server/mDNS.js +168 -0
  31. package/server/mDNS.ts +198 -0
  32. package/server/platform.js +7 -0
  33. package/server/platform.ts +8 -0
  34. package/server/proxy.js +58 -19
  35. package/server/proxy.ts +31 -3
  36. package/server/smartapp.js +397 -18
  37. package/server/smartapp.ts +432 -20
  38. package/server/socapp.js +2 -1
  39. package/server/socapp.ts +2 -1
  40. package/server/views/assets/Duotecno_logo_black.svg +10 -0
  41. package/server/views/assets/Duotecno_logo_white.svg +10 -0
  42. package/server/views/device-details.ejs +139 -0
  43. package/server/views/device-list.ejs +44 -0
  44. package/server/views/footer.ejs +0 -1
  45. package/server/views/head.ejs +9 -0
  46. package/server/views/homekit.ejs +2 -9
  47. package/server/views/link-details.ejs +2 -15
  48. package/server/views/link-list.ejs +2 -9
  49. package/server/views/login.ejs +8 -9
  50. package/server/views/master-detail.ejs +3 -9
  51. package/server/views/master-list.ejs +3 -10
  52. package/server/views/nav.ejs +44 -11
  53. package/server/views/node-details.ejs +1 -8
  54. package/server/views/power-binding.ejs +3 -10
  55. package/server/views/power-rule.ejs +3 -10
  56. package/server/views/power.ejs +8 -15
  57. package/server/views/proxy.ejs +1 -8
  58. package/server/views/scripts.ejs +10 -0
  59. package/server/views/service-list.ejs +2 -9
  60. package/server/views/settings.ejs +61 -22
  61. package/server/views/style.ejs +49 -5
  62. package/server/views/switch-details.ejs +2 -9
  63. package/server/views/switch-list.ejs +2 -9
  64. package/server/webapp.js +150 -11
  65. package/server/webapp.ts +166 -14
  66. package/{index.spec.js → testHB.js} +1 -1
  67. package/{run.js → testProxy.js} +2 -2
  68. package/{run.ts → testProxy.ts} +1 -1
  69. package/testWS.js +137 -0
  70. package/testWS.ts +174 -0
  71. package/run9998.js +0 -7
  72. package/run9998.ts +0 -6
  73. package/run9999.js +0 -7
  74. package/run9999.ts +0 -6
  75. /package/{index.spec.ts → testHB.ts} +0 -0
package/server/mDNS.js ADDED
@@ -0,0 +1,168 @@
1
+ "use strict";
2
+ // mDNS Service Manager
3
+ // Purpose: Handle mDNS service discovery and hostname publishing
4
+ // Similar to ESP32's MDNS.begin() functionality
5
+ //
6
+ // Johan Coppieters, Nov 2025
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.MDNSService = void 0;
9
+ const Bonjour = require("bonjour-service");
10
+ const mdns = require("multicast-dns");
11
+ const os = require("os");
12
+ const logger_1 = require("../duotecno/logger");
13
+ class MDNSService {
14
+ constructor(config) {
15
+ this.config = config;
16
+ }
17
+ /**
18
+ * Initialize mDNS service (call this before registering)
19
+ */
20
+ init() {
21
+ try {
22
+ this.bonjour = new Bonjour.Bonjour();
23
+ (0, logger_1.log)("mDNS", "Service initialized");
24
+ }
25
+ catch (e) {
26
+ (0, logger_1.err)("mDNS", `Failed to initialize: ${e.message}`);
27
+ throw e;
28
+ }
29
+ }
30
+ /**
31
+ * Register both HTTP service and hostname (like ESP32 MDNS.begin())
32
+ */
33
+ register() {
34
+ if (!this.bonjour) {
35
+ (0, logger_1.err)("mDNS", "Service not initialized, call init() first");
36
+ return;
37
+ }
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
+ const service = this.bonjour.publish({
44
+ name,
45
+ type: 'http',
46
+ port: primaryPort,
47
+ txt: {
48
+ version: version || '1.0.0',
49
+ ports: ports.join(','),
50
+ path: '/'
51
+ }
52
+ });
53
+ (0, logger_1.log)("mDNS", `HTTP service registered: ${name}.local on port(s) ${ports.join(', ')}`);
54
+ // Handle service errors
55
+ service.on('error', (error) => {
56
+ (0, logger_1.err)("mDNS", `Service error: ${error.message}`);
57
+ });
58
+ // Publish the hostname A record (like ESP32 MDNS.begin())
59
+ this.publishHostname(name);
60
+ }
61
+ catch (e) {
62
+ (0, logger_1.err)("mDNS", `Failed to register service: ${e.message}`);
63
+ }
64
+ }
65
+ /**
66
+ * Publish hostname as A/AAAA records (ESP32-style hostname publishing)
67
+ */
68
+ publishHostname(hostname) {
69
+ try {
70
+ // Create multicast DNS instance
71
+ this.mdns = mdns();
72
+ // Get local IP addresses
73
+ const addresses = this.getLocalIPAddresses();
74
+ if (addresses.length === 0) {
75
+ (0, logger_1.err)("mDNS", "No network interfaces found, cannot publish hostname");
76
+ return;
77
+ }
78
+ (0, logger_1.log)("mDNS", `Publishing hostname ${hostname}.local with IPs: ${addresses.join(', ')}`);
79
+ // Respond to mDNS queries for our hostname (like ESP32 MDNS.begin())
80
+ this.mdns.on('query', (query) => {
81
+ // Check if someone is asking for our hostname
82
+ const hostnameQuery = query.questions.find(q => q.name === `${hostname}.local` && (q.type === 'A' || q.type === 'AAAA' || q.type === 'ANY'));
83
+ if (hostnameQuery) {
84
+ // Respond with A records for all our IP addresses
85
+ const answers = addresses.map(ip => {
86
+ const isIPv6 = ip.includes(':');
87
+ return {
88
+ name: `${hostname}.local`,
89
+ type: isIPv6 ? 'AAAA' : 'A',
90
+ ttl: 120,
91
+ data: ip
92
+ };
93
+ });
94
+ this.mdns.respond(answers, () => {
95
+ (0, logger_1.debug)("mDNS", `Responded to query for ${hostname}.local`);
96
+ });
97
+ }
98
+ });
99
+ // Send announcement (like ESP32 does on startup)
100
+ this.announceHostname(hostname, addresses);
101
+ (0, logger_1.log)("mDNS", `Hostname responder started for ${hostname}.local`);
102
+ }
103
+ catch (e) {
104
+ (0, logger_1.err)("mDNS", `Failed to publish hostname: ${e.message}`);
105
+ }
106
+ }
107
+ /**
108
+ * Send unsolicited announcement (like ESP32 mDNS does on startup)
109
+ */
110
+ announceHostname(hostname, addresses) {
111
+ const answers = addresses.map(ip => {
112
+ const isIPv6 = ip.includes(':');
113
+ return {
114
+ name: `${hostname}.local`,
115
+ type: isIPv6 ? 'AAAA' : 'A',
116
+ ttl: 120,
117
+ data: ip
118
+ };
119
+ });
120
+ this.mdns.respond(answers, () => {
121
+ (0, logger_1.log)("mDNS", `Announced ${hostname}.local on the network`);
122
+ });
123
+ }
124
+ /**
125
+ * Get local IPv4 addresses (excluding loopback and virtual interfaces)
126
+ */
127
+ getLocalIPAddresses() {
128
+ const addresses = [];
129
+ const interfaces = os.networkInterfaces();
130
+ for (const name in interfaces) {
131
+ const iface = interfaces[name];
132
+ if (!iface)
133
+ continue;
134
+ for (const addr of iface) {
135
+ // Skip internal (loopback) and non-IPv4 addresses
136
+ if (addr.internal || addr.family !== 'IPv4')
137
+ continue;
138
+ // Skip docker and virtual interfaces
139
+ if (name.startsWith('docker') || name.startsWith('veth'))
140
+ continue;
141
+ addresses.push(addr.address);
142
+ }
143
+ }
144
+ return addresses;
145
+ }
146
+ /**
147
+ * Stop and cleanup mDNS services
148
+ */
149
+ destroy() {
150
+ try {
151
+ if (this.mdns) {
152
+ this.mdns.destroy();
153
+ this.mdns = null;
154
+ (0, logger_1.log)("mDNS", "Hostname responder stopped");
155
+ }
156
+ if (this.bonjour) {
157
+ this.bonjour.destroy();
158
+ this.bonjour = null;
159
+ (0, logger_1.log)("mDNS", "Service discovery stopped");
160
+ }
161
+ }
162
+ catch (e) {
163
+ (0, logger_1.err)("mDNS", `Error during cleanup: ${e.message}`);
164
+ }
165
+ }
166
+ }
167
+ exports.MDNSService = MDNSService;
168
+ //# sourceMappingURL=mDNS.js.map
package/server/mDNS.ts ADDED
@@ -0,0 +1,198 @@
1
+ // mDNS Service Manager
2
+ // Purpose: Handle mDNS service discovery and hostname publishing
3
+ // Similar to ESP32's MDNS.begin() functionality
4
+ //
5
+ // Johan Coppieters, Nov 2025
6
+
7
+ import * as Bonjour from 'bonjour-service';
8
+ import * as mdns from 'multicast-dns';
9
+ import * as os from 'os';
10
+ import { log, debug, err } from '../duotecno/logger';
11
+
12
+ export interface MDNSConfig {
13
+ name: string;
14
+ port: number | number[];
15
+ version?: string;
16
+ }
17
+
18
+ export class MDNSService {
19
+ private bonjour: any;
20
+ private mdns: any;
21
+ private config: MDNSConfig;
22
+
23
+ constructor(config: MDNSConfig) {
24
+ this.config = config;
25
+ }
26
+
27
+ /**
28
+ * Initialize mDNS service (call this before registering)
29
+ */
30
+ init() {
31
+ try {
32
+ this.bonjour = new Bonjour.Bonjour();
33
+ log("mDNS", "Service initialized");
34
+ } catch (e) {
35
+ err("mDNS", `Failed to initialize: ${e.message}`);
36
+ throw e;
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Register both HTTP service and hostname (like ESP32 MDNS.begin())
42
+ */
43
+ register() {
44
+ if (!this.bonjour) {
45
+ err("mDNS", "Service not initialized, call init() first");
46
+ return;
47
+ }
48
+
49
+ try {
50
+ const { name, port, version } = this.config;
51
+ const ports = Array.isArray(port) ? port : [port];
52
+ const primaryPort = ports[0];
53
+
54
+ // Register the HTTP service using bonjour
55
+ const service = this.bonjour.publish({
56
+ name,
57
+ type: 'http',
58
+ port: primaryPort,
59
+ txt: {
60
+ version: version || '1.0.0',
61
+ ports: ports.join(','),
62
+ path: '/'
63
+ }
64
+ });
65
+
66
+ log("mDNS", `HTTP service registered: ${name}.local on port(s) ${ports.join(', ')}`);
67
+
68
+ // Handle service errors
69
+ service.on('error', (error) => {
70
+ err("mDNS", `Service error: ${error.message}`);
71
+ });
72
+
73
+ // Publish the hostname A record (like ESP32 MDNS.begin())
74
+ this.publishHostname(name);
75
+
76
+ } catch (e) {
77
+ err("mDNS", `Failed to register service: ${e.message}`);
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Publish hostname as A/AAAA records (ESP32-style hostname publishing)
83
+ */
84
+ private publishHostname(hostname: string) {
85
+ try {
86
+ // Create multicast DNS instance
87
+ this.mdns = mdns();
88
+
89
+ // Get local IP addresses
90
+ const addresses = this.getLocalIPAddresses();
91
+
92
+ if (addresses.length === 0) {
93
+ err("mDNS", "No network interfaces found, cannot publish hostname");
94
+ return;
95
+ }
96
+
97
+ log("mDNS", `Publishing hostname ${hostname}.local with IPs: ${addresses.join(', ')}`);
98
+
99
+ // Respond to mDNS queries for our hostname (like ESP32 MDNS.begin())
100
+ this.mdns.on('query', (query) => {
101
+ // Check if someone is asking for our hostname
102
+ const hostnameQuery = query.questions.find(q =>
103
+ q.name === `${hostname}.local` && (q.type === 'A' || q.type === 'AAAA' || q.type === 'ANY')
104
+ );
105
+
106
+ if (hostnameQuery) {
107
+ // Respond with A records for all our IP addresses
108
+ const answers = addresses.map(ip => {
109
+ const isIPv6 = ip.includes(':');
110
+ return {
111
+ name: `${hostname}.local`,
112
+ type: isIPv6 ? 'AAAA' : 'A',
113
+ ttl: 120,
114
+ data: ip
115
+ };
116
+ });
117
+
118
+ this.mdns.respond(answers, () => {
119
+ debug("mDNS", `Responded to query for ${hostname}.local`);
120
+ });
121
+ }
122
+ });
123
+
124
+ // Send announcement (like ESP32 does on startup)
125
+ this.announceHostname(hostname, addresses);
126
+
127
+ log("mDNS", `Hostname responder started for ${hostname}.local`);
128
+
129
+ } catch (e) {
130
+ err("mDNS", `Failed to publish hostname: ${e.message}`);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Send unsolicited announcement (like ESP32 mDNS does on startup)
136
+ */
137
+ private announceHostname(hostname: string, addresses: string[]) {
138
+ const answers = addresses.map(ip => {
139
+ const isIPv6 = ip.includes(':');
140
+ return {
141
+ name: `${hostname}.local`,
142
+ type: isIPv6 ? 'AAAA' : 'A',
143
+ ttl: 120,
144
+ data: ip
145
+ };
146
+ });
147
+
148
+ this.mdns.respond(answers, () => {
149
+ log("mDNS", `Announced ${hostname}.local on the network`);
150
+ });
151
+ }
152
+
153
+ /**
154
+ * Get local IPv4 addresses (excluding loopback and virtual interfaces)
155
+ */
156
+ private getLocalIPAddresses(): string[] {
157
+ const addresses: string[] = [];
158
+ const interfaces = os.networkInterfaces();
159
+
160
+ for (const name in interfaces) {
161
+ const iface = interfaces[name];
162
+ if (!iface) continue;
163
+
164
+ for (const addr of iface) {
165
+ // Skip internal (loopback) and non-IPv4 addresses
166
+ if (addr.internal || addr.family !== 'IPv4') continue;
167
+
168
+ // Skip docker and virtual interfaces
169
+ if (name.startsWith('docker') || name.startsWith('veth')) continue;
170
+
171
+ addresses.push(addr.address);
172
+ }
173
+ }
174
+
175
+ return addresses;
176
+ }
177
+
178
+ /**
179
+ * Stop and cleanup mDNS services
180
+ */
181
+ destroy() {
182
+ try {
183
+ if (this.mdns) {
184
+ this.mdns.destroy();
185
+ this.mdns = null;
186
+ log("mDNS", "Hostname responder stopped");
187
+ }
188
+
189
+ if (this.bonjour) {
190
+ this.bonjour.destroy();
191
+ this.bonjour = null;
192
+ log("mDNS", "Service discovery stopped");
193
+ }
194
+ } catch (e) {
195
+ err("mDNS", `Error during cleanup: ${e.message}`);
196
+ }
197
+ }
198
+ }
@@ -83,6 +83,9 @@ class Platform extends base_1.Base {
83
83
  try {
84
84
  this.smartapp = new smartapp_1.SmartApp(this.system, this.power, this);
85
85
  this.smartapp.serve();
86
+ // Register webapp with proxy for graceful shutdown
87
+ const { setWebApp } = require('./proxy');
88
+ setWebApp(this.smartapp);
86
89
  }
87
90
  catch (err) {
88
91
  (0, logger_1.log)("homebridge", err);
@@ -111,6 +114,8 @@ class Platform extends base_1.Base {
111
114
  else {
112
115
  (0, logger_1.log)("homebridge", "platform - No SocApp configured.");
113
116
  }
117
+ // Always set proxy kind to "gw" when running under Platform/HomeBridge
118
+ // This prevents auto-restart on errors even if proxy is not configured
114
119
  if (this.config.proxy && ((_a = this.config.proxy) === null || _a === void 0 ? void 0 : _a.uniqueId)) {
115
120
  this.config.proxy.kind = "gw";
116
121
  (0, proxy_1.setProxyConfig)(this.config.proxy);
@@ -119,6 +124,8 @@ class Platform extends base_1.Base {
119
124
  }
120
125
  else {
121
126
  (0, logger_1.log)("homebridge", "platform - No Proxy configured.");
127
+ // Still set kind to "gw" to prevent auto-restart on web server errors
128
+ (0, proxy_1.setProxyConfig)(Object.assign(Object.assign({}, this.config.proxy), { kind: "gw" }));
122
129
  }
123
130
  }
124
131
  startPower(type, PowerMgr) {
@@ -89,6 +89,10 @@ export class Platform extends Base {
89
89
  try {
90
90
  this.smartapp = new SmartApp(this.system, this.power, this);
91
91
  this.smartapp.serve();
92
+
93
+ // Register webapp with proxy for graceful shutdown
94
+ const { setWebApp } = require('./proxy');
95
+ setWebApp(this.smartapp);
92
96
 
93
97
  } catch(err) {
94
98
  log("homebridge", err);
@@ -117,6 +121,8 @@ export class Platform extends Base {
117
121
  log("homebridge", "platform - No SocApp configured.");
118
122
  }
119
123
 
124
+ // Always set proxy kind to "gw" when running under Platform/HomeBridge
125
+ // This prevents auto-restart on errors even if proxy is not configured
120
126
  if (this.config.proxy && this.config.proxy?.uniqueId) {
121
127
  this.config.proxy.kind = "gw";
122
128
  setProxyConfig(this.config.proxy);
@@ -124,6 +130,8 @@ export class Platform extends Base {
124
130
  log("homebridge", "platform - Proxy configured: " + JSON.stringify(this.config.proxy, null, 2));
125
131
  } else {
126
132
  log("homebridge", "platform - No Proxy configured.");
133
+ // Still set kind to "gw" to prevent auto-restart on web server errors
134
+ setProxyConfig({ ...this.config.proxy, kind: "gw" });
127
135
  }
128
136
  }
129
137
 
package/server/proxy.js CHANGED
@@ -1,6 +1,15 @@
1
1
  "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
2
11
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Context = exports.cleanStart = exports.makeNewCloudConnection = exports.setProxyConfig = exports.kEmptyProxy = void 0;
12
+ exports.Context = exports.cleanStart = exports.makeNewCloudConnection = exports.setProxyConfig = exports.setWebApp = exports.kEmptyProxy = void 0;
4
13
  const net = require("net");
5
14
  const child_process_1 = require("child_process");
6
15
  const process_1 = require("process");
@@ -14,6 +23,12 @@ exports.kEmptyProxy = {
14
23
  kind: "solo" // default running under PM2 or other process manager -> don't restart yourself
15
24
  };
16
25
  const config = Object.assign({}, exports.kEmptyProxy);
26
+ // Store reference to the webapp so we can close servers before restarting
27
+ let gWebApp = null;
28
+ function setWebApp(webapp) {
29
+ gWebApp = webapp;
30
+ }
31
+ exports.setWebApp = setWebApp;
17
32
  function setProxyConfig(c) {
18
33
  // set the proxy config
19
34
  if (c) {
@@ -60,7 +75,18 @@ function error(str) {
60
75
  ///////////////////////////////
61
76
  // Start up the proxy server //
62
77
  ///////////////////////////////
63
- cleanStart(true);
78
+ // Only auto-start if we have a config-proxy.json file (standalone mode)
79
+ // When running under Platform/HomeBridge, let Platform control the startup
80
+ try {
81
+ require.resolve('../config-proxy.json');
82
+ // config-proxy.json exists, we're running standalone -> start the proxy
83
+ log("[PROXY] Running in standalone mode (config-proxy.json found), starting proxy...");
84
+ cleanStart(true);
85
+ }
86
+ catch (e) {
87
+ // No config-proxy.json, we're probably running under Platform/HomeBridge
88
+ log("[PROXY] No config-proxy.json found, waiting for Platform to start proxy...");
89
+ }
64
90
  function makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId, count = 1) {
65
91
  // retry making a cloud connection
66
92
  function reconnect(err) {
@@ -312,23 +338,36 @@ function restart() {
312
338
  }
313
339
  // Graceful shutdown handler
314
340
  function handleShutdown(signal) {
315
- log(`[PROXY] → [SYSTEM] Received ${signal}, shutting down gracefully...`);
316
- if (connectionChecker) {
317
- clearInterval(connectionChecker);
318
- connectionChecker = null;
319
- }
320
- // Close all cloud connections
321
- gCloudConnections.forEach(ctx => ctx.cleanupSockets());
322
- gCloudConnections.splice(0, gCloudConnections.length);
323
- log(`[PROXY] → [SYSTEM] All connections closed. Exiting.`);
324
- if (config.kind === "solo") {
325
- log(`[PROXY] → [SYSTEM] Restarting proxy in solo mode...`);
326
- restart();
327
- }
328
- else {
329
- log(`[PROXY] → [SYSTEM] Exiting without restart.`);
330
- setTimeout(() => process.exit(0), 100);
331
- }
341
+ return __awaiter(this, void 0, void 0, function* () {
342
+ log(`[PROXY] → [SYSTEM] Received ${signal}, shutting down gracefully...`);
343
+ if (connectionChecker) {
344
+ clearInterval(connectionChecker);
345
+ connectionChecker = null;
346
+ }
347
+ // Close all cloud connections
348
+ gCloudConnections.forEach(ctx => ctx.cleanupSockets());
349
+ gCloudConnections.splice(0, gCloudConnections.length);
350
+ // Close HTTP servers if available
351
+ if (gWebApp && typeof gWebApp.closeServers === 'function') {
352
+ log(`[PROXY] → [SYSTEM] Closing HTTP servers...`);
353
+ try {
354
+ yield gWebApp.closeServers();
355
+ }
356
+ catch (e) {
357
+ error(`[PROXY] → [SYSTEM] Error closing servers: ${e.message}`);
358
+ }
359
+ }
360
+ log(`[PROXY] → [SYSTEM] All connections closed. Exiting.`);
361
+ if (config.kind === "solo") {
362
+ log(`[PROXY] → [SYSTEM] Restarting proxy in solo mode...`);
363
+ restart();
364
+ }
365
+ else {
366
+ log(`[PROXY] → [SYSTEM] Exiting without restart.`);
367
+ // Give OS time to release ports before exit (helps prevent EADDRINUSE on restart)
368
+ setTimeout(() => process.exit(0), 500);
369
+ }
370
+ });
332
371
  }
333
372
  // Listen for termination signals
334
373
  process.on('SIGINT', () => handleShutdown('SIGINT'));
package/server/proxy.ts CHANGED
@@ -28,6 +28,13 @@ export const kEmptyProxy: ProxyConfig = {
28
28
 
29
29
  const config = {...kEmptyProxy};
30
30
 
31
+ // Store reference to the webapp so we can close servers before restarting
32
+ let gWebApp: any = null;
33
+
34
+ export function setWebApp(webapp: any) {
35
+ gWebApp = webapp;
36
+ }
37
+
31
38
  export function setProxyConfig(c?: ProxyConfig): ProxyConfig {
32
39
  // set the proxy config
33
40
  if (c) {
@@ -81,7 +88,17 @@ function error(str: string) {
81
88
  ///////////////////////////////
82
89
  // Start up the proxy server //
83
90
  ///////////////////////////////
84
- cleanStart(true);
91
+ // Only auto-start if we have a config-proxy.json file (standalone mode)
92
+ // When running under Platform/HomeBridge, let Platform control the startup
93
+ try {
94
+ require.resolve('../config-proxy.json');
95
+ // config-proxy.json exists, we're running standalone -> start the proxy
96
+ log("[PROXY] Running in standalone mode (config-proxy.json found), starting proxy...");
97
+ cleanStart(true);
98
+ } catch (e) {
99
+ // No config-proxy.json, we're probably running under Platform/HomeBridge
100
+ log("[PROXY] No config-proxy.json found, waiting for Platform to start proxy...");
101
+ }
85
102
 
86
103
 
87
104
  export function makeNewCloudConnection(master: string, masterPort: number, server: string, serverPort: number, uniqueId: string, count = 1) {
@@ -389,7 +406,7 @@ function restart() {
389
406
 
390
407
 
391
408
  // Graceful shutdown handler
392
- function handleShutdown(signal: string) {
409
+ async function handleShutdown(signal: string) {
393
410
  log(`[PROXY] → [SYSTEM] Received ${signal}, shutting down gracefully...`);
394
411
 
395
412
  if (connectionChecker) {
@@ -401,6 +418,16 @@ function handleShutdown(signal: string) {
401
418
  gCloudConnections.forEach(ctx => ctx.cleanupSockets());
402
419
  gCloudConnections.splice(0, gCloudConnections.length);
403
420
 
421
+ // Close HTTP servers if available
422
+ if (gWebApp && typeof gWebApp.closeServers === 'function') {
423
+ log(`[PROXY] → [SYSTEM] Closing HTTP servers...`);
424
+ try {
425
+ await gWebApp.closeServers();
426
+ } catch (e) {
427
+ error(`[PROXY] → [SYSTEM] Error closing servers: ${e.message}`);
428
+ }
429
+ }
430
+
404
431
  log(`[PROXY] → [SYSTEM] All connections closed. Exiting.`);
405
432
 
406
433
  if (config.kind === "solo") {
@@ -408,7 +435,8 @@ function handleShutdown(signal: string) {
408
435
  restart();
409
436
  } else {
410
437
  log(`[PROXY] → [SYSTEM] Exiting without restart.`);
411
- setTimeout(() => process.exit(0), 100);
438
+ // Give OS time to release ports before exit (helps prevent EADDRINUSE on restart)
439
+ setTimeout(() => process.exit(0), 500);
412
440
  }
413
441
  }
414
442