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 +3 -0
- package/package.json +2 -4
- package/server/base.js +4 -0
- package/server/mDNS.js +108 -38
- package/server/proxy.js +47 -20
- package/server/smartapp.js +23 -3
- package/server/views/head.ejs +1 -0
- package/server/views/nav.ejs +16 -8
- package/server/views/proxy.ejs +124 -18
- package/server/views/style.ejs +2 -1
- package/server/webapp.js +7 -0
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.
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
91
|
+
// Unpublish the conflicting service
|
|
92
|
+
if (service) {
|
|
93
|
+
try {
|
|
94
|
+
service.stop();
|
|
62
95
|
}
|
|
63
|
-
|
|
64
|
-
|
|
96
|
+
catch (e) {
|
|
97
|
+
// Ignore errors during stop
|
|
65
98
|
}
|
|
66
|
-
// Continue anyway - service will work once the old registration expires
|
|
67
99
|
}
|
|
68
|
-
|
|
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
|
-
(
|
|
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
|
-
//
|
|
81
|
-
|
|
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 (
|
|
84
|
-
if (
|
|
85
|
-
|
|
86
|
-
(0, logger_1.err)("mDNS", "
|
|
87
|
-
(
|
|
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
|
-
|
|
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
|
|
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
|
|
131
|
-
|
|
132
|
-
|
|
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
|
|
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
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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') {
|
package/server/smartapp.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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
|
|
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=...®ister=...&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);
|
package/server/views/head.ejs
CHANGED
|
@@ -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"/>
|
package/server/views/nav.ejs
CHANGED
|
@@ -49,15 +49,23 @@
|
|
|
49
49
|
</ul>
|
|
50
50
|
|
|
51
51
|
<% if (notReady) { %>
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
<
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
<form>
|
|
64
|
+
<div class="card-panel green">
|
|
65
|
+
<h2>Message</h2>
|
|
66
|
+
<div>
|
|
67
|
+
<%= message %>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
</form>
|
|
63
71
|
<% } %>
|
package/server/views/proxy.ejs
CHANGED
|
@@ -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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
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
|
-
<
|
|
37
|
-
|
|
38
|
-
|
|
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
|
-
<
|
|
45
|
-
|
|
46
|
-
|
|
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="
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
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.
|
|
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>
|
package/server/views/style.ejs
CHANGED
|
@@ -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:
|
|
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
|
}
|