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.
- package/README.md +2 -0
- package/duotecno/types.js +1 -1
- package/package.json +1 -1
- package/server/base.js +4 -0
- package/server/mDNS.js +108 -38
- package/server/platform.js +7 -0
- package/server/proxy.js +99 -44
- package/server/smartapp.js +22 -32
- package/server/views/head.ejs +1 -0
- package/server/views/nav.ejs +16 -8
- package/server/views/proxy.ejs +196 -17
- package/server/views/style.ejs +2 -1
- package/server/webapp.js +7 -0
- package/.gitattributes +0 -4
- package/MIXINS-GUIDE.md +0 -210
- package/SETTINGS-UPDATE.md +0 -195
- package/TESTING-GUIDE.md +0 -220
- package/WEBSOCKET-API.md +0 -303
- package/accessories/accessory.ts +0 -128
- package/accessories/bulb.ts +0 -38
- package/accessories/dimmer.ts +0 -97
- package/accessories/door.ts +0 -88
- package/accessories/garagedoor.ts +0 -134
- package/accessories/lock.ts +0 -103
- package/accessories/mood.ts +0 -68
- package/accessories/switch.ts +0 -34
- package/accessories/temperature.ts +0 -55
- package/accessories/windowcovering.ts +0 -189
- package/config.homebridge.json +0 -23
- package/config.json +0 -49
- package/duotecno/Q.ts +0 -56
- package/duotecno/api.ts +0 -63
- package/duotecno/colors.ts +0 -96
- package/duotecno/config.ts +0 -125
- package/duotecno/logger.ts +0 -43
- package/duotecno/master.ts +0 -884
- package/duotecno/protocol.ts +0 -992
- package/duotecno/smartsocket.ts +0 -118
- package/duotecno/system.ts +0 -439
- package/duotecno/types.ts +0 -900
- package/index.ts +0 -9
- package/log.txt +0 -74
- package/raspberry-pi-setup.sh +0 -80
- package/server/HA-API.ts +0 -102
- package/server/HB.ts +0 -343
- package/server/base.ts +0 -54
- package/server/mDNS.ts +0 -223
- package/server/p1.ts +0 -226
- package/server/platform.ts +0 -305
- package/server/powerbase.ts +0 -317
- package/server/proxy.ts +0 -482
- package/server/shelly.ts +0 -212
- package/server/smappee.ts +0 -309
- package/server/smartapp.ts +0 -1710
- package/server/socapp.ts +0 -182
- package/server/somfy.ts +0 -174
- package/server/support.ts +0 -142
- package/server/webapp.ts +0 -688
- package/testHB.js +0 -41
- package/testHB.ts +0 -54
- package/testProxy.js +0 -7
- package/testProxy.ts +0 -6
- package/testWS.js +0 -137
- package/testWS.ts +0 -174
- package/update.sh +0 -16
- 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
|
+
"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
|
-
|
|
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/platform.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
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
|
|
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
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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
|
-
|
|
258
|
-
|
|
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
|
|
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]
|
|
370
|
-
|
|
371
|
-
|
|
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
|
}
|
package/server/smartapp.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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=...®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
|
+
}
|
|
1408
1398
|
else if (context.action === "set") {
|
|
1409
1399
|
if (!master)
|
|
1410
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"/>
|