homebridge-smartsystem 7.0.6 → 7.1.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.
package/README.md CHANGED
@@ -322,6 +322,9 @@ Homebridge UI version
322
322
  - 4: fixed a bug when shelly has empty list of ip addresses
323
323
  - 5: better error handling in the proxy.ts + show connection in the /proxy page
324
324
 
325
+ ### v.7.1.x - jan 2026
326
+ - 0: master + config ports in the proxy
327
+ - 1: fix for reading the proxy config
325
328
 
326
329
  ## Hardware
327
330
  A Raspberry Pi that connects to a Duotecno IP Node
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-smartsystem",
3
3
  "displayname": "Duotecno Bridge",
4
- "version": "7.0.6",
4
+ "version": "7.1.1",
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",
@@ -13,9 +13,7 @@
13
13
  "build": "npx tsc --build",
14
14
  "unpublish": "npm unpublish --force homebridge-smartsystem@6.8.17",
15
15
  "prepublishOnly": "npm run build",
16
- "publish": "npm publish",
17
- "version:patch": "npm version patch && npm publish",
18
- "version:minor": "npm version minor && npm publish"
16
+ "to-publish": "npm publish"
19
17
  },
20
18
  "engines": {
21
19
  "node": "^18.20.4 || ^20.15.1 || ^22",
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
  }
package/server/proxy.js CHANGED
@@ -16,9 +16,12 @@ 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
26
  kind: "gw" // default running under the gateway or other process manager -> don't restart yourself
24
27
  };
@@ -34,24 +37,18 @@ 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
53
  exports.gCloudConnections = [];
57
54
  const kDebug = config.debug || false; // enable debug mode from config
@@ -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) {
@@ -127,10 +125,28 @@ function cleanStart(restart = false) {
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
152
  const freeConnection = exports.gCloudConnections.find((ctx) => (!ctx.deviceSocket) && ctx.cloudSocket && (ctx.cloudSocket.readyState === 'open'));
@@ -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,8 @@ 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 === "";
164
182
  this.deviceStatus = {
165
183
  connected: false,
166
184
  lastAttempt: null,
@@ -206,12 +224,21 @@ class Context {
206
224
  // Handle connection response from the cloud server
207
225
  }
208
226
  else {
209
- // There is real incomming data, it's a fresh connection, so: a new client wants to connect to the device
210
- // 1) create a new (free) connection to the cloud server for the next client
211
- // 2) connect to the device and send the initial data
212
- log(`[PROXY] → [CLOUD] New client connection, creating new free connection for this proxy/device: ${config.uniqueId}.`);
213
- makeNewCloudConnection(this.master, this.masterPort, this.server, this.serverPort, this.uniqueId);
214
- 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
+ }
215
242
  }
216
243
  }
217
244
  else if (this.deviceSocket.readyState === 'open') {
@@ -322,16 +322,21 @@ class SmartApp extends webapp_1.WebApp {
322
322
  ///////////
323
323
  doProxy(context) {
324
324
  return __awaiter(this, void 0, void 0, function* () {
325
- let config = this.config.proxy || proxy_1.kEmptyProxy;
325
+ let config = this.read("proxy") || proxy_1.kEmptyProxy;
326
326
  if (context.action === "save") {
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, connections: proxy_1.gCloudConnections });
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
  //////////////
@@ -1359,7 +1370,7 @@ class SmartApp extends webapp_1.WebApp {
1359
1370
  //////////////////////////////
1360
1371
  doUnits(context) {
1361
1372
  return __awaiter(this, void 0, void 0, function* () {
1362
- // get masterAddress and Port
1373
+ // get master Address and Port into "masterAddress" & "masterPort"
1363
1374
  context.getMaster("action", "node");
1364
1375
  const master = this.system.findMaster(context["masterAddress"], context["masterPort"]);
1365
1376
  if (context.action === "save") {
@@ -1375,6 +1386,15 @@ class SmartApp extends webapp_1.WebApp {
1375
1386
  context.action = "edit";
1376
1387
  return this.doRequest(context);
1377
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
+ }
1378
1398
  else if (context.action === "set") {
1379
1399
  if (!master)
1380
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"/>
@@ -49,15 +49,23 @@
49
49
  </ul>
50
50
 
51
51
  <% if (notReady) { %>
52
- <h2>Not ready</h2>
53
- <div>
54
- <%= notReadyMessage %>
55
- </div>
52
+ <form>
53
+ <div class="card-panel red">
54
+ <h2>Not ready</h2>
55
+ <div>
56
+ <%= notReadyMessage %>
57
+ </div>
58
+ </div>
59
+ </form>
56
60
  <% } %>
57
61
 
58
62
  <% if (message) { %>
59
- <h2>Message</h2>
60
- <div>
61
- <%= message %>
62
- </div>
63
+ <form>
64
+ <div class="card-panel green">
65
+ <h2>Message</h2>
66
+ <div>
67
+ <%= message %>
68
+ </div>
69
+ </div>
70
+ </form>
63
71
  <% } %>
@@ -16,53 +16,145 @@
16
16
  <button class="right btn waves-effect waves-light red" type="submit" name="action" value="restart">Restart</button>
17
17
  </h1>
18
18
 
19
- <div class="row">
19
+ <h2 style="margin-top: 2rem; font-size: 1.5rem; font-weight: 500;">
20
+ <i class="material-icons left" style="vertical-align: middle;">cloud</i>
21
+ Cloud Server Settings
22
+ </h2>
23
+
24
+ <div class="row" style="margin-top: 1rem;">
20
25
  <div class="input-field col s12">
21
- <input placeholder="ws.duotecno.eu" id="cloudServer" name="cloudServer" type="text"
26
+ <label for="cloudServer">Cloud Server Address</label>
27
+ <input placeholder="masters.duotecno.eu" id="cloudServer" name="cloudServer" type="text"
22
28
  value="<%= config.cloudServer %>">
23
- <label for="name">CloudServer</label>
24
29
  </div>
25
30
  </div>
31
+
26
32
  <div class="row">
27
33
  <div class="input-field col s12">
28
- <input placeholder="5099" id="cloudPort" name="cloudPort" type="number"
34
+ <label for="cloudPort">Cloud Server Port</label>
35
+ <input placeholder="5097" id="cloudPort" name="cloudPort" type="number"
29
36
  value="<%= config.cloudPort %>">
30
- <label for="name">CloudPort</label>
31
37
  </div>
32
38
  </div>
33
39
 
34
- <div class="row">
40
+ <h2 style="margin-top: 3rem; font-size: 1.5rem; font-weight: 500;">
41
+ <i class="material-icons left" style="vertical-align: middle;">home</i>
42
+ Master Device
43
+ </h2>
44
+ <p class="grey-text" style="margin-bottom: 2rem;">
45
+ Configure your local master device with 2 ports.</small>
46
+ </p>
47
+
48
+ <div class="row" style="margin-top: 1rem;">
35
49
  <div class="input-field col s12">
36
- <input placeholder="192.168.0.2" id="masterAddress" name="masterAddress" type="text"
37
- value="<%= config.masterAddress %>">
38
- <label for="name">MasterAddress</label>
50
+ <label for="uniqueId">Unique ID (without port)</label>
51
+ <input placeholder="john-doe.duotecno.be" id="uniqueId" name="uniqueId" type="text"
52
+ value="<%= config.uniqueId %>">
53
+ <span class="helper-text">
54
+ This ID is used for both connections:<br>
55
+ • Websocket: [uniqueId]:5001<br>
56
+ • Config API: [uniqueId]:8080<br>
57
+ </span>
39
58
  </div>
40
59
  </div>
41
60
 
42
61
  <div class="row">
43
62
  <div class="input-field col s12">
44
- <input placeholder="5001" id="masterPort" name="masterPort" type="text"
45
- value="<%= config.masterPort %>">
46
- <label for="name">MasterPort</label>
63
+ <label for="masterAddress">IP Address</label>
64
+ <input placeholder="192.168.0.97" id="masterAddress" name="masterAddress" type="text"
65
+ value="<%= config.masterAddress %>">
47
66
  </div>
48
67
  </div>
49
68
 
50
69
  <div class="row">
51
- <div class="input-field col s12">
52
- <input placeholder="client:5002" id="uniqueId" name="uniqueId" type="text"
53
- value="<%= config.uniqueId %>">
54
- <label for="name">UniqueId</label>
70
+ <div class="col s6">
71
+ <p>
72
+ <label>
73
+ <input type="checkbox" class="filled-in" checked disabled />
74
+ <span><strong>Websocket</strong></span>
75
+ </label>
76
+ </p>
77
+ <div class="input-field">
78
+ <input placeholder="5001" id="masterPort" name="masterPort" type="number"
79
+ value="<%= config.masterPort || 5001 %>">
80
+ </div>
81
+ </div>
82
+ <div class="col s6">
83
+ <p>
84
+ <label>
85
+ <input type="checkbox" class="filled-in" checked disabled />
86
+ <span><strong>Config port</strong></span>
87
+ </label>
88
+ </p>
89
+ <div class="input-field">
90
+ <input placeholder="8080" id="masterConfigPort" name="masterConfigPort" type="number"
91
+ value="<%= config.masterConfigPort || 8080 %>">
92
+ </div>
55
93
  </div>
56
94
  </div>
57
95
 
96
+ <!-- Hidden fields for compatibility - these are no longer shown in UI -->
97
+ <input type="hidden" name="configPort" value="<%= config.configPort || config.cloudPort %>">
98
+ <input type="hidden" name="configUniqueId" value="<%= config.configUniqueId || '' %>">
99
+
58
100
  </form>
59
101
 
102
+ <!-- mDNS Status Section -->
103
+ <% if (mdnsStatus) { %>
104
+ <form>
105
+ <h1>mDNS Status</h1>
106
+ <% if (mdnsStatus.registrationStatus === 'success') { %>
107
+ <div class="card-panel green lighten-4">
108
+ <span class="green-text text-darken-4">
109
+ <i class="material-icons left">check_circle</i>
110
+ <strong>mDNS Service Registered Successfully</strong>
111
+ </span>
112
+ <p>Hostname: <strong><%= mdnsStatus.registeredName %>.local</strong></p>
113
+ <% if (mdnsStatus.registeredName && !mdnsStatus.registeredName.includes('.')) { %>
114
+ <p class="grey-text">The gateway is accessible on the network as <%= mdnsStatus.registeredName %>.local</p>
115
+ <% } else { %>
116
+ <p class="orange-text">
117
+ <i class="material-icons tiny">warning</i>
118
+ Note: Original name was taken, registered with suffix. Check Settings to change the mDNS name.
119
+ </p>
120
+ <% } %>
121
+ </div>
122
+ <% } else if (mdnsStatus.registrationStatus === 'failed') { %>
123
+ <div class="card-panel red lighten-4">
124
+ <span class="red-text text-darken-4">
125
+ <i class="material-icons left">error</i>
126
+ <strong>mDNS Registration Failed</strong>
127
+ </span>
128
+ <% if (mdnsStatus.registrationError) { %>
129
+ <p>Error: <%= mdnsStatus.registrationError %></p>
130
+ <% } %>
131
+ <p>The gateway is still running, but may not be discoverable via mDNS hostname.</p>
132
+ <p class="grey-text">Solutions:</p>
133
+ <ul class="grey-text">
134
+ <li>Change the mDNS name in <a href="/settings">Settings</a></li>
135
+ <li>Stop other devices/services using the same name</li>
136
+ <li>Access the gateway using IP address instead</li>
137
+ </ul>
138
+ </div>
139
+ <% } else { %>
140
+ <div class="card-panel blue lighten-4">
141
+ <span class="blue-text text-darken-4">
142
+ <i class="material-icons left">info</i>
143
+ mDNS registration is pending...
144
+ </span>
145
+ </div>
146
+ <% } %>
147
+ </form>
148
+ <% } %>
149
+
60
150
  <!-- Connection Status Section -->
61
151
  <form>
62
152
  <% if (connections && connections.length > 0) { %>
63
153
  <table class="striped highlight">
64
154
  <thead>
65
155
  <tr>
156
+ <th>Type</th>
157
+ <th>Unique ID</th>
66
158
  <th>Cloud Connection</th>
67
159
  <th>Device Connection</th>
68
160
  <th>Last Attempt</th>
@@ -74,6 +166,16 @@
74
166
  <tbody>
75
167
  <% connections.forEach(function(conn) { %>
76
168
  <tr>
169
+ <td>
170
+ <% if (conn.isGateway) { %>
171
+ <span class="blue-text"><strong>Gateway</strong></span>
172
+ <% } else { %>
173
+ <span class="teal-text"><strong>Master</strong></span>
174
+ <% } %>
175
+ </td>
176
+ <td>
177
+ <small><%= conn.uniqueId %></small>
178
+ </td>
77
179
  <td>
78
180
  <% if (conn.cloudSocket && conn.cloudSocket.readyState === 'open') { %>
79
181
  <span class="green-text">✓ Connected</span><br>
@@ -83,12 +185,16 @@
83
185
  <% } %>
84
186
  </td>
85
187
  <td>
86
- <% if (conn.deviceStatus.connected) { %>
188
+ <% if (conn.isGateway) { %>
189
+ <span class="grey-text">N/A (HTTP)</span>
190
+ <% } else if (conn.deviceStatus.connected) { %>
87
191
  <span class="green-text">✓ Connected</span><br>
88
192
  <small><%= conn.master %>:<%= conn.masterPort %></small>
89
- <% } else { %>
193
+ <% } else if (conn.master) { %>
90
194
  <span class="red-text">✗ Disconnected</span><br>
91
195
  <small><%= conn.master %>:<%= conn.masterPort %></small>
196
+ <% } else { %>
197
+ <span class="grey-text">Not configured</span>
92
198
  <% } %>
93
199
  </td>
94
200
  <td>
@@ -6,7 +6,8 @@
6
6
  /* Main content area - grows to fill space */
7
7
  body > *:not(nav):not(.page-footer) { flex: 1 0 auto }
8
8
 
9
- form { padding-left: 7px; padding-right: 9px; padding-bottom: 80px }
9
+ form { padding-left: 7px; padding-right: 9px; padding-bottom: 20px }
10
+ form:last-of-type { padding-bottom: 80px }
10
11
 
11
12
  nav, nav .nav-wrapper { height: 64px !important; line-height: 64px !important; flex-shrink: 0 }
12
13
  nav .brand-logo { left: 10px !important; -webkit-transform: none !important; transform: none !important }
package/server/webapp.js CHANGED
@@ -346,6 +346,11 @@ class WebApp extends base_1.Base {
346
346
  */
347
347
  initMDNS(name, version) {
348
348
  try {
349
+ // Cleanup any existing mDNS service first (important for hot reload/restart)
350
+ if (this.mdnsService) {
351
+ (0, logger_1.log)("webapp", "Cleaning up existing mDNS service before reinitializing");
352
+ this.destroyMDNS();
353
+ }
349
354
  const ports = Array.isArray(this.port) ? this.port : [this.port];
350
355
  this.mdnsService = new mDNS_1.MDNSService({
351
356
  name,
@@ -369,6 +374,8 @@ class WebApp extends base_1.Base {
369
374
  (0, logger_1.err)("webapp", "mDNS service not initialized, cannot register");
370
375
  return;
371
376
  }
377
+ // The mDNS service now handles conflicts internally with suffix retry
378
+ // No need for additional error handling here
372
379
  try {
373
380
  this.mdnsService.register();
374
381
  }