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.
- package/.vscode/launch.json +15 -4
- package/MIXINS-GUIDE.md +210 -0
- package/README.md +32 -0
- package/SETTINGS-UPDATE.md +195 -0
- package/TESTING-GUIDE.md +220 -0
- package/WEBSOCKET-API.md +303 -0
- package/accessories/accessory.js +20 -4
- package/accessories/accessory.ts +19 -4
- package/accessories/bulb.js +0 -5
- package/accessories/bulb.ts +1 -5
- package/accessories/dimmer.js +11 -3
- package/accessories/dimmer.ts +11 -4
- package/accessories/garagedoor.js +19 -9
- package/accessories/garagedoor.ts +21 -9
- package/accessories/lock.js +11 -3
- package/accessories/lock.ts +10 -3
- package/accessories/temperature.js +10 -2
- package/accessories/temperature.ts +9 -2
- package/accessories/windowcovering.js +30 -14
- package/accessories/windowcovering.ts +29 -15
- package/config.json +57 -19
- package/duotecno/types.js +48 -2
- package/duotecno/types.ts +75 -1
- package/package.json +12 -4
- package/raspberry-pi-setup.sh +80 -0
- package/server/HA-API.js +4 -1
- package/server/HA-API.ts +4 -1
- package/server/HB.js +6 -1
- package/server/HB.ts +5 -1
- package/server/mDNS.js +168 -0
- package/server/mDNS.ts +198 -0
- package/server/platform.js +7 -0
- package/server/platform.ts +8 -0
- package/server/proxy.js +58 -19
- package/server/proxy.ts +31 -3
- package/server/smartapp.js +397 -18
- package/server/smartapp.ts +432 -20
- package/server/socapp.js +2 -1
- package/server/socapp.ts +2 -1
- package/server/views/assets/Duotecno_logo_black.svg +10 -0
- package/server/views/assets/Duotecno_logo_white.svg +10 -0
- package/server/views/device-details.ejs +139 -0
- package/server/views/device-list.ejs +44 -0
- package/server/views/footer.ejs +0 -1
- package/server/views/head.ejs +9 -0
- package/server/views/homekit.ejs +2 -9
- package/server/views/link-details.ejs +2 -15
- package/server/views/link-list.ejs +2 -9
- package/server/views/login.ejs +8 -9
- package/server/views/master-detail.ejs +3 -9
- package/server/views/master-list.ejs +3 -10
- package/server/views/nav.ejs +44 -11
- package/server/views/node-details.ejs +1 -8
- package/server/views/power-binding.ejs +3 -10
- package/server/views/power-rule.ejs +3 -10
- package/server/views/power.ejs +8 -15
- package/server/views/proxy.ejs +1 -8
- package/server/views/scripts.ejs +10 -0
- package/server/views/service-list.ejs +2 -9
- package/server/views/settings.ejs +61 -22
- package/server/views/style.ejs +49 -5
- package/server/views/switch-details.ejs +2 -9
- package/server/views/switch-list.ejs +2 -9
- package/server/webapp.js +150 -11
- package/server/webapp.ts +166 -14
- package/{index.spec.js → testHB.js} +1 -1
- package/{run.js → testProxy.js} +2 -2
- package/{run.ts → testProxy.ts} +1 -1
- package/testWS.js +137 -0
- package/testWS.ts +174 -0
- package/run9998.js +0 -7
- package/run9998.ts +0 -6
- package/run9999.js +0 -7
- package/run9999.ts +0 -6
- /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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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=
|
|
41
|
+
//# sourceMappingURL=testHB.js.map
|
package/{run.js → testProxy.js}
RENAMED
|
@@ -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("
|
|
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=
|
|
7
|
+
//# sourceMappingURL=testProxy.js.map
|
package/{run.ts → testProxy.ts}
RENAMED
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
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
|
File without changes
|