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
@@ -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
- this.port = this.config.port || this.port || 80;
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") || (context.request === "links")) &&
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
- config = Object.assign({}, types_1.kEmptySettings);
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
- if (hbValue === -1) {
466
- (0, logger_1.log)("smartapp", "-> Virtual unit send 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
467
- const val = yield (0, HB_1.hbStop)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, this.token);
468
- if (typeof val != "undefined")
469
- link.hbValue = val;
470
- }
471
- else if (hbValue !== link.hbValue) {
472
- (0, logger_1.log)("smartapp", "-> Setting HB value for accessory: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName() + " with value " + u.value);
473
- link.hbValue = hbValue;
474
- yield (0, HB_1.hbSetState)(this.config.apiHost + ":" + this.config.apiPort, link.accId, link.accService, hbValue, this.token);
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
- else {
477
- (0, logger_1.debug)("smartapp", "-> No change for accessory: " + link.accName + " to " + u.value + " / " + u.status);
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
  }