homebridge-smartsystem 6.8.13 → 7.0.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.
Files changed (75) hide show
  1. package/.vscode/launch.json +15 -4
  2. package/MIXINS-GUIDE.md +210 -0
  3. package/README.md +32 -0
  4. package/SETTINGS-UPDATE.md +195 -0
  5. package/TESTING-GUIDE.md +220 -0
  6. package/WEBSOCKET-API.md +303 -0
  7. package/accessories/accessory.js +20 -4
  8. package/accessories/accessory.ts +19 -4
  9. package/accessories/bulb.js +0 -5
  10. package/accessories/bulb.ts +1 -5
  11. package/accessories/dimmer.js +11 -3
  12. package/accessories/dimmer.ts +11 -4
  13. package/accessories/garagedoor.js +19 -9
  14. package/accessories/garagedoor.ts +21 -9
  15. package/accessories/lock.js +11 -3
  16. package/accessories/lock.ts +10 -3
  17. package/accessories/temperature.js +10 -2
  18. package/accessories/temperature.ts +9 -2
  19. package/accessories/windowcovering.js +30 -14
  20. package/accessories/windowcovering.ts +29 -15
  21. package/config.json +57 -19
  22. package/duotecno/types.js +48 -2
  23. package/duotecno/types.ts +75 -1
  24. package/package.json +12 -4
  25. package/raspberry-pi-setup.sh +80 -0
  26. package/server/HA-API.js +4 -1
  27. package/server/HA-API.ts +4 -1
  28. package/server/HB.js +6 -1
  29. package/server/HB.ts +5 -1
  30. package/server/mDNS.js +168 -0
  31. package/server/mDNS.ts +198 -0
  32. package/server/platform.js +7 -0
  33. package/server/platform.ts +8 -0
  34. package/server/proxy.js +58 -19
  35. package/server/proxy.ts +31 -3
  36. package/server/smartapp.js +397 -18
  37. package/server/smartapp.ts +432 -20
  38. package/server/socapp.js +2 -1
  39. package/server/socapp.ts +2 -1
  40. package/server/views/assets/Duotecno_logo_black.svg +10 -0
  41. package/server/views/assets/Duotecno_logo_white.svg +10 -0
  42. package/server/views/device-details.ejs +139 -0
  43. package/server/views/device-list.ejs +44 -0
  44. package/server/views/footer.ejs +0 -1
  45. package/server/views/head.ejs +9 -0
  46. package/server/views/homekit.ejs +2 -9
  47. package/server/views/link-details.ejs +2 -15
  48. package/server/views/link-list.ejs +2 -9
  49. package/server/views/login.ejs +8 -9
  50. package/server/views/master-detail.ejs +3 -9
  51. package/server/views/master-list.ejs +3 -10
  52. package/server/views/nav.ejs +44 -11
  53. package/server/views/node-details.ejs +1 -8
  54. package/server/views/power-binding.ejs +3 -10
  55. package/server/views/power-rule.ejs +3 -10
  56. package/server/views/power.ejs +8 -15
  57. package/server/views/proxy.ejs +1 -8
  58. package/server/views/scripts.ejs +10 -0
  59. package/server/views/service-list.ejs +2 -9
  60. package/server/views/settings.ejs +61 -22
  61. package/server/views/style.ejs +49 -5
  62. package/server/views/switch-details.ejs +2 -9
  63. package/server/views/switch-list.ejs +2 -9
  64. package/server/webapp.js +150 -11
  65. package/server/webapp.ts +166 -14
  66. package/{index.spec.js → testHB.js} +1 -1
  67. package/{run.js → testProxy.js} +2 -2
  68. package/{run.ts → testProxy.ts} +1 -1
  69. package/testWS.js +137 -0
  70. package/testWS.ts +174 -0
  71. package/run9998.js +0 -7
  72. package/run9998.ts +0 -6
  73. package/run9999.js +0 -7
  74. package/run9999.ts +0 -6
  75. /package/{index.spec.ts → testHB.ts} +0 -0
@@ -1,27 +1,36 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <link type="text/css" rel="stylesheet" href="/files/min.css" media="screen,projection"/>
5
-
6
- <!--Let browser know website is optimized for mobile-->
7
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
8
- <link rel="shortcut icon" href="/files/favicon.ico" type="image/x-icon" />
9
- <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
10
-
11
- <%- include('/views/style'); -%>
4
+ <%- include('/views/head'); -%>
12
5
  </head>
13
6
 
14
7
  <body>
15
- <%- include('/views/nav'); -%>
8
+ <%- include('/views/nav', this); -%>
16
9
 
17
10
  <form method="POST" action="/settings">
18
11
  <input type="hidden" name="daction" value="save">
19
- <h1>
20
- Settings<button class="right btn waves-effect waves-light" type="submit" name="action" value="reset">Reset to DHCP</button>
21
- <button class="right btn waves-effect waves-light" type="submit" name="action" value="install">Install IP Settings</button>
22
- <button class="right btn waves-effect waves-light" type="submit" name="action" value="reboot">Reboot PI</button>
23
- </h1>
12
+ <h1>Settings</h1>
13
+
14
+ <% if (typeof message !== 'undefined' && message) { %>
15
+ <div class="row">
16
+ <div class="col s12">
17
+ <div class="card-panel <%= message.includes('successfully') ? 'green lighten-4' : 'orange lighten-4' %>">
18
+ <span class="<%= message.includes('successfully') ? 'green-text text-darken-4' : 'orange-text text-darken-4' %>">
19
+ <i class="material-icons left"><%= message.includes('successfully') ? 'check_circle' : 'info' %></i>
20
+ <%= message %>
21
+ </span>
22
+ </div>
23
+ </div>
24
+ </div>
25
+ <% } %>
24
26
 
27
+ <!--
28
+ TODO: Network IP configuration is temporarily disabled pending implementation fixes.
29
+ Focus is on testing the Devices WebSocket functionality first.
30
+ These fields will be re-enabled once DHCP configuration is properly implemented.
31
+ -->
32
+
33
+ <!-- NETWORK SETTINGS - TEMPORARILY DISABLED
25
34
  <div class="row">
26
35
  <div class="input-field col s6">
27
36
  <input placeholder="" id="ip1" name="ip1" type="text" value="<%= config.network.ip1 %>">
@@ -39,17 +48,47 @@
39
48
  <label for="nameservers">Nameservers</label>
40
49
  </div>
41
50
  </div>
51
+ END NETWORK SETTINGS -->
42
52
 
43
- <!-- div class="row">
44
- <div class="input-field col s6">
45
- <input placeholder="" id="ip2" name="ip2" type="text" value="<%= config.network.ip2 %>">
46
- <label for="ip2">Secondary Fixed IP Address / netmask</label>
53
+ <h5>mDNS Configuration</h5>
54
+ <div class="row">
55
+ <div class="input-field col s12">
56
+ <input placeholder="duotecno-gateway" id="mdnsName" name="mdnsName" type="text" value="<%= config.mdnsName || 'duotecno-gateway' %>">
57
+ <label for="mdnsName">mDNS Service Name (e.g., duotecno-gateway → duotecno-gateway.local)</label>
47
58
  </div>
48
- <div class="input-field col s6">
49
- <input placeholder="" id="gateway2" name="gateway2" type="text" value="<%= config.network.gateway2 %>">
50
- <label for="gateway2">Secondary Gateway</label>
59
+ </div>
60
+
61
+ <div class="row">
62
+ <div class="col s12">
63
+ <button class="btn waves-effect waves-light" type="submit" name="action" value="save">
64
+ <i class="material-icons left">save</i>
65
+ Save Settings
66
+ </button>
67
+ <button class="btn waves-effect waves-light orange" type="submit" name="action" value="restart">
68
+ <i class="material-icons left">refresh</i>
69
+ Restart Server
70
+ </button>
51
71
  </div>
52
- </div -->
72
+ </div>
73
+
74
+ <!-- SYSTEM ACTIONS - TEMPORARILY DISABLED
75
+ <div class="row">
76
+ <div class="col s12">
77
+ <button class="btn waves-effect waves-light red" type="submit" name="action" value="reboot">
78
+ <i class="material-icons left">power_settings_new</i>
79
+ Reboot System
80
+ </button>
81
+ <button class="btn waves-effect waves-light grey" type="submit" name="action" value="reset">
82
+ <i class="material-icons left">settings_backup_restore</i>
83
+ Reset to DHCP
84
+ </button>
85
+ <button class="btn waves-effect waves-light blue" type="submit" name="action" value="install">
86
+ <i class="material-icons left">system_update</i>
87
+ Install IP Settings
88
+ </button>
89
+ </div>
90
+ </div>
91
+ END SYSTEM ACTIONS -->
53
92
 
54
93
  </form>
55
94
 
@@ -1,11 +1,25 @@
1
1
  <style>
2
- form { padding-left: 7px; padding-right: 9px }
3
- a.brand-logo { left: 7px; -webkit-transform: none; transform: none }
2
+ /* Page layout with sticky footer */
3
+ html, body { height: 100%; margin: 0; display: flex; flex-direction: column }
4
+ body { min-height: 100vh }
5
+
6
+ /* Main content area - grows to fill space */
7
+ body > *:not(nav):not(.page-footer) { flex: 1 0 auto }
8
+
9
+ form { padding-left: 7px; padding-right: 9px; padding-bottom: 80px }
10
+
11
+ nav, nav .nav-wrapper { height: 64px !important; line-height: 64px !important; flex-shrink: 0 }
12
+ nav .brand-logo { left: 10px !important; -webkit-transform: none !important; transform: none !important }
13
+
4
14
  h1 { font-size: 30px }
5
15
  h1.btn { margin-top: 18px }
6
16
  h1 .btn { margin-left: 10px }
7
- a span { margin-bottom: 7px; display: inline-block; }
8
- .page-footer p { text-align: right; padding-bottom: 4px }
17
+ a span { margin-bottom: 7px; display: inline-block }
18
+
19
+ /* Sticky footer */
20
+ .page-footer { position: fixed; bottom: 0; left: 0; right: 0; width: 100%; flex-shrink: 0; z-index: 100; }
21
+ .page-footer p { text-align: right; padding-bottom: 4px; margin-right: 6px }
22
+ .page-footer a { float: left; margin-left: 10px; color: white; }
9
23
  .page-footer { padding-top: 5px; padding-right: 6px }
10
24
  .range-field { margin-bottom: -1em }
11
25
  b { color: #ee6e73 }
@@ -14,7 +28,37 @@
14
28
  .btn-floating.btn-small { width: 32px; height: 31px }
15
29
  .rules p { display: inline-block; margin-bottom: 0px }
16
30
  div.select-wrapper { max-width: 212px }
17
- div.select-wrapper input.select-dropdown { font-size: 12px; height: 2.3rem }
31
+ div.select-wrapper input.select-dropdown { font-size: 16px !important; height: 3rem !important; }
18
32
  span.used { color: darkgrey }
19
33
  .message { margin-left: 10px; font-size: 15px }
34
+
35
+
36
+ /* Hamburger menu icon - positioned on right side */
37
+ nav .sidenav-trigger {
38
+ position: absolute; right: 10px; top: 0;
39
+ height: 64px; line-height: 64px; padding: 0 15px; margin: 0;
40
+ }
41
+ nav .sidenav-trigger i { line-height: 64px; font-size: 2rem }
42
+
43
+ /* Hide hamburger on desktop, show desktop menu */
44
+ @media only screen and (min-width: 993px) {
45
+ nav .sidenav-trigger { display: none !important; }
46
+ }
47
+
48
+ /* Show hamburger on mobile, hide desktop menu */
49
+ @media only screen and (max-width: 992px) {
50
+ nav .sidenav-trigger { display: block !important; }
51
+ }
52
+
53
+ /* Desktop navigation active state */
54
+ nav ul li.active { background-color: rgba(0, 0, 0, 0.1) }
55
+
56
+ /* Sidenav styling */
57
+ .sidenav { width: 280px; }
58
+ .sidenav .user-view { padding: 1px 32px 16px 16px }
59
+ .sidenav .user-view .name { font-size: 18px; font-weight: 500; margin-top: 16px; display: block; }
60
+ .sidenav li > a { height: auto; line-height: 1px; padding: 14px 32px; }
61
+ .sidenav .divider { margin: 8px 0; }
62
+ .sidenav li.active { background-color: rgba(0,0,0,0.05); }
63
+
20
64
  </style>
@@ -1,14 +1,7 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <link type="text/css" rel="stylesheet" href="/files/min.css" media="screen,projection"/>
5
-
6
- <!--Let browser know website is optimized for mobile-->
7
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
8
- <link rel="shortcut icon" href="/files/favicon.ico" type="image/x-icon" />
9
- <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
10
-
11
- <%- include('/views/style'); -%>
4
+ <%- include('/views/head'); -%>
12
5
  </head>
13
6
 
14
7
  <body>
@@ -18,7 +11,7 @@
18
11
  <input type="hidden" name="daction" value="save">
19
12
  <input type="hidden" name="id" value="<%= id %>">
20
13
  <h1>
21
- Switch #<%= id %>
14
+ Switch #<%= id+1 %>
22
15
  <button class="right btn waves-effect waves-light" type="submit" name="action" value="cancel">Back</button>
23
16
  <button class="right btn waves-effect waves-light" type="submit" name="action" value="delete">Delete</button>
24
17
  <button class="right btn waves-effect waves-light" type="submit" name="action" value="change">Save</button>
@@ -1,18 +1,11 @@
1
1
  <!DOCTYPE html>
2
2
  <html>
3
3
  <head>
4
- <link type="text/css" rel="stylesheet" href="/files/min.css" media="screen,projection"/>
5
-
6
- <!--Let browser know website is optimized for mobile-->
7
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
8
- <link rel="shortcut icon" href="/files/favicon.ico" type="image/x-icon" />
9
- <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
10
-
11
- <%- include('/views/style'); -%>
4
+ <%- include('/views/head'); -%>
12
5
  </head>
13
6
 
14
7
  <body>
15
- <%- include('/views/nav'); -%>
8
+ <%- include('/views/nav', this); -%>
16
9
 
17
10
  <form method="POST" action="/switches">
18
11
  <h1>
package/server/webapp.js CHANGED
@@ -23,6 +23,7 @@ const types = require("../duotecno/types");
23
23
  const logger = require("../duotecno/logger");
24
24
  const logger_1 = require("../duotecno/logger");
25
25
  const child_process_1 = require("child_process");
26
+ const mDNS_1 = require("./mDNS");
26
27
  ;
27
28
  const kLogoutHeader = { "Set-Cookie": "ticket=x" };
28
29
  //////////////////////
@@ -48,6 +49,7 @@ class Context {
48
49
  this.date = types.date;
49
50
  this.datetime = types.datetime;
50
51
  this.makeInt = types.makeInt;
52
+ this.cap = types.cap;
51
53
  const bodyParams = qs.parse(body);
52
54
  const urlParts = url.parse(req.url, true);
53
55
  this.params = Object.assign(Object.assign({}, bodyParams), urlParts.query);
@@ -229,10 +231,26 @@ class WebApp extends base_1.Base {
229
231
  this.token = ""; // "$#@!" + new Date().getTime() + "!@#$"; // should never match
230
232
  this.user = "";
231
233
  this.password = "";
234
+ this.mdnsService = null; // mDNS service for hostname and service discovery
232
235
  this.port = 80;
233
236
  (0, logger_1.log)("server", "Creating http server");
234
237
  this.files = {};
235
- this.server = http.createServer((req, res) => {
238
+ this.servers = [];
239
+ // Note: Server will be fully configured in serve() method
240
+ // We don't create it here anymore since we might need multiple servers
241
+ }
242
+ // Override this in subclasses to control template caching behavior
243
+ // Return true to always reload templates from disk (development mode)
244
+ // Return false to use preloaded cached templates (production mode)
245
+ isDebugMode() {
246
+ return false; // Default: use cached templates for performance
247
+ }
248
+ serve(onConnected) {
249
+ // Handle both single port and multiple ports
250
+ const ports = Array.isArray(this.port) ? this.port : [this.port];
251
+ (0, logger_1.log)("server", "WebApp - Start serving http on port(s): " + ports.join(", "));
252
+ // Create the request handler function (shared across all servers)
253
+ const requestHandler = (req, res) => {
236
254
  this.doServe(req, res).then(result => {
237
255
  res.writeHead(result.status, Object.assign({ 'Content-Type': result.type || 'text/html' }, result.header));
238
256
  res.write(result.data);
@@ -242,14 +260,135 @@ class WebApp extends base_1.Base {
242
260
  res.write(reason.toString());
243
261
  res.end();
244
262
  });
263
+ };
264
+ // Track how many servers are listening for the callback
265
+ let listeningCount = 0;
266
+ const totalPorts = ports.length;
267
+ const onListening = (port) => {
268
+ (0, logger_1.log)("server", `WebApp - Listening on port ${port}`);
269
+ listeningCount++;
270
+ // Call onConnected only after ALL servers are listening
271
+ if (listeningCount === totalPorts && onConnected) {
272
+ onConnected();
273
+ }
274
+ };
275
+ // Create a server for each port (each http.Server can only listen on ONE port)
276
+ ports.forEach((port) => {
277
+ const server = http.createServer(requestHandler);
278
+ // Set socket options to allow port reuse (prevents EADDRINUSE on restart)
279
+ server.on('listening', () => {
280
+ const address = server.address();
281
+ if (address && typeof address === 'object') {
282
+ onListening(address.port);
283
+ }
284
+ });
285
+ this.servers.push(server);
286
+ // Listen with options to reuse address
287
+ server.listen({
288
+ port: port,
289
+ host: '::',
290
+ exclusive: false // Allow multiple processes to bind (for cluster mode)
291
+ });
245
292
  });
246
293
  }
247
- serve(onConnected) {
248
- (0, logger_1.log)("server", "WebApp - Start serving http on port " + this.port);
249
- this.server.listen(this.port, onConnected);
294
+ /**
295
+ * Close all HTTP servers and cleanup
296
+ * Call this before restarting or shutting down
297
+ */
298
+ closeServers() {
299
+ return __awaiter(this, void 0, void 0, function* () {
300
+ return new Promise((resolve) => {
301
+ if (this.servers.length === 0) {
302
+ resolve();
303
+ return;
304
+ }
305
+ let closedCount = 0;
306
+ const totalServers = this.servers.length;
307
+ const onClosed = () => {
308
+ closedCount++;
309
+ if (closedCount === totalServers) {
310
+ (0, logger_1.log)("server", "All HTTP servers closed");
311
+ this.servers = [];
312
+ resolve();
313
+ }
314
+ };
315
+ // Close each server
316
+ this.servers.forEach((server, index) => {
317
+ if (server.listening) {
318
+ server.close((error) => {
319
+ if (error) {
320
+ logger.err("server", `Error closing server ${index}: ${error.message}`);
321
+ }
322
+ else {
323
+ (0, logger_1.log)("server", `Server ${index} closed`);
324
+ }
325
+ onClosed();
326
+ });
327
+ }
328
+ else {
329
+ onClosed();
330
+ }
331
+ });
332
+ });
333
+ });
334
+ }
335
+ /**
336
+ * Initialize mDNS service with the given name and version
337
+ * Call this before serve() or during initialization
338
+ */
339
+ initMDNS(name, version) {
340
+ try {
341
+ const ports = Array.isArray(this.port) ? this.port : [this.port];
342
+ this.mdnsService = new mDNS_1.MDNSService({
343
+ name,
344
+ port: ports,
345
+ version: version || '1.0.0'
346
+ });
347
+ this.mdnsService.init();
348
+ (0, logger_1.log)("webapp", "mDNS service initialized");
349
+ }
350
+ catch (e) {
351
+ (0, logger_1.err)("webapp", `Failed to initialize mDNS: ${e.message}`);
352
+ this.mdnsService = null;
353
+ }
354
+ }
355
+ /**
356
+ * Register mDNS service and hostname
357
+ * Call this after server is listening (e.g., in onConnected callback)
358
+ */
359
+ registerMDNS() {
360
+ if (!this.mdnsService) {
361
+ (0, logger_1.err)("webapp", "mDNS service not initialized, cannot register");
362
+ return;
363
+ }
364
+ try {
365
+ this.mdnsService.register();
366
+ }
367
+ catch (e) {
368
+ (0, logger_1.err)("webapp", `Failed to register mDNS service: ${e.message}`);
369
+ }
370
+ }
371
+ /**
372
+ * Stop and cleanup mDNS service
373
+ */
374
+ destroyMDNS() {
375
+ if (this.mdnsService) {
376
+ try {
377
+ this.mdnsService.destroy();
378
+ this.mdnsService = null;
379
+ }
380
+ catch (e) {
381
+ (0, logger_1.err)("webapp", `Failed to destroy mDNS service: ${e.message}`);
382
+ }
383
+ }
250
384
  }
251
385
  addFile(name, filename, type, enc) {
252
- this.addContent(name, fs.readFileSync(filename, { encoding: enc || 'utf-8' }), type || "text/html", filename);
386
+ // Ensure HTML files have charset specified
387
+ let contentType = type || "text/html";
388
+ if (contentType === "text/html" || contentType.startsWith("text/html")) {
389
+ contentType = "text/html; charset=utf-8";
390
+ }
391
+ this.addContent(name, fs.readFileSync(filename, { encoding: enc || 'utf-8' }), contentType, filename);
253
392
  }
254
393
  addContent(name, content, type, filename) {
255
394
  this.files[name] = { content, type, filename };
@@ -269,17 +408,17 @@ class WebApp extends base_1.Base {
269
408
  return { status: 200, data: file.content, type: file.type };
270
409
  }
271
410
  html(html) {
272
- return { status: 200, data: html, type: "text/html" };
411
+ return { status: 200, data: html, type: "text/html; charset=utf-8" };
273
412
  }
274
413
  ejs(name, context, objects, header = {}) {
275
414
  // types.debug("webapp", context); !! circular ref in socket/http
276
- // try to get cached file
277
- let file = this.getFile(name);
278
- // for non cached files
415
+ // In debug mode, always reload from disk to see changes without restart
416
+ let file = this.isDebugMode() ? null : this.getFile(name);
417
+ // for non cached files or debug mode
279
418
  if (!file) {
280
419
  try {
281
420
  const filename = __dirname + "/views/" + name;
282
- file = { content: fs.readFileSync(filename, { encoding: 'utf-8' }), type: "text/html", filename };
421
+ file = { content: fs.readFileSync(filename, { encoding: 'utf-8' }), type: "text/html; charset=utf-8", filename };
283
422
  }
284
423
  catch (e) {
285
424
  return this.error(context, e.toString());
@@ -291,7 +430,7 @@ class WebApp extends base_1.Base {
291
430
  context[key] = objects[key];
292
431
  try {
293
432
  const html = ejs.render(file.content, context, { root: __dirname, filename: file.filename });
294
- return { status: 200, type: file.type, data: html, header };
433
+ return { status: 200, type: file.type || "text/html; charset=utf-8", data: html, header };
295
434
  }
296
435
  catch (e) {
297
436
  return this.error(context, e.toString());