homebridge-smartsystem 7.0.4 → 7.0.6
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/platform.js +7 -0
- package/server/proxy.js +52 -24
- package/server/smartapp.js +1 -31
- package/server/views/proxy.ejs +73 -1
- 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.0.6",
|
|
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/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,7 +9,7 @@ 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");
|
|
@@ -20,7 +20,7 @@ exports.kEmptyProxy = {
|
|
|
20
20
|
masterPort: 5001,
|
|
21
21
|
uniqueId: "",
|
|
22
22
|
debug: false,
|
|
23
|
-
kind: "gw" // default running under
|
|
23
|
+
kind: "gw" // default running under the gateway or other process manager -> don't restart yourself
|
|
24
24
|
};
|
|
25
25
|
const config = Object.assign({}, exports.kEmptyProxy);
|
|
26
26
|
// Store reference to the webapp so we can close servers before restarting
|
|
@@ -53,7 +53,7 @@ catch (e) {
|
|
|
53
53
|
log("[PROXY] No config-proxy.json found, using default values from homebridge or other.");
|
|
54
54
|
}
|
|
55
55
|
let connectionChecker = null;
|
|
56
|
-
|
|
56
|
+
exports.gCloudConnections = [];
|
|
57
57
|
const kDebug = config.debug || false; // enable debug mode from config
|
|
58
58
|
/////////////
|
|
59
59
|
// Logging //
|
|
@@ -108,8 +108,8 @@ function makeNewCloudConnection(master, masterPort, server, serverPort, uniqueId
|
|
|
108
108
|
cloudSocket.write(`[${uniqueId}]`);
|
|
109
109
|
debug(`[PROXY] → [CLOUD] Sent unique ID: ${uniqueId}`);
|
|
110
110
|
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}`);
|
|
111
|
+
exports.gCloudConnections.push(context);
|
|
112
|
+
log(`[PROXY] → [CLOUD] New free connection #${exports.gCloudConnections.length} to the Cloud at ${server}:${serverPort}`);
|
|
113
113
|
});
|
|
114
114
|
}
|
|
115
115
|
exports.makeNewCloudConnection = makeNewCloudConnection;
|
|
@@ -117,10 +117,10 @@ exports.makeNewCloudConnection = makeNewCloudConnection;
|
|
|
117
117
|
// Restarting //
|
|
118
118
|
////////////////
|
|
119
119
|
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
|
|
120
|
+
if (exports.gCloudConnections === null || exports.gCloudConnections === void 0 ? void 0 : exports.gCloudConnections.length) {
|
|
121
|
+
exports.gCloudConnections.forEach(ctx => ctx.cleanupSockets());
|
|
122
|
+
log(`[PROXY] → [CLOUD] Cleaned up all ${exports.gCloudConnections.length} cloud connections.`);
|
|
123
|
+
exports.gCloudConnections.splice(0, exports.gCloudConnections.length); // clear the array
|
|
124
124
|
}
|
|
125
125
|
if (connectionChecker) {
|
|
126
126
|
clearInterval(connectionChecker);
|
|
@@ -133,7 +133,7 @@ function cleanStart(restart = false) {
|
|
|
133
133
|
makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
|
|
134
134
|
// check every 16 second for a free connection
|
|
135
135
|
connectionChecker = setInterval(() => {
|
|
136
|
-
const freeConnection = gCloudConnections.find((ctx) => (!ctx.deviceSocket) && ctx.cloudSocket && (ctx.cloudSocket.readyState === 'open'));
|
|
136
|
+
const freeConnection = exports.gCloudConnections.find((ctx) => (!ctx.deviceSocket) && ctx.cloudSocket && (ctx.cloudSocket.readyState === 'open'));
|
|
137
137
|
if (freeConnection) {
|
|
138
138
|
debug(`[PROXY] → [CLOUD] Found free connection #${freeConnection.uniqueId} to the Cloud -> we're still ok.`);
|
|
139
139
|
}
|
|
@@ -161,6 +161,13 @@ class Context {
|
|
|
161
161
|
this.server = server;
|
|
162
162
|
this.serverPort = serverPort;
|
|
163
163
|
this.uniqueId = uniqueId;
|
|
164
|
+
this.deviceStatus = {
|
|
165
|
+
connected: false,
|
|
166
|
+
lastAttempt: null,
|
|
167
|
+
lastSuccess: null,
|
|
168
|
+
lastError: null,
|
|
169
|
+
retryCount: 0
|
|
170
|
+
};
|
|
164
171
|
this.setupCloudSocket();
|
|
165
172
|
}
|
|
166
173
|
setupCloudSocket() {
|
|
@@ -217,8 +224,25 @@ class Context {
|
|
|
217
224
|
}
|
|
218
225
|
makeDeviceConnection(address, port, data) {
|
|
219
226
|
log(`[PROXY] → [DEVICE] Attempting to connect to local device at ${address}:${port}`);
|
|
227
|
+
this.deviceStatus.lastAttempt = new Date();
|
|
220
228
|
if (!this.deviceSocket) {
|
|
221
229
|
this.deviceSocket = new net.Socket();
|
|
230
|
+
// Add error handler BEFORE connecting to prevent uncaught exceptions
|
|
231
|
+
this.deviceSocket.on('error', (err) => {
|
|
232
|
+
error(`[DEVICE] → [PROXY] Connection error: ${err.message}`);
|
|
233
|
+
// Update device status
|
|
234
|
+
this.deviceStatus.connected = false;
|
|
235
|
+
this.deviceStatus.lastError = err.message;
|
|
236
|
+
this.deviceStatus.retryCount++;
|
|
237
|
+
log(`[DEVICE] → [PROXY] Connection failed. Will retry on next cloud request.`);
|
|
238
|
+
// Clean up this socket
|
|
239
|
+
if (this.deviceSocket && !this.deviceSocket.destroyed) {
|
|
240
|
+
this.deviceSocket.destroy();
|
|
241
|
+
}
|
|
242
|
+
this.deviceSocket = null;
|
|
243
|
+
// Don't create a new cloud connection - we already have one!
|
|
244
|
+
// The device connection will be retried when the next data comes from the cloud
|
|
245
|
+
});
|
|
222
246
|
// Connect to local device and send the data that came in from the cloud
|
|
223
247
|
this.deviceSocket.connect(port, address, () => {
|
|
224
248
|
// Check if socket is still valid (not cleaned up by removeConnection)
|
|
@@ -227,6 +251,11 @@ class Context {
|
|
|
227
251
|
return;
|
|
228
252
|
}
|
|
229
253
|
log(`[PROXY] → [DEVICE] Connected to local device at ${address}:${port}`);
|
|
254
|
+
// Update device status
|
|
255
|
+
this.deviceStatus.connected = true;
|
|
256
|
+
this.deviceStatus.lastSuccess = new Date();
|
|
257
|
+
this.deviceStatus.lastError = null;
|
|
258
|
+
this.deviceStatus.retryCount = 0;
|
|
230
259
|
this.setUpDeviceSocket();
|
|
231
260
|
log(`[PROXY] → [DEVICE] Sending initial message: ${data.toString().trim()}`);
|
|
232
261
|
this.deviceSocket.write(data);
|
|
@@ -254,10 +283,8 @@ class Context {
|
|
|
254
283
|
log(`[DEVICE] → [PROXY] Device socket closed`);
|
|
255
284
|
this.removeConnection();
|
|
256
285
|
});
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
this.removeConnection();
|
|
260
|
-
});
|
|
286
|
+
// Note: error handler is already added in makeDeviceConnection()
|
|
287
|
+
// Don't add duplicate error handler here
|
|
261
288
|
}
|
|
262
289
|
cleanupSockets() {
|
|
263
290
|
// Clean up the device socket
|
|
@@ -280,12 +307,12 @@ class Context {
|
|
|
280
307
|
}
|
|
281
308
|
}
|
|
282
309
|
removeConnection() {
|
|
283
|
-
const index = gCloudConnections.indexOf(this);
|
|
310
|
+
const index = exports.gCloudConnections.indexOf(this);
|
|
284
311
|
if (index !== -1)
|
|
285
|
-
gCloudConnections.splice(index, 1);
|
|
312
|
+
exports.gCloudConnections.splice(index, 1);
|
|
286
313
|
this.cleanupSockets();
|
|
287
314
|
log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
|
|
288
|
-
if (gCloudConnections && gCloudConnections.length === 0) {
|
|
315
|
+
if (exports.gCloudConnections && exports.gCloudConnections.length === 0) {
|
|
289
316
|
log(`[PROXY] → [CLOUD] No more cloud connections, restarting proxy.`);
|
|
290
317
|
// just to be sure: restart...
|
|
291
318
|
if (config.kind === "pm2") {
|
|
@@ -346,9 +373,9 @@ function handleShutdown(signal) {
|
|
|
346
373
|
connectionChecker = null;
|
|
347
374
|
}
|
|
348
375
|
// Close all cloud connections
|
|
349
|
-
if (gCloudConnections && gCloudConnections.length > 0) {
|
|
350
|
-
gCloudConnections.forEach(ctx => ctx.cleanupSockets());
|
|
351
|
-
gCloudConnections.splice(0, gCloudConnections.length);
|
|
376
|
+
if (exports.gCloudConnections && exports.gCloudConnections.length > 0) {
|
|
377
|
+
exports.gCloudConnections.forEach(ctx => ctx.cleanupSockets());
|
|
378
|
+
exports.gCloudConnections.splice(0, exports.gCloudConnections.length);
|
|
352
379
|
}
|
|
353
380
|
// Close HTTP servers if available
|
|
354
381
|
if (gWebApp && typeof gWebApp.closeServers === 'function') {
|
|
@@ -360,15 +387,16 @@ function handleShutdown(signal) {
|
|
|
360
387
|
error(`[PROXY] → [SYSTEM] Error closing servers: ${e.message}`);
|
|
361
388
|
}
|
|
362
389
|
}
|
|
363
|
-
log(`[PROXY] → [SYSTEM] All connections closed
|
|
390
|
+
log(`[PROXY] → [SYSTEM] All connections closed.`);
|
|
364
391
|
if (config.kind === "solo") {
|
|
365
392
|
log(`[PROXY] → [SYSTEM] Restarting proxy in solo mode...`);
|
|
366
393
|
restart();
|
|
367
394
|
}
|
|
368
395
|
else {
|
|
369
|
-
log(`[PROXY] → [SYSTEM]
|
|
370
|
-
|
|
371
|
-
|
|
396
|
+
log(`[PROXY] → [SYSTEM] Running under HomeBridge/Gateway - NOT calling process.exit() to avoid restarting entire system.`);
|
|
397
|
+
log(`[PROXY] → [SYSTEM] Plugin will remain running. Check /settings page for connection status.`);
|
|
398
|
+
// Don't call process.exit() when running under HomeBridge!
|
|
399
|
+
// This would restart the entire HomeBridge + UI system
|
|
372
400
|
}
|
|
373
401
|
});
|
|
374
402
|
}
|
package/server/smartapp.js
CHANGED
|
@@ -339,7 +339,7 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
339
339
|
else {
|
|
340
340
|
// just show the proxy config
|
|
341
341
|
}
|
|
342
|
-
return this.ejs("proxy", context, { config });
|
|
342
|
+
return this.ejs("proxy", context, { config, connections: proxy_1.gCloudConnections });
|
|
343
343
|
});
|
|
344
344
|
}
|
|
345
345
|
//////////////
|
|
@@ -363,36 +363,6 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
363
363
|
context.request = "restart";
|
|
364
364
|
return this.doRestart(false);
|
|
365
365
|
}
|
|
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
366
|
else {
|
|
397
367
|
// Just display the settings form
|
|
398
368
|
}
|
package/server/views/proxy.ejs
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
<body>
|
|
8
8
|
<%- include('/views/nav', this); -%>
|
|
9
9
|
|
|
10
|
-
<form method="POST" action="/proxy">
|
|
10
|
+
<form method="POST" action="/proxy" style="padding-bottom: 0">
|
|
11
11
|
<input type="hidden" name="daction" value="save">
|
|
12
12
|
<h1>
|
|
13
13
|
Proxy Config
|
|
@@ -56,6 +56,78 @@
|
|
|
56
56
|
</div>
|
|
57
57
|
|
|
58
58
|
</form>
|
|
59
|
+
|
|
60
|
+
<!-- Connection Status Section -->
|
|
61
|
+
<form>
|
|
62
|
+
<% if (connections && connections.length > 0) { %>
|
|
63
|
+
<table class="striped highlight">
|
|
64
|
+
<thead>
|
|
65
|
+
<tr>
|
|
66
|
+
<th>Cloud Connection</th>
|
|
67
|
+
<th>Device Connection</th>
|
|
68
|
+
<th>Last Attempt</th>
|
|
69
|
+
<th>Last Success</th>
|
|
70
|
+
<th>Retry Count</th>
|
|
71
|
+
<th>Last Error</th>
|
|
72
|
+
</tr>
|
|
73
|
+
</thead>
|
|
74
|
+
<tbody>
|
|
75
|
+
<% connections.forEach(function(conn) { %>
|
|
76
|
+
<tr>
|
|
77
|
+
<td>
|
|
78
|
+
<% if (conn.cloudSocket && conn.cloudSocket.readyState === 'open') { %>
|
|
79
|
+
<span class="green-text">✓ Connected</span><br>
|
|
80
|
+
<small><%= conn.server %>:<%= conn.serverPort %></small>
|
|
81
|
+
<% } else { %>
|
|
82
|
+
<span class="red-text">✗ Disconnected</span>
|
|
83
|
+
<% } %>
|
|
84
|
+
</td>
|
|
85
|
+
<td>
|
|
86
|
+
<% if (conn.deviceStatus.connected) { %>
|
|
87
|
+
<span class="green-text">✓ Connected</span><br>
|
|
88
|
+
<small><%= conn.master %>:<%= conn.masterPort %></small>
|
|
89
|
+
<% } else { %>
|
|
90
|
+
<span class="red-text">✗ Disconnected</span><br>
|
|
91
|
+
<small><%= conn.master %>:<%= conn.masterPort %></small>
|
|
92
|
+
<% } %>
|
|
93
|
+
</td>
|
|
94
|
+
<td>
|
|
95
|
+
<% if (conn.deviceStatus.lastAttempt) { %>
|
|
96
|
+
<%= new Date(conn.deviceStatus.lastAttempt).toLocaleString() %>
|
|
97
|
+
<% } else { %>
|
|
98
|
+
<span class="grey-text">Never</span>
|
|
99
|
+
<% } %>
|
|
100
|
+
</td>
|
|
101
|
+
<td>
|
|
102
|
+
<% if (conn.deviceStatus.lastSuccess) { %>
|
|
103
|
+
<%= new Date(conn.deviceStatus.lastSuccess).toLocaleString() %>
|
|
104
|
+
<% } else { %>
|
|
105
|
+
<span class="grey-text">Never</span>
|
|
106
|
+
<% } %>
|
|
107
|
+
</td>
|
|
108
|
+
<td>
|
|
109
|
+
<% if (conn.deviceStatus.retryCount > 0) { %>
|
|
110
|
+
<span class="orange-text"><%= conn.deviceStatus.retryCount %></span>
|
|
111
|
+
<% } else { %>
|
|
112
|
+
<%= conn.deviceStatus.retryCount %>
|
|
113
|
+
<% } %>
|
|
114
|
+
</td>
|
|
115
|
+
<td>
|
|
116
|
+
<% if (conn.deviceStatus.lastError) { %>
|
|
117
|
+
<span class="red-text"><%= conn.deviceStatus.lastError %></span>
|
|
118
|
+
<% } else { %>
|
|
119
|
+
<span class="grey-text">None</span>
|
|
120
|
+
<% } %>
|
|
121
|
+
</td>
|
|
122
|
+
</tr>
|
|
123
|
+
<% }); %>
|
|
124
|
+
</tbody>
|
|
125
|
+
</table>
|
|
126
|
+
<% } else { %>
|
|
127
|
+
<p class="red-text">No active connections</p>
|
|
128
|
+
<% } %>
|
|
129
|
+
</form>
|
|
130
|
+
|
|
59
131
|
<%- include('/views/footer'); -%>
|
|
60
132
|
|
|
61
133
|
</body>
|
package/.gitattributes
DELETED
package/MIXINS-GUIDE.md
DELETED
|
@@ -1,210 +0,0 @@
|
|
|
1
|
-
# TypeScript Mixins Pattern for SmartApp
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
Your SmartApp is getting large because it handles multiple concerns:
|
|
6
|
-
- Device management (WebSocket, power measurements)
|
|
7
|
-
- Link management (Homebridge accessories)
|
|
8
|
-
- Switch management (HTTP switches)
|
|
9
|
-
- Power management (Smappee, P1, Shelly)
|
|
10
|
-
- Proxy configuration
|
|
11
|
-
- Settings management
|
|
12
|
-
|
|
13
|
-
TypeScript doesn't support multiple inheritance, but **mixins** provide a similar capability.
|
|
14
|
-
|
|
15
|
-
## Solution Options
|
|
16
|
-
|
|
17
|
-
### **Option 1: Mixin Pattern** (Most Flexible)
|
|
18
|
-
|
|
19
|
-
Create separate mixin functions that add functionality to your class:
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
// server/mixins/DeviceMixin.ts
|
|
23
|
-
type Constructor<T = {}> = new (...args: any[]) => T;
|
|
24
|
-
|
|
25
|
-
export interface DeviceMixinBase {
|
|
26
|
-
devices: Array<Device>;
|
|
27
|
-
system: System;
|
|
28
|
-
power: Power;
|
|
29
|
-
// ... other required properties
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export function DeviceMixin<TBase extends Constructor<DeviceMixinBase>>(Base: TBase) {
|
|
33
|
-
return class extends Base {
|
|
34
|
-
setupWebSocketServers() { /* ... */ }
|
|
35
|
-
sendDeviceList(ws: WebSocket) { /* ... */ }
|
|
36
|
-
getDeviceList() { /* ... */ }
|
|
37
|
-
// ... all device-related methods
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
// Usage in smartapp.ts
|
|
42
|
-
import { DeviceMixin } from './mixins/DeviceMixin';
|
|
43
|
-
import { LinkMixin } from './mixins/LinkMixin';
|
|
44
|
-
import { SwitchMixin } from './mixins/SwitchMixin';
|
|
45
|
-
|
|
46
|
-
// Apply mixins
|
|
47
|
-
const SmartAppBase = SwitchMixin(LinkMixin(DeviceMixin(WebApp)));
|
|
48
|
-
|
|
49
|
-
export class SmartApp extends SmartAppBase {
|
|
50
|
-
// Only SmartApp-specific logic here
|
|
51
|
-
constructor(system: System, power: Power, platform: Platform) {
|
|
52
|
-
super("smartapp");
|
|
53
|
-
// ...
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
```
|
|
57
|
-
|
|
58
|
-
**Pros:**
|
|
59
|
-
- True composition - each mixin is independent
|
|
60
|
-
- Can be reused across different classes
|
|
61
|
-
- TypeScript type-safe
|
|
62
|
-
- Easy to test mixins individually
|
|
63
|
-
|
|
64
|
-
**Cons:**
|
|
65
|
-
- Requires some boilerplate
|
|
66
|
-
- Mixing order matters
|
|
67
|
-
- Can be confusing at first
|
|
68
|
-
|
|
69
|
-
---
|
|
70
|
-
|
|
71
|
-
### **Option 2: Delegate Pattern** (Simpler)
|
|
72
|
-
|
|
73
|
-
Create separate service/manager classes and delegate to them:
|
|
74
|
-
|
|
75
|
-
```typescript
|
|
76
|
-
// server/services/DeviceManager.ts
|
|
77
|
-
export class DeviceManager {
|
|
78
|
-
constructor(
|
|
79
|
-
private devices: Array<Device>,
|
|
80
|
-
private system: System,
|
|
81
|
-
private power: Power
|
|
82
|
-
) {}
|
|
83
|
-
|
|
84
|
-
setupWebSocketServers(servers: http.Server[]) { /* ... */ }
|
|
85
|
-
getDeviceList() { /* ... */ }
|
|
86
|
-
// ... all device methods
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// server/services/LinkManager.ts
|
|
90
|
-
export class LinkManager {
|
|
91
|
-
constructor(
|
|
92
|
-
private links: Array<Link>,
|
|
93
|
-
private system: System
|
|
94
|
-
) {}
|
|
95
|
-
|
|
96
|
-
initLinks() { /* ... */ }
|
|
97
|
-
updateLink(inx: number, link: Link) { /* ... */ }
|
|
98
|
-
// ... all link methods
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// In smartapp.ts
|
|
102
|
-
export class SmartApp extends WebApp {
|
|
103
|
-
private deviceManager: DeviceManager;
|
|
104
|
-
private linkManager: LinkManager;
|
|
105
|
-
|
|
106
|
-
constructor(system: System, power: Power, platform: Platform) {
|
|
107
|
-
super("smartapp");
|
|
108
|
-
|
|
109
|
-
this.deviceManager = new DeviceManager(this.devices, this.system, this.power);
|
|
110
|
-
this.linkManager = new LinkManager(this.links, this.system);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Delegate to managers
|
|
114
|
-
setupWebSocketServers() {
|
|
115
|
-
this.deviceManager.setupWebSocketServers(this.servers);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
initLinks() {
|
|
119
|
-
this.linkManager.initLinks();
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
```
|
|
123
|
-
|
|
124
|
-
**Pros:**
|
|
125
|
-
- Simple and intuitive
|
|
126
|
-
- Easy to understand
|
|
127
|
-
- Good separation of concerns
|
|
128
|
-
- Easy to unit test
|
|
129
|
-
|
|
130
|
-
**Cons:**
|
|
131
|
-
- More delegation boilerplate
|
|
132
|
-
- Not as "clean" as mixins
|
|
133
|
-
- Managers need access to SmartApp state
|
|
134
|
-
|
|
135
|
-
---
|
|
136
|
-
|
|
137
|
-
### **Option 3: Module Pattern** (Most Practical)
|
|
138
|
-
|
|
139
|
-
Split into separate modules but keep in same class:
|
|
140
|
-
|
|
141
|
-
```typescript
|
|
142
|
-
// server/smartapp/devices.ts
|
|
143
|
-
export class DeviceManagement {
|
|
144
|
-
static setupWebSocketServers(app: SmartApp) { /* ... */ }
|
|
145
|
-
static getDeviceList(app: SmartApp) { /* ... */ }
|
|
146
|
-
// ... all device methods as static functions
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
// server/smartapp/links.ts
|
|
150
|
-
export class LinkManagement {
|
|
151
|
-
static initLinks(app: SmartApp) { /* ... */ }
|
|
152
|
-
static updateLink(app: SmartApp, inx: number, link: Link) { /* ... */ }
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
// In smartapp.ts
|
|
156
|
-
import { DeviceManagement } from './smartapp/devices';
|
|
157
|
-
import { LinkManagement } from './smartapp/links';
|
|
158
|
-
|
|
159
|
-
export class SmartApp extends WebApp {
|
|
160
|
-
setupWebSocketServers() {
|
|
161
|
-
DeviceManagement.setupWebSocketServers(this);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
initLinks() {
|
|
165
|
-
LinkManagement.initLinks(this);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
```
|
|
169
|
-
|
|
170
|
-
**Pros:**
|
|
171
|
-
- Simple to implement
|
|
172
|
-
- Code is organized in separate files
|
|
173
|
-
- No complex patterns needed
|
|
174
|
-
- Easy to refactor incrementally
|
|
175
|
-
|
|
176
|
-
**Cons:**
|
|
177
|
-
- Still some methods in main class
|
|
178
|
-
- Not true composition
|
|
179
|
-
- Methods take `app` parameter
|
|
180
|
-
|
|
181
|
-
---
|
|
182
|
-
|
|
183
|
-
## **Recommended Approach**
|
|
184
|
-
|
|
185
|
-
For your situation, I'd recommend **Option 2 (Delegate Pattern)** or **Option 3 (Module Pattern)**.
|
|
186
|
-
|
|
187
|
-
Here's why:
|
|
188
|
-
1. **Easier to understand** - no complex mixin magic
|
|
189
|
-
2. **Incremental refactoring** - move one feature at a time
|
|
190
|
-
3. **Better testability** - each manager/module is isolated
|
|
191
|
-
4. **Clearer dependencies** - you see what each part needs
|
|
192
|
-
|
|
193
|
-
### Quick Win: Module Pattern
|
|
194
|
-
|
|
195
|
-
Split your 1680-line smartapp.ts into:
|
|
196
|
-
|
|
197
|
-
```
|
|
198
|
-
server/
|
|
199
|
-
smartapp.ts (main class, ~300 lines)
|
|
200
|
-
smartapp/
|
|
201
|
-
devices.ts (WebSocket & power, ~300 lines)
|
|
202
|
-
links.ts (Homebridge links, ~200 lines)
|
|
203
|
-
switches.ts (HTTP switches, ~200 lines)
|
|
204
|
-
power.ts (Power bindings, ~150 lines)
|
|
205
|
-
proxy.ts (Proxy config, ~150 lines)
|
|
206
|
-
routes.ts (HTTP routing, ~200 lines)
|
|
207
|
-
units.ts (Unit management, ~150 lines)
|
|
208
|
-
```
|
|
209
|
-
|
|
210
|
-
Would you like me to help you refactor SmartApp using one of these patterns?
|