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/smartapp.js
CHANGED
|
@@ -29,6 +29,7 @@ const config_1 = require("../duotecno/config");
|
|
|
29
29
|
const HB_1 = require("./HB");
|
|
30
30
|
const HA_API_1 = require("./HA-API");
|
|
31
31
|
const proxy_1 = require("./proxy");
|
|
32
|
+
const WebSocket = require("ws");
|
|
32
33
|
const kMaster = { name: "master", type: "string", default: "0.0.0.0:5001" };
|
|
33
34
|
const kAddress = { name: "address", type: "string", default: "0.0.0.0" };
|
|
34
35
|
const kAddresses = { name: "addresses", type: "string", default: "[0.0.0.0]" };
|
|
@@ -56,15 +57,37 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
56
57
|
this.system.emitter.on('update', this.informLinks.bind(this));
|
|
57
58
|
this.system.emitter.on('plug', this.alertSwitch.bind(this));
|
|
58
59
|
// get some configurated params
|
|
59
|
-
|
|
60
|
+
const configuredPort = this.config.port || 80;
|
|
60
61
|
this.password = platform.config.password || this.config.password || "";
|
|
61
62
|
this.user = platform.config.user || this.config.user || "pi";
|
|
63
|
+
// Set up multi-port listening if port is not 80
|
|
64
|
+
// Note: Port 80 requires root/sudo on Unix-like systems
|
|
65
|
+
// For development on macOS/Linux without sudo, use only configured port
|
|
66
|
+
if (configuredPort !== 80) {
|
|
67
|
+
// Check if we're running with appropriate privileges (process.getuid exists only on Unix)
|
|
68
|
+
const canBindPort80 = process.platform === 'win32' || (process.getuid && process.getuid() === 0);
|
|
69
|
+
if (canBindPort80) {
|
|
70
|
+
(0, logger_1.log)("smartapp", `Configured for dual-port mode: ${configuredPort} and 80`);
|
|
71
|
+
this.port = [80, configuredPort];
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
(0, logger_1.log)("smartapp", `Running without root privileges - listening only on port ${configuredPort}`);
|
|
75
|
+
(0, logger_1.log)("smartapp", `To enable port 80, run with sudo or use: sudo setcap 'cap_net_bind_service=+ep' $(which node)`);
|
|
76
|
+
this.port = configuredPort;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
else {
|
|
80
|
+
this.port = 80;
|
|
81
|
+
}
|
|
82
|
+
// Initialize mDNS service
|
|
83
|
+
this.setupMDNS();
|
|
62
84
|
this.system = system;
|
|
63
85
|
this.power = power;
|
|
64
86
|
this.platform = platform;
|
|
65
87
|
// when all masters are loaded -> attach units to the switches
|
|
66
88
|
this.system.emitter.on('ready', this.initSwitchUnits.bind(this));
|
|
67
89
|
this.system.emitter.on('ready', this.initLinks.bind(this));
|
|
90
|
+
this.system.emitter.on('ready', this.initDeviceUnits.bind(this));
|
|
68
91
|
this.addFile("unitList", __dirname + "/views/unit-list.ejs", "application/json");
|
|
69
92
|
this.addFile("masterList", __dirname + "/views/master-list.ejs", "text/html");
|
|
70
93
|
this.addFile("masterDetail", __dirname + "/views/master-detail.ejs", "text/html");
|
|
@@ -77,17 +100,77 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
77
100
|
this.addFile("switchList", __dirname + "/views/switch-list.ejs", "text/html");
|
|
78
101
|
this.addFile("linkDetail", __dirname + "/views/link-details.ejs", "text/html");
|
|
79
102
|
this.addFile("linkList", __dirname + "/views/link-list.ejs", "text/html");
|
|
103
|
+
this.addFile("deviceDetail", __dirname + "/views/device-details.ejs", "text/html");
|
|
104
|
+
this.addFile("deviceList", __dirname + "/views/device-list.ejs", "text/html");
|
|
80
105
|
this.addFile("powerRule", __dirname + "/views/power-rule.ejs", "text/html");
|
|
81
106
|
this.addFile("powerBinding", __dirname + "/views/power-binding.ejs", "text/html");
|
|
82
107
|
this.addFile("materializeCSS", __dirname + "/views/assets/materialize.min.css", "text/css");
|
|
83
108
|
this.addFile("materializeJS", __dirname + "/views/assets/materialize.min.js", "text/javascript");
|
|
84
109
|
this.addFile("favicon", __dirname + "/views/assets/favicon.ico", "image/x-icon");
|
|
110
|
+
this.addFile("logoWhite", __dirname + "/views/assets/Duotecno_logo_white.svg", "image/svg+xml");
|
|
85
111
|
this.addFile("proxy", __dirname + "/views/proxy.ejs", "text/html");
|
|
86
112
|
}
|
|
113
|
+
// Override to use config.system.debug for template caching control
|
|
114
|
+
isDebugMode() {
|
|
115
|
+
var _a, _b;
|
|
116
|
+
return ((_b = (_a = this.system) === null || _a === void 0 ? void 0 : _a.config) === null || _b === void 0 ? void 0 : _b.debug) === true;
|
|
117
|
+
}
|
|
118
|
+
setupMDNS() {
|
|
119
|
+
// Read settings to get configured mDNS name
|
|
120
|
+
const settings = this.read("settings");
|
|
121
|
+
const mdnsName = (settings === null || settings === void 0 ? void 0 : settings.mdnsName) || "duotecno-gateway";
|
|
122
|
+
const version = require('../package.json').version;
|
|
123
|
+
// Initialize mDNS through parent class
|
|
124
|
+
this.initMDNS(mdnsName, version);
|
|
125
|
+
}
|
|
126
|
+
serve(onConnected) {
|
|
127
|
+
// Initialize WebSocket servers array
|
|
128
|
+
this.wsServers = [];
|
|
129
|
+
// Call parent serve method
|
|
130
|
+
super.serve(() => {
|
|
131
|
+
// Set up WebSocket servers on the HTTP servers
|
|
132
|
+
this.setupWebSocketServers();
|
|
133
|
+
// Register mDNS service after server is listening
|
|
134
|
+
this.registerMDNS();
|
|
135
|
+
// Call the original callback if provided
|
|
136
|
+
if (onConnected) {
|
|
137
|
+
onConnected();
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
setupWebSocketServers() {
|
|
142
|
+
// Get all HTTP servers
|
|
143
|
+
this.servers.forEach((httpServer, index) => {
|
|
144
|
+
const wss = new WebSocket.Server({ server: httpServer, path: '/devices' });
|
|
145
|
+
this.wsServers.push(wss);
|
|
146
|
+
wss.on('connection', (ws) => {
|
|
147
|
+
(0, logger_1.log)("smartapp", `WebSocket client connected on server ${index}`);
|
|
148
|
+
// Send initial device list
|
|
149
|
+
this.sendDeviceList(ws);
|
|
150
|
+
ws.on('message', (message) => {
|
|
151
|
+
try {
|
|
152
|
+
const data = JSON.parse(message.toString());
|
|
153
|
+
this.handleDeviceMessage(ws, data);
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
(0, logger_1.err)("smartapp", `Invalid WebSocket message: ${e.message}`);
|
|
157
|
+
}
|
|
158
|
+
});
|
|
159
|
+
ws.on('close', () => {
|
|
160
|
+
(0, logger_1.log)("smartapp", `WebSocket client disconnected from server ${index}`);
|
|
161
|
+
});
|
|
162
|
+
ws.on('error', (error) => {
|
|
163
|
+
(0, logger_1.err)("smartapp", `WebSocket error on server ${index}: ${error.message}`);
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
(0, logger_1.log)("smartapp", `WebSocket server ${index} set up on path /devices`);
|
|
167
|
+
});
|
|
168
|
+
}
|
|
87
169
|
writeConfig() {
|
|
88
170
|
// copy switches into config, eliminate the runtime stuff (like unit)
|
|
89
171
|
this.config.switches = this.switches.map(s => types_1.Sanitizers.makeSwitchConfig(s));
|
|
90
172
|
this.config.links = this.links.map(s => types_1.Sanitizers.makeLinkConfig(s));
|
|
173
|
+
this.config.devices = this.devices.map(d => types_1.Sanitizers.makeDeviceConfig(d));
|
|
91
174
|
super.writeConfig();
|
|
92
175
|
}
|
|
93
176
|
readConfig() {
|
|
@@ -95,6 +178,7 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
95
178
|
// copy switches from config
|
|
96
179
|
this.switches = this.config.switches.map(s => types_1.Sanitizers.switchConfig(s));
|
|
97
180
|
this.links = this.config.links.map(s => types_1.Sanitizers.linkConfig(s));
|
|
181
|
+
this.devices = (this.config.devices || []).map(d => types_1.Sanitizers.deviceConfig(d));
|
|
98
182
|
}
|
|
99
183
|
checkReady(context) {
|
|
100
184
|
if (this.platform && !this.platform.ready) {
|
|
@@ -140,7 +224,7 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
140
224
|
// Router //
|
|
141
225
|
//////////////////////////////
|
|
142
226
|
needsLogin(context) {
|
|
143
|
-
const noNeed = ((context.request === "units") || (context.request === "switches")
|
|
227
|
+
const noNeed = ((context.request === "units") || (context.request === "switches")) &&
|
|
144
228
|
((context.action == "press") || (context.action == "get") || (context.action == "set"));
|
|
145
229
|
return super.needsLogin(context) && (context.request != "files") && (!noNeed);
|
|
146
230
|
}
|
|
@@ -181,6 +265,9 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
181
265
|
else if (context.request === "links") {
|
|
182
266
|
return this.doLinks(context);
|
|
183
267
|
}
|
|
268
|
+
else if (context.request === "devices") {
|
|
269
|
+
return this.doDevices(context);
|
|
270
|
+
}
|
|
184
271
|
else if (context.request === "switches") {
|
|
185
272
|
return this.doSwitches(context);
|
|
186
273
|
}
|
|
@@ -261,32 +348,55 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
261
348
|
doSettings(context) {
|
|
262
349
|
return __awaiter(this, void 0, void 0, function* () {
|
|
263
350
|
let config = this.read("settings");
|
|
351
|
+
let message = "";
|
|
264
352
|
if (context.action === "save") {
|
|
265
353
|
config = this.scrapeSettings(context);
|
|
266
354
|
this.write("settings", config);
|
|
355
|
+
message = "Settings saved successfully. Restart the server for changes to take effect.";
|
|
356
|
+
// Re-register mDNS with new name if changed
|
|
357
|
+
if (this.mdnsService) {
|
|
358
|
+
// Note: Full restart recommended for mDNS changes to take effect properly
|
|
359
|
+
(0, logger_1.log)("smartapp", "Settings saved. mDNS name updated to: " + config.mdnsName);
|
|
360
|
+
}
|
|
267
361
|
}
|
|
268
362
|
else if (context.action === "restart") {
|
|
269
363
|
context.request = "restart";
|
|
270
364
|
return this.doRestart(false);
|
|
271
365
|
}
|
|
272
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
|
|
273
371
|
config = this.scrapeSettings(context);
|
|
274
372
|
context.request = "install";
|
|
275
373
|
this.write("settings", config);
|
|
276
|
-
this.writeDHCP(context, config);
|
|
374
|
+
this.writeDHCP(context, <SettingsConfig>config);
|
|
375
|
+
*/
|
|
277
376
|
}
|
|
278
377
|
else if (context.action === "reset") {
|
|
279
|
-
|
|
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 };
|
|
280
383
|
this.resetDHCP(context);
|
|
281
384
|
this.write("settings", config);
|
|
282
385
|
return this.doReboot(context, false);
|
|
386
|
+
*/
|
|
283
387
|
}
|
|
284
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
|
|
285
393
|
return this.doReboot(context, false);
|
|
394
|
+
*/
|
|
286
395
|
}
|
|
287
396
|
else {
|
|
397
|
+
// Just display the settings form
|
|
288
398
|
}
|
|
289
|
-
return this.ejs("settings", context, { config });
|
|
399
|
+
return this.ejs("settings", context, { config, message });
|
|
290
400
|
});
|
|
291
401
|
}
|
|
292
402
|
scrapeSettings(context) {
|
|
@@ -297,7 +407,8 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
297
407
|
nameservers: context.getParam({ name: "nameservers", type: "string" }),
|
|
298
408
|
ip2: context.getParam({ name: "ip2", type: "string" }),
|
|
299
409
|
gateway2: context.getParam({ name: "gateway2", type: "string" })
|
|
300
|
-
}
|
|
410
|
+
},
|
|
411
|
+
mdnsName: context.getParam({ name: "mdnsName", type: "string", default: "duotecno-gateway" })
|
|
301
412
|
});
|
|
302
413
|
}
|
|
303
414
|
writeDHCP(context, config) {
|
|
@@ -462,19 +573,24 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
462
573
|
if (u.isUnit(link.masterAddress, link.masterPort, link.logicalNodeAddress, link.logicalAddress)) {
|
|
463
574
|
const hbValue = (0, HB_1.dt2hbvalue)(u, link.hbValue, link.min, link.max);
|
|
464
575
|
if (typeof hbValue != "undefined") {
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
576
|
+
try {
|
|
577
|
+
if (hbValue === -1) {
|
|
578
|
+
(0, logger_1.log)("smartapp", "-> Virtual unit send 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
|
|
579
|
+
const val = yield (0, HB_1.hbStop)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, this.token);
|
|
580
|
+
if (typeof val != "undefined")
|
|
581
|
+
link.hbValue = val;
|
|
582
|
+
}
|
|
583
|
+
else if (hbValue !== link.hbValue) {
|
|
584
|
+
(0, logger_1.log)("smartapp", "-> Setting HB value for accessory: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName() + " with value " + u.value);
|
|
585
|
+
link.hbValue = hbValue;
|
|
586
|
+
yield (0, HB_1.hbSetState)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, hbValue, this.token);
|
|
587
|
+
}
|
|
588
|
+
else {
|
|
589
|
+
(0, logger_1.debug)("smartapp", "-> No change for accessory: " + link.accName + " to " + u.value + " / " + u.status);
|
|
590
|
+
}
|
|
475
591
|
}
|
|
476
|
-
|
|
477
|
-
(0, logger_1.
|
|
592
|
+
catch (e) {
|
|
593
|
+
(0, logger_1.err)("smartapp", "informLinks: failed to set HB value for accessory " + link.accName + " -> " + e.toString());
|
|
478
594
|
}
|
|
479
595
|
}
|
|
480
596
|
}
|
|
@@ -568,6 +684,266 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
568
684
|
this.writeConfig();
|
|
569
685
|
}
|
|
570
686
|
}
|
|
687
|
+
/////////////
|
|
688
|
+
// Devices //
|
|
689
|
+
/////////////
|
|
690
|
+
initDeviceUnits() {
|
|
691
|
+
(0, logger_1.log)("smartapp", "Init " + this.devices.length + " Devices -> add units");
|
|
692
|
+
this.devices.forEach(device => {
|
|
693
|
+
device.unit || (device.unit = this.system.findUnit(this.system.findMaster(device.masterAddress, device.masterPort), device.logicalNodeAddress, device.logicalAddress));
|
|
694
|
+
if (device.unit) {
|
|
695
|
+
device.relay = device.unit.status > 0;
|
|
696
|
+
}
|
|
697
|
+
else {
|
|
698
|
+
(0, logger_1.log)("smartapp", "** error ** missing device unit: " + (0, types_1.hex)(device.logicalNodeAddress) + "/" + (0, types_1.hex)(device.logicalAddress) + " **");
|
|
699
|
+
}
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
doDevices(context) {
|
|
703
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
704
|
+
let inx = parseInt(context.id);
|
|
705
|
+
const units = this.system.allUsedUnits();
|
|
706
|
+
let message;
|
|
707
|
+
try {
|
|
708
|
+
if (context.action === "add") {
|
|
709
|
+
this.devices.push(Object.assign(Object.assign({}, types_1.kEmptyDevice), { measured: false }));
|
|
710
|
+
inx = this.devices.length - 1;
|
|
711
|
+
return this.ejs("deviceDetail", context, { device: this.devices[inx], id: inx,
|
|
712
|
+
masters: this.system.masters, units });
|
|
713
|
+
}
|
|
714
|
+
else if (context.action === "edit") {
|
|
715
|
+
return this.ejs("deviceDetail", context, { device: this.devices[inx], id: inx,
|
|
716
|
+
masters: this.system.masters, units });
|
|
717
|
+
}
|
|
718
|
+
else if (context.action === "delete") {
|
|
719
|
+
this.deleteDevice(inx);
|
|
720
|
+
}
|
|
721
|
+
else if (context.action === "change") {
|
|
722
|
+
this.updateDevice(inx, this.scrapeDevice(context));
|
|
723
|
+
}
|
|
724
|
+
else {
|
|
725
|
+
// possible new IP Nodes, hence Units could be online
|
|
726
|
+
this.initDeviceUnits();
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
catch (e) {
|
|
730
|
+
message = e.toString();
|
|
731
|
+
}
|
|
732
|
+
return this.ejs("deviceList", context, { masters: this.system.masters, devices: this.devices, message });
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
scrapeDevice(context) {
|
|
736
|
+
const { masterAddress, masterPort, logicalAddress, logicalNodeAddress } = this.scrapeUnit(context, '');
|
|
737
|
+
const name = context.getParam({ name: "name", type: "string", default: "Device" });
|
|
738
|
+
const powerSource = context.getParam({ name: "powerSource", type: "string", default: "none" });
|
|
739
|
+
const powerChannel = context.getParam({ name: "powerChannel", type: "string", default: "" });
|
|
740
|
+
const estimatedPower = context.getParam({ name: "estimatedPower", type: "integer", default: 0 });
|
|
741
|
+
// Calculate ID from node/unit
|
|
742
|
+
const id = logicalNodeAddress * 256 + logicalAddress;
|
|
743
|
+
// Measured is true if powerSource is not "none"
|
|
744
|
+
const measured = powerSource !== "none";
|
|
745
|
+
return {
|
|
746
|
+
id,
|
|
747
|
+
name,
|
|
748
|
+
masterAddress,
|
|
749
|
+
masterPort,
|
|
750
|
+
logicalAddress,
|
|
751
|
+
logicalNodeAddress,
|
|
752
|
+
measured,
|
|
753
|
+
powerSource: powerSource,
|
|
754
|
+
powerChannel,
|
|
755
|
+
estimatedPower,
|
|
756
|
+
power: 0,
|
|
757
|
+
energy: 0,
|
|
758
|
+
voltage: 230.0,
|
|
759
|
+
current: 0,
|
|
760
|
+
relay: false
|
|
761
|
+
};
|
|
762
|
+
}
|
|
763
|
+
updateDevice(inx, device) {
|
|
764
|
+
if ((inx >= 0) && (inx < this.devices.length)) {
|
|
765
|
+
this.devices[inx] = device;
|
|
766
|
+
this.initDeviceUnits();
|
|
767
|
+
this.writeConfig();
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
deleteDevice(inx) {
|
|
771
|
+
if ((inx >= 0) && (inx < this.devices.length)) {
|
|
772
|
+
this.devices.splice(inx, 1);
|
|
773
|
+
this.initDeviceUnits();
|
|
774
|
+
this.writeConfig();
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
//////////////////////
|
|
778
|
+
// Device - sockets //
|
|
779
|
+
//////////////////////
|
|
780
|
+
sendDeviceList(ws) {
|
|
781
|
+
const devices = this.getDeviceList();
|
|
782
|
+
if (ws.readyState === WebSocket.OPEN) {
|
|
783
|
+
ws.send(JSON.stringify(devices));
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
getDeviceList() {
|
|
787
|
+
return this.devices.map(device => {
|
|
788
|
+
// Get current relay state from unit if available
|
|
789
|
+
let relayState = device.relay || false;
|
|
790
|
+
if (device.unit) {
|
|
791
|
+
relayState = device.unit.status > 0;
|
|
792
|
+
}
|
|
793
|
+
// Get power measurements based on power source
|
|
794
|
+
let power = 0;
|
|
795
|
+
let energy = 0;
|
|
796
|
+
let voltage = 230.0;
|
|
797
|
+
let current = 0;
|
|
798
|
+
let measured = false;
|
|
799
|
+
if (device.powerSource === types_1.PowerSource.kSmappee && this.power.smappee) {
|
|
800
|
+
// Get power from Smappee
|
|
801
|
+
const smappeePower = this.getPowerFromSmappee(device.powerChannel);
|
|
802
|
+
if (smappeePower !== null) {
|
|
803
|
+
power = smappeePower;
|
|
804
|
+
measured = true;
|
|
805
|
+
current = power / voltage;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
else if (device.powerSource === types_1.PowerSource.kP1 && this.power.p1) {
|
|
809
|
+
// Get power from P1 meter
|
|
810
|
+
const p1Power = this.getPowerFromP1(device.powerChannel);
|
|
811
|
+
if (p1Power !== null) {
|
|
812
|
+
power = p1Power;
|
|
813
|
+
measured = true;
|
|
814
|
+
current = power / voltage;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
else if (device.powerSource === types_1.PowerSource.kShelly && this.power.shelly) {
|
|
818
|
+
// Get power from Shelly
|
|
819
|
+
const shellyPower = this.getPowerFromShelly(device.powerChannel);
|
|
820
|
+
if (shellyPower !== null) {
|
|
821
|
+
power = shellyPower;
|
|
822
|
+
measured = true;
|
|
823
|
+
current = power / voltage;
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
else if (device.powerSource === types_1.PowerSource.kNone || !device.powerSource) {
|
|
827
|
+
// Use estimated power when relay is on
|
|
828
|
+
power = relayState ? (device.estimatedPower || 0) : 0;
|
|
829
|
+
measured = false;
|
|
830
|
+
}
|
|
831
|
+
return {
|
|
832
|
+
id: device.id,
|
|
833
|
+
name: device.name,
|
|
834
|
+
power: power,
|
|
835
|
+
energy: energy,
|
|
836
|
+
voltage: voltage,
|
|
837
|
+
current: current,
|
|
838
|
+
relay: relayState,
|
|
839
|
+
measured: measured
|
|
840
|
+
};
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
getPowerFromSmappee(channel) {
|
|
844
|
+
if (!this.power.smappee)
|
|
845
|
+
return null;
|
|
846
|
+
try {
|
|
847
|
+
// Channel format could be "0", "0+1", "0+1+2", etc.
|
|
848
|
+
const channels = channel.split('+').map(c => parseInt(c.trim()));
|
|
849
|
+
let totalPower = 0;
|
|
850
|
+
for (const ch of channels) {
|
|
851
|
+
const binding = this.power.smappee.bindings.find(b => b.channel.includes(ch.toString()));
|
|
852
|
+
if (binding && binding.value !== undefined) {
|
|
853
|
+
totalPower += binding.value;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
return totalPower;
|
|
857
|
+
}
|
|
858
|
+
catch (e) {
|
|
859
|
+
(0, logger_1.err)("smartapp", `Error getting Smappee power for channel ${channel}: ${e.message}`);
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
getPowerFromP1(channel) {
|
|
864
|
+
if (!this.power.p1)
|
|
865
|
+
return null;
|
|
866
|
+
try {
|
|
867
|
+
// P1 meter typically has single power value
|
|
868
|
+
const binding = this.power.p1.bindings.find(b => b.channel.includes(channel));
|
|
869
|
+
if (binding && binding.value !== undefined) {
|
|
870
|
+
return binding.value;
|
|
871
|
+
}
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
catch (e) {
|
|
875
|
+
(0, logger_1.err)("smartapp", `Error getting P1 power for channel ${channel}: ${e.message}`);
|
|
876
|
+
return null;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
getPowerFromShelly(channel) {
|
|
880
|
+
if (!this.power.shelly)
|
|
881
|
+
return null;
|
|
882
|
+
try {
|
|
883
|
+
// Shelly channel format similar to Smappee
|
|
884
|
+
const channels = channel.split('+').map(c => parseInt(c.trim()));
|
|
885
|
+
let totalPower = 0;
|
|
886
|
+
for (const ch of channels) {
|
|
887
|
+
const binding = this.power.shelly.bindings.find(b => b.channel.includes(ch.toString()));
|
|
888
|
+
if (binding && binding.value !== undefined) {
|
|
889
|
+
totalPower += binding.value;
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return totalPower;
|
|
893
|
+
}
|
|
894
|
+
catch (e) {
|
|
895
|
+
(0, logger_1.err)("smartapp", `Error getting Shelly power for channel ${channel}: ${e.message}`);
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
handleDeviceMessage(ws, data) {
|
|
900
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
901
|
+
if (!Array.isArray(data)) {
|
|
902
|
+
(0, logger_1.err)("smartapp", "WebSocket message must be an array");
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
// Empty array = keep-alive / poll
|
|
906
|
+
if (data.length === 0) {
|
|
907
|
+
this.sendDeviceList(ws);
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
// Process relay control commands
|
|
911
|
+
for (const cmd of data) {
|
|
912
|
+
if (cmd.id && cmd.relay !== undefined) {
|
|
913
|
+
yield this.setDeviceRelay(cmd.id, cmd.relay);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
// Send updated device list
|
|
917
|
+
this.sendDeviceList(ws);
|
|
918
|
+
});
|
|
919
|
+
}
|
|
920
|
+
setDeviceRelay(id, state) {
|
|
921
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
922
|
+
// Convert ID to node/unit (id = node * 256 + unit)
|
|
923
|
+
const node = Math.floor(id / 256);
|
|
924
|
+
const unit = id % 256;
|
|
925
|
+
(0, logger_1.log)("smartapp", `Setting device ${id} (node=${node}, unit=${unit}) to ${state}`);
|
|
926
|
+
// Get the first master
|
|
927
|
+
const master = this.system.masters[0];
|
|
928
|
+
if (!master) {
|
|
929
|
+
(0, logger_1.err)("smartapp", "No master available");
|
|
930
|
+
return;
|
|
931
|
+
}
|
|
932
|
+
// Find the unit
|
|
933
|
+
const nodeObj = master.nodes.find(n => n.logicalAddress === node);
|
|
934
|
+
if (!nodeObj) {
|
|
935
|
+
(0, logger_1.err)("smartapp", `Node ${node} not found`);
|
|
936
|
+
return;
|
|
937
|
+
}
|
|
938
|
+
const unitObj = nodeObj.units.find(u => u.logicalAddress === unit);
|
|
939
|
+
if (!unitObj) {
|
|
940
|
+
(0, logger_1.err)("smartapp", `Unit ${unit} in node ${node} not found`);
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
// Set the unit state (convert boolean to 0/100 for dimmer/switch)
|
|
944
|
+
yield master.setUnitStatus(unitObj, state ? 100 : 0);
|
|
945
|
+
});
|
|
946
|
+
}
|
|
571
947
|
setLink(inx, newstate, newvalue) {
|
|
572
948
|
return __awaiter(this, void 0, void 0, function* () {
|
|
573
949
|
// find the switch if an index is given
|
|
@@ -1172,6 +1548,9 @@ class SmartApp extends webapp_1.WebApp {
|
|
|
1172
1548
|
else if (file === "favicon.ico") {
|
|
1173
1549
|
return this.file("favicon");
|
|
1174
1550
|
}
|
|
1551
|
+
else if (file === "Duotecno_logo_white.svg") {
|
|
1552
|
+
return this.file("logoWhite");
|
|
1553
|
+
}
|
|
1175
1554
|
else
|
|
1176
1555
|
return this.notFound();
|
|
1177
1556
|
}
|