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
package/server/webapp.ts CHANGED
@@ -10,8 +10,9 @@ import * as qs from "querystring";
10
10
  import { Base } from './base';
11
11
  import * as types from "../duotecno/types";
12
12
  import * as logger from "../duotecno/logger";
13
- import { log } from "../duotecno/logger";
13
+ import { log, err } from "../duotecno/logger";
14
14
  import { exec } from "child_process";
15
+ import { MDNSService } from './mDNS';
15
16
 
16
17
  export interface Params {[key: string]: string | Array<string>};
17
18
 
@@ -87,6 +88,7 @@ export class Context {
87
88
  date = types.date;
88
89
  datetime = types.datetime;
89
90
  makeInt = types.makeInt;
91
+ cap = types.cap;
90
92
 
91
93
 
92
94
  constructor(body: string, req: http.IncomingMessage, res: http.ServerResponse) {
@@ -272,11 +274,12 @@ try {
272
274
 
273
275
  export class WebApp extends Base {
274
276
  files: {[filename: string]: WebAppFile}; // hashmap for serving files, including ejs files for rendering
275
- server: http.Server;
276
- port: number;
277
+ servers: http.Server[]; // array to hold all HTTP servers when listening on one or multiple ports
278
+ port: number | number[];
277
279
  token = ""; // "$#@!" + new Date().getTime() + "!@#$"; // should never match
278
280
  user = "";
279
281
  password = "";
282
+ mdnsService: any | null = null; // mDNS service for hostname and service discovery
280
283
 
281
284
  constructor(type: string) {
282
285
  super(type);
@@ -284,8 +287,27 @@ export class WebApp extends Base {
284
287
 
285
288
  log("server", "Creating http server");
286
289
  this.files = {};
290
+ this.servers = [];
287
291
 
288
- this.server = http.createServer( (req: http.IncomingMessage, res: http.ServerResponse) => {
292
+ // Note: Server will be fully configured in serve() method
293
+ // We don't create it here anymore since we might need multiple servers
294
+ }
295
+
296
+ // Override this in subclasses to control template caching behavior
297
+ // Return true to always reload templates from disk (development mode)
298
+ // Return false to use preloaded cached templates (production mode)
299
+ protected isDebugMode(): boolean {
300
+ return false; // Default: use cached templates for performance
301
+ }
302
+
303
+ serve(onConnected?: () => void) {
304
+ // Handle both single port and multiple ports
305
+ const ports = Array.isArray(this.port) ? this.port : [this.port];
306
+
307
+ log("server", "WebApp - Start serving http on port(s): " + ports.join(", "));
308
+
309
+ // Create the request handler function (shared across all servers)
310
+ const requestHandler = (req: http.IncomingMessage, res: http.ServerResponse) => {
289
311
  this.doServe(req, res).then(
290
312
  result => {
291
313
  res.writeHead(result.status, {'Content-Type': result.type || 'text/html', ...result.header});
@@ -299,16 +321,146 @@ export class WebApp extends Base {
299
321
  res.end();
300
322
  }
301
323
  );
324
+ };
325
+
326
+ // Track how many servers are listening for the callback
327
+ let listeningCount = 0;
328
+ const totalPorts = ports.length;
329
+
330
+ const onListening = (port: number) => {
331
+ log("server", `WebApp - Listening on port ${port}`);
332
+ listeningCount++;
333
+
334
+ // Call onConnected only after ALL servers are listening
335
+ if (listeningCount === totalPorts && onConnected) {
336
+ onConnected();
337
+ }
338
+ };
339
+
340
+ // Create a server for each port (each http.Server can only listen on ONE port)
341
+ ports.forEach((port) => {
342
+ const server = http.createServer(requestHandler);
343
+
344
+ // Set socket options to allow port reuse (prevents EADDRINUSE on restart)
345
+ server.on('listening', () => {
346
+ const address = server.address();
347
+ if (address && typeof address === 'object') {
348
+ onListening(address.port);
349
+ }
350
+ });
351
+
352
+ this.servers.push(server);
353
+
354
+ // Listen with options to reuse address
355
+ server.listen({
356
+ port: port,
357
+ host: '::', // Listen on all IPv6 addresses (includes IPv4)
358
+ exclusive: false // Allow multiple processes to bind (for cluster mode)
359
+ });
302
360
  });
303
361
  }
304
362
 
305
- serve(onConnected?: () => void) {
306
- log("server", "WebApp - Start serving http on port " + this.port);
307
- this.server.listen(this.port, onConnected);
363
+ /**
364
+ * Close all HTTP servers and cleanup
365
+ * Call this before restarting or shutting down
366
+ */
367
+ async closeServers(): Promise<void> {
368
+ return new Promise((resolve) => {
369
+ if (this.servers.length === 0) {
370
+ resolve();
371
+ return;
372
+ }
373
+
374
+ let closedCount = 0;
375
+ const totalServers = this.servers.length;
376
+
377
+ const onClosed = () => {
378
+ closedCount++;
379
+ if (closedCount === totalServers) {
380
+ log("server", "All HTTP servers closed");
381
+ this.servers = [];
382
+ resolve();
383
+ }
384
+ };
385
+
386
+ // Close each server
387
+ this.servers.forEach((server, index) => {
388
+ if (server.listening) {
389
+ server.close((error) => {
390
+ if (error) {
391
+ logger.err("server", `Error closing server ${index}: ${error.message}`);
392
+ } else {
393
+ log("server", `Server ${index} closed`);
394
+ }
395
+ onClosed();
396
+ });
397
+ } else {
398
+ onClosed();
399
+ }
400
+ });
401
+ });
402
+ }
403
+
404
+ /**
405
+ * Initialize mDNS service with the given name and version
406
+ * Call this before serve() or during initialization
407
+ */
408
+ initMDNS(name: string, version?: string) {
409
+ try {
410
+ const ports = Array.isArray(this.port) ? this.port : [this.port];
411
+
412
+ this.mdnsService = new MDNSService({
413
+ name,
414
+ port: ports,
415
+ version: version || '1.0.0'
416
+ });
417
+
418
+ this.mdnsService.init();
419
+ log("webapp", "mDNS service initialized");
420
+ } catch (e) {
421
+ err("webapp", `Failed to initialize mDNS: ${e.message}`);
422
+ this.mdnsService = null;
423
+ }
424
+ }
425
+
426
+ /**
427
+ * Register mDNS service and hostname
428
+ * Call this after server is listening (e.g., in onConnected callback)
429
+ */
430
+ registerMDNS() {
431
+ if (!this.mdnsService) {
432
+ err("webapp", "mDNS service not initialized, cannot register");
433
+ return;
434
+ }
435
+
436
+ try {
437
+ this.mdnsService.register();
438
+ } catch (e) {
439
+ err("webapp", `Failed to register mDNS service: ${e.message}`);
440
+ }
441
+ }
442
+
443
+ /**
444
+ * Stop and cleanup mDNS service
445
+ */
446
+ destroyMDNS() {
447
+ if (this.mdnsService) {
448
+ try {
449
+ this.mdnsService.destroy();
450
+ this.mdnsService = null;
451
+ } catch (e) {
452
+ err("webapp", `Failed to destroy mDNS service: ${e.message}`);
453
+ }
454
+ }
308
455
  }
309
456
 
310
457
  addFile(name: string, filename: string, type?: string, enc?: BufferEncoding) {
311
- this.addContent(name, fs.readFileSync(filename, {encoding: enc || 'utf-8'}), type || "text/html", filename);
458
+ // Ensure HTML files have charset specified
459
+ let contentType = type || "text/html";
460
+ if (contentType === "text/html" || contentType.startsWith("text/html")) {
461
+ contentType = "text/html; charset=utf-8";
462
+ }
463
+ this.addContent(name, fs.readFileSync(filename, {encoding: enc || 'utf-8'}), contentType, filename);
312
464
  }
313
465
  addContent(name: string, content: string, type: string, filename: string) {
314
466
  this.files[name] = { content, type, filename };
@@ -329,19 +481,19 @@ export class WebApp extends Base {
329
481
  return { status: 200, data: file.content, type: file.type };
330
482
  }
331
483
  html(html: string): HttpResponse {
332
- return { status: 200, data: html, type: "text/html" }
484
+ return { status: 200, data: html, type: "text/html; charset=utf-8" }
333
485
  }
334
486
  ejs(name: string, context: Context, objects: object, header = {}): HttpResponse {
335
487
  // types.debug("webapp", context); !! circular ref in socket/http
336
488
 
337
- // try to get cached file
338
- let file = this.getFile(name);
489
+ // In debug mode, always reload from disk to see changes without restart
490
+ let file = this.isDebugMode() ? null : this.getFile(name);
339
491
 
340
- // for non cached files
492
+ // for non cached files or debug mode
341
493
  if (!file) {
342
494
  try {
343
495
  const filename = __dirname + "/views/" + name;
344
- file = { content: fs.readFileSync(filename, {encoding: 'utf-8'}), type: "text/html", filename};
496
+ file = { content: fs.readFileSync(filename, {encoding: 'utf-8'}), type: "text/html; charset=utf-8", filename};
345
497
  } catch(e) {
346
498
  return this.error(context, e.toString());
347
499
  }
@@ -352,7 +504,7 @@ export class WebApp extends Base {
352
504
  for (const key in objects) context[key] = objects[key];
353
505
  try {
354
506
  const html = ejs.render(file.content, context, { root: __dirname, filename: file.filename });
355
- return { status: 200, type: file.type, data: html, header };
507
+ return { status: 200, type: file.type || "text/html; charset=utf-8", data: html, header };
356
508
 
357
509
  } catch(e) {
358
510
  return this.error(context, e.toString());
@@ -38,4 +38,4 @@ catch (err) {
38
38
  // u = new Unit(n, Sanitizers.unitInfo({name: "unit|20", type: 1}));
39
39
  // log("spec", "Testing names -> getName = " + u.getName() + ", getDisplayname = " + u.getDisplayName() + ", type " + u.type + ", extType = " + u.extendedType + ", getTypeName = " + u.typeName());
40
40
  // }
41
- //# sourceMappingURL=index.spec.js.map
41
+ //# sourceMappingURL=testHB.js.map
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const socapp_1 = require("./server/socapp");
4
- console.log("run.ts - running in directory: " + process.cwd());
4
+ console.log("testProxy.ts - running in directory: " + process.cwd());
5
5
  const socServer = new socapp_1.SocApp(null, "server");
6
6
  socServer.serve();
7
- //# sourceMappingURL=run.js.map
7
+ //# sourceMappingURL=testProxy.js.map
@@ -1,6 +1,6 @@
1
1
  import { SocApp } from "./server/socapp";
2
2
 
3
- console.log("run.ts - running in directory: " + process.cwd());
3
+ console.log("testProxy.ts - running in directory: " + process.cwd());
4
4
 
5
5
  const socServer = new SocApp(null, "server");
6
6
  socServer.serve();
package/testWS.js ADDED
@@ -0,0 +1,137 @@
1
+ "use strict";
2
+ // WebSocket Test Client for Device Control
3
+ // Tests the SmartApp WebSocket device control functionality
4
+ //
5
+ // Usage: ts-node testWS.ts [ws://host:port/devices]
6
+ //
7
+ // Johan Coppieters, Nov 2025
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ const WebSocket = require("ws");
10
+ class WebSocketDeviceTest {
11
+ constructor(url) {
12
+ this.devices = [];
13
+ this.pollInterval = null;
14
+ this.randomInterval = null;
15
+ this.url = url;
16
+ }
17
+ connect() {
18
+ console.log(`\n🔌 Connecting to WebSocket server: ${this.url}`);
19
+ this.ws = new WebSocket(this.url);
20
+ this.ws.on('open', () => {
21
+ console.log('✅ Connected to WebSocket server\n');
22
+ this.startPolling();
23
+ this.startRandomControl();
24
+ });
25
+ this.ws.on('message', (data) => {
26
+ try {
27
+ console.log('\n📥 Received message from server:');
28
+ console.log(data.toString());
29
+ const devices = JSON.parse(data.toString());
30
+ this.devices = devices;
31
+ console.log(`\n📊 Received ${devices.length} device(s):`);
32
+ console.log('═'.repeat(100));
33
+ console.log('ID | Name | Power(W) | Current(A) | Voltage(V) | Relay | Measured');
34
+ console.log('─'.repeat(100));
35
+ devices.forEach(device => {
36
+ const relayState = device.relay ? '🟢 ON ' : '🔴 OFF';
37
+ const measuredFlag = device.measured ? 'YES' : 'MANUAL';
38
+ console.log(`${String(device.id).padEnd(4)}| ` +
39
+ `${device.name.padEnd(20)} | ` +
40
+ `${device.power.toFixed(1).padStart(8)} | ` +
41
+ `${device.current.toFixed(2).padStart(10)} | ` +
42
+ `${device.voltage.toFixed(1).padStart(10)} | ` +
43
+ `${relayState} | ${measuredFlag}`);
44
+ });
45
+ console.log('═'.repeat(100));
46
+ }
47
+ catch (e) {
48
+ console.error('❌ Error parsing message:', e.message);
49
+ }
50
+ });
51
+ this.ws.on('close', () => {
52
+ console.log('\n🔌 Connection closed');
53
+ this.stopIntervals();
54
+ });
55
+ this.ws.on('error', (error) => {
56
+ console.error('❌ WebSocket error:', error.message);
57
+ this.stopIntervals();
58
+ });
59
+ }
60
+ startPolling() {
61
+ // Send keep-alive every 5 seconds
62
+ this.pollInterval = setInterval(() => {
63
+ if (this.ws.readyState === WebSocket.OPEN) {
64
+ console.log('\n⏱️ Sending keep-alive poll...');
65
+ this.ws.send('[]');
66
+ }
67
+ }, 5000);
68
+ }
69
+ startRandomControl() {
70
+ // Send random relay control every 10-15 seconds
71
+ this.randomInterval = setInterval(() => {
72
+ if (this.ws.readyState === WebSocket.OPEN && this.devices.length > 0) {
73
+ this.sendRandomControl();
74
+ }
75
+ }, 10000 + Math.random() * 5000);
76
+ }
77
+ sendRandomControl() {
78
+ if (this.devices.length === 0) {
79
+ console.log('⚠️ No devices available for control');
80
+ return;
81
+ }
82
+ // Pick 1-3 random devices
83
+ const numDevices = Math.min(this.devices.length, Math.floor(Math.random() * 3) + 1);
84
+ const shuffled = [...this.devices].sort(() => Math.random() - 0.5);
85
+ const selectedDevices = shuffled.slice(0, numDevices);
86
+ const commands = selectedDevices.map(device => ({
87
+ id: device.id,
88
+ relay: Math.random() > 0.5 // Random ON/OFF
89
+ }));
90
+ console.log('\n🎲 Sending random control commands:');
91
+ commands.forEach(cmd => {
92
+ const device = this.devices.find(d => d.id === cmd.id);
93
+ const action = cmd.relay ? '🟢 ON' : '🔴 OFF';
94
+ const node = Math.floor(cmd.id / 256);
95
+ const unit = cmd.id % 256;
96
+ console.log(` → ${device === null || device === void 0 ? void 0 : device.name} (node=${node}, unit=${unit}, id=${cmd.id}): ${action}`);
97
+ });
98
+ console.log('📤 Sending commands to server...');
99
+ console.log(JSON.stringify(commands, null, 2));
100
+ this.ws.send(JSON.stringify(commands));
101
+ }
102
+ stopIntervals() {
103
+ if (this.pollInterval) {
104
+ clearInterval(this.pollInterval);
105
+ this.pollInterval = null;
106
+ }
107
+ if (this.randomInterval) {
108
+ clearInterval(this.randomInterval);
109
+ this.randomInterval = null;
110
+ }
111
+ }
112
+ disconnect() {
113
+ console.log('\n🔌 Disconnecting...');
114
+ this.stopIntervals();
115
+ if (this.ws) {
116
+ this.ws.close();
117
+ }
118
+ }
119
+ }
120
+ // Main
121
+ const args = process.argv.slice(2);
122
+ const url = args[0] || 'ws://localhost:5002/devices';
123
+ console.log('╔════════════════════════════════════════════════════════════════════╗');
124
+ console.log('║ WebSocket Device Control Test Client ║');
125
+ console.log('║ Press Ctrl+C to exit ║');
126
+ console.log('╚════════════════════════════════════════════════════════════════════╝');
127
+ const client = new WebSocketDeviceTest(url);
128
+ client.connect();
129
+ // Handle graceful shutdown
130
+ process.on('SIGINT', () => {
131
+ console.log('\n\n👋 Shutting down gracefully...');
132
+ client.disconnect();
133
+ setTimeout(() => process.exit(0), 500);
134
+ });
135
+ // Keep the process running
136
+ process.stdin.resume();
137
+ //# sourceMappingURL=testWS.js.map
package/testWS.ts ADDED
@@ -0,0 +1,174 @@
1
+ // WebSocket Test Client for Device Control
2
+ // Tests the SmartApp WebSocket device control functionality
3
+ //
4
+ // Usage: ts-node testWS.ts [ws://host:port/devices]
5
+ //
6
+ // Johan Coppieters, Nov 2025
7
+
8
+ import * as WebSocket from 'ws';
9
+
10
+ interface Device {
11
+ id: number;
12
+ name: string;
13
+ power: number;
14
+ energy: number;
15
+ voltage: number;
16
+ current: number;
17
+ relay: boolean;
18
+ measured: boolean;
19
+ }
20
+
21
+ class WebSocketDeviceTest {
22
+ private ws: WebSocket;
23
+ private devices: Device[] = [];
24
+ private url: string;
25
+ private pollInterval: NodeJS.Timeout | null = null;
26
+ private randomInterval: NodeJS.Timeout | null = null;
27
+
28
+ constructor(url: string) {
29
+ this.url = url;
30
+ }
31
+
32
+ connect() {
33
+ console.log(`\n🔌 Connecting to WebSocket server: ${this.url}`);
34
+
35
+ this.ws = new WebSocket(this.url);
36
+
37
+ this.ws.on('open', () => {
38
+ console.log('✅ Connected to WebSocket server\n');
39
+ this.startPolling();
40
+ this.startRandomControl();
41
+ });
42
+
43
+ this.ws.on('message', (data: string) => {
44
+ try {
45
+ console.log('\n📥 Received message from server:');
46
+ console.log(data.toString());
47
+ const devices: Device[] = JSON.parse(data.toString());
48
+ this.devices = devices;
49
+
50
+ console.log(`\n📊 Received ${devices.length} device(s):`);
51
+ console.log('═'.repeat(100));
52
+ console.log('ID | Name | Power(W) | Current(A) | Voltage(V) | Relay | Measured');
53
+ console.log('─'.repeat(100));
54
+
55
+ devices.forEach(device => {
56
+ const relayState = device.relay ? '🟢 ON ' : '🔴 OFF';
57
+ const measuredFlag = device.measured ? 'YES' : 'MANUAL';
58
+ console.log(
59
+ `${String(device.id).padEnd(4)}| ` +
60
+ `${device.name.padEnd(20)} | ` +
61
+ `${device.power.toFixed(1).padStart(8)} | ` +
62
+ `${device.current.toFixed(2).padStart(10)} | ` +
63
+ `${device.voltage.toFixed(1).padStart(10)} | ` +
64
+ `${relayState} | ${measuredFlag}`
65
+ );
66
+ });
67
+ console.log('═'.repeat(100));
68
+
69
+ } catch (e) {
70
+ console.error('❌ Error parsing message:', e.message);
71
+ }
72
+ });
73
+
74
+ this.ws.on('close', () => {
75
+ console.log('\n🔌 Connection closed');
76
+ this.stopIntervals();
77
+ });
78
+
79
+ this.ws.on('error', (error) => {
80
+ console.error('❌ WebSocket error:', error.message);
81
+ this.stopIntervals();
82
+ });
83
+ }
84
+
85
+ startPolling() {
86
+ // Send keep-alive every 5 seconds
87
+ this.pollInterval = setInterval(() => {
88
+ if (this.ws.readyState === WebSocket.OPEN) {
89
+ console.log('\n⏱️ Sending keep-alive poll...');
90
+ this.ws.send('[]');
91
+ }
92
+ }, 5000);
93
+ }
94
+
95
+ startRandomControl() {
96
+ // Send random relay control every 10-15 seconds
97
+ this.randomInterval = setInterval(() => {
98
+ if (this.ws.readyState === WebSocket.OPEN && this.devices.length > 0) {
99
+ this.sendRandomControl();
100
+ }
101
+ }, 10000 + Math.random() * 5000);
102
+ }
103
+
104
+ sendRandomControl() {
105
+ if (this.devices.length === 0) {
106
+ console.log('⚠️ No devices available for control');
107
+ return;
108
+ }
109
+
110
+ // Pick 1-3 random devices
111
+ const numDevices = Math.min(this.devices.length, Math.floor(Math.random() * 3) + 1);
112
+ const shuffled = [...this.devices].sort(() => Math.random() - 0.5);
113
+ const selectedDevices = shuffled.slice(0, numDevices);
114
+
115
+ const commands = selectedDevices.map(device => ({
116
+ id: device.id,
117
+ relay: Math.random() > 0.5 // Random ON/OFF
118
+ }));
119
+
120
+ console.log('\n🎲 Sending random control commands:');
121
+ commands.forEach(cmd => {
122
+ const device = this.devices.find(d => d.id === cmd.id);
123
+ const action = cmd.relay ? '🟢 ON' : '🔴 OFF';
124
+ const node = Math.floor(cmd.id / 256);
125
+ const unit = cmd.id % 256;
126
+ console.log(` → ${device?.name} (node=${node}, unit=${unit}, id=${cmd.id}): ${action}`);
127
+ });
128
+
129
+ console.log('📤 Sending commands to server...');
130
+ console.log(JSON.stringify(commands, null, 2));
131
+ this.ws.send(JSON.stringify(commands));
132
+ }
133
+
134
+ stopIntervals() {
135
+ if (this.pollInterval) {
136
+ clearInterval(this.pollInterval);
137
+ this.pollInterval = null;
138
+ }
139
+ if (this.randomInterval) {
140
+ clearInterval(this.randomInterval);
141
+ this.randomInterval = null;
142
+ }
143
+ }
144
+
145
+ disconnect() {
146
+ console.log('\n🔌 Disconnecting...');
147
+ this.stopIntervals();
148
+ if (this.ws) {
149
+ this.ws.close();
150
+ }
151
+ }
152
+ }
153
+
154
+ // Main
155
+ const args = process.argv.slice(2);
156
+ const url = args[0] || 'ws://localhost:5002/devices';
157
+
158
+ console.log('╔════════════════════════════════════════════════════════════════════╗');
159
+ console.log('║ WebSocket Device Control Test Client ║');
160
+ console.log('║ Press Ctrl+C to exit ║');
161
+ console.log('╚════════════════════════════════════════════════════════════════════╝');
162
+
163
+ const client = new WebSocketDeviceTest(url);
164
+ client.connect();
165
+
166
+ // Handle graceful shutdown
167
+ process.on('SIGINT', () => {
168
+ console.log('\n\n👋 Shutting down gracefully...');
169
+ client.disconnect();
170
+ setTimeout(() => process.exit(0), 500);
171
+ });
172
+
173
+ // Keep the process running
174
+ process.stdin.resume();
package/run9998.js DELETED
@@ -1,7 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const socapp_1 = require("./server/socapp");
4
- console.log("run.ts - running in directory: " + process.cwd());
5
- const socServer = new socapp_1.SocApp(null, "server", 9998);
6
- socServer.serve();
7
- //# sourceMappingURL=run9998.js.map
package/run9998.ts DELETED
@@ -1,6 +0,0 @@
1
- import { SocApp } from "./server/socapp";
2
-
3
- console.log("run.ts - running in directory: " + process.cwd());
4
-
5
- const socServer = new SocApp(null, "server", 9998);
6
- socServer.serve();
package/run9999.js DELETED
@@ -1,7 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const socapp_1 = require("./server/socapp");
4
- console.log("run.ts - running in directory: " + process.cwd());
5
- const socServer = new socapp_1.SocApp(null, "server", 9999);
6
- socServer.serve();
7
- //# sourceMappingURL=run9999.js.map
package/run9999.ts DELETED
@@ -1,6 +0,0 @@
1
- import { SocApp } from "./server/socapp";
2
-
3
- console.log("run.ts - running in directory: " + process.cwd());
4
-
5
- const socServer = new SocApp(null, "server", 9999);
6
- socServer.serve();
File without changes