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.
Files changed (60) hide show
  1. package/README.md +2 -0
  2. package/duotecno/types.js +1 -1
  3. package/package.json +1 -1
  4. package/server/platform.js +7 -0
  5. package/server/proxy.js +52 -24
  6. package/server/smartapp.js +1 -31
  7. package/server/views/proxy.ejs +73 -1
  8. package/.gitattributes +0 -4
  9. package/MIXINS-GUIDE.md +0 -210
  10. package/SETTINGS-UPDATE.md +0 -195
  11. package/TESTING-GUIDE.md +0 -220
  12. package/WEBSOCKET-API.md +0 -303
  13. package/accessories/accessory.ts +0 -128
  14. package/accessories/bulb.ts +0 -38
  15. package/accessories/dimmer.ts +0 -97
  16. package/accessories/door.ts +0 -88
  17. package/accessories/garagedoor.ts +0 -134
  18. package/accessories/lock.ts +0 -103
  19. package/accessories/mood.ts +0 -68
  20. package/accessories/switch.ts +0 -34
  21. package/accessories/temperature.ts +0 -55
  22. package/accessories/windowcovering.ts +0 -189
  23. package/config.homebridge.json +0 -23
  24. package/config.json +0 -49
  25. package/duotecno/Q.ts +0 -56
  26. package/duotecno/api.ts +0 -63
  27. package/duotecno/colors.ts +0 -96
  28. package/duotecno/config.ts +0 -125
  29. package/duotecno/logger.ts +0 -43
  30. package/duotecno/master.ts +0 -884
  31. package/duotecno/protocol.ts +0 -992
  32. package/duotecno/smartsocket.ts +0 -118
  33. package/duotecno/system.ts +0 -439
  34. package/duotecno/types.ts +0 -900
  35. package/index.ts +0 -9
  36. package/log.txt +0 -74
  37. package/raspberry-pi-setup.sh +0 -80
  38. package/server/HA-API.ts +0 -102
  39. package/server/HB.ts +0 -343
  40. package/server/base.ts +0 -54
  41. package/server/mDNS.ts +0 -223
  42. package/server/p1.ts +0 -226
  43. package/server/platform.ts +0 -305
  44. package/server/powerbase.ts +0 -317
  45. package/server/proxy.ts +0 -482
  46. package/server/shelly.ts +0 -212
  47. package/server/smappee.ts +0 -309
  48. package/server/smartapp.ts +0 -1710
  49. package/server/socapp.ts +0 -182
  50. package/server/somfy.ts +0 -174
  51. package/server/support.ts +0 -142
  52. package/server/webapp.ts +0 -688
  53. package/testHB.js +0 -41
  54. package/testHB.ts +0 -54
  55. package/testProxy.js +0 -7
  56. package/testProxy.ts +0 -6
  57. package/testWS.js +0 -137
  58. package/testWS.ts +0 -174
  59. package/update.sh +0 -16
  60. 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",
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",
@@ -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 PM2 or other process manager -> don't restart yourself
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
- const gCloudConnections = [];
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
- this.deviceSocket.on('error', (err) => {
258
- error(`[DEVICE] → [PROXY] Device socket error: ${err.message}`);
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. Exiting.`);
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] Exiting without restart.`);
370
- // Give OS time to release ports before exit (helps prevent EADDRINUSE on restart)
371
- setTimeout(() => process.exit(0), 500);
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
  }
@@ -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
  }
@@ -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
@@ -1,4 +0,0 @@
1
- *.css linguist-detectable=false
2
- *.map linguist-detectable=false
3
- *.js linguist-detectable=false
4
- *.ts linguist-detectable=true
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?