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
@@ -9,7 +9,7 @@
9
9
  // v2.0: mar/apr 2020
10
10
 
11
11
  import { Context, HttpResponse, WebApp } from "./webapp";
12
- import { Sanitizers, Rule, kEmptyRule, actionValue, Action, kEmptySwitch, Switch, SwitchType, hex, Binding, NetworkConfig, SettingsConfig, kEmptySettings, Link, LinkType, kEmptyLink } from "../duotecno/types";
12
+ import { Sanitizers, Rule, kEmptyRule, actionValue, Action, kEmptySwitch, Switch, SwitchType, hex, Binding, NetworkConfig, SettingsConfig, kEmptySettings, Link, LinkType, kEmptyLink, Device, kEmptyDevice, PowerSource } from "../duotecno/types";
13
13
  import { log, debug, err } from "../duotecno/logger";
14
14
  import { System } from "../duotecno/system";
15
15
  import { Node, Unit } from "../duotecno/protocol";
@@ -25,6 +25,7 @@ import { PowerBase } from "./powerbase";
25
25
  import { dt2hbvalue, hb2dtvalue, hbAccessories, hbGetValue, hbLogin, hbName, hbService, hbSetState, hbStop, hbSubscribe } from "./HB";
26
26
  import { httpRequest, ContentType } from "./HA-API";
27
27
  import { cleanStart, kEmptyProxy, ProxyConfig, setProxyConfig } from "./proxy";
28
+ import * as WebSocket from 'ws';
28
29
 
29
30
  const kMaster = {name: "master", type: "string", default: "0.0.0.0:5001"} as const;
30
31
  const kAddress = {name: "address", type: "string", default: "0.0.0.0"} as const;
@@ -60,7 +61,9 @@ export class SmartApp extends WebApp {
60
61
  platform: Platform;
61
62
  switches: Array<Switch>;
62
63
  links: Array<Link>;
64
+ devices: Array<Device>;
63
65
  proxy: ProxyConfig;
66
+ wsServers: WebSocket.Server[]; // WebSocket servers for device control
64
67
 
65
68
 
66
69
  constructor(system: System, power: Power, platform: Platform) {
@@ -73,9 +76,31 @@ export class SmartApp extends WebApp {
73
76
  this.system.emitter.on('plug', this.alertSwitch.bind(this));
74
77
 
75
78
  // get some configurated params
76
- this.port = this.config.port || this.port || 80;
79
+ const configuredPort = this.config.port || 80;
77
80
  this.password = platform.config.password || this.config.password || "";
78
- this.user = platform.config.user || this.config.user || "pi";
81
+ this.user = platform.config.user || this.config.user || "pi";
82
+
83
+ // Set up multi-port listening if port is not 80
84
+ // Note: Port 80 requires root/sudo on Unix-like systems
85
+ // For development on macOS/Linux without sudo, use only configured port
86
+ if (configuredPort !== 80) {
87
+ // Check if we're running with appropriate privileges (process.getuid exists only on Unix)
88
+ const canBindPort80 = process.platform === 'win32' || (process.getuid && process.getuid() === 0);
89
+
90
+ if (canBindPort80) {
91
+ log("smartapp", `Configured for dual-port mode: ${configuredPort} and 80`);
92
+ this.port = [80, configuredPort];
93
+ } else {
94
+ log("smartapp", `Running without root privileges - listening only on port ${configuredPort}`);
95
+ log("smartapp", `To enable port 80, run with sudo or use: sudo setcap 'cap_net_bind_service=+ep' $(which node)`);
96
+ this.port = configuredPort;
97
+ }
98
+ } else {
99
+ this.port = 80;
100
+ }
101
+
102
+ // Initialize mDNS service
103
+ this.setupMDNS();
79
104
 
80
105
  this.system = system;
81
106
  this.power = power;
@@ -84,6 +109,7 @@ export class SmartApp extends WebApp {
84
109
  // when all masters are loaded -> attach units to the switches
85
110
  this.system.emitter.on('ready', this.initSwitchUnits.bind(this));
86
111
  this.system.emitter.on('ready', this.initLinks.bind(this));
112
+ this.system.emitter.on('ready', this.initDeviceUnits.bind(this));
87
113
 
88
114
  this.addFile("unitList", __dirname + "/views/unit-list.ejs", "application/json");
89
115
  this.addFile("masterList", __dirname + "/views/master-list.ejs", "text/html");
@@ -97,19 +123,92 @@ export class SmartApp extends WebApp {
97
123
  this.addFile("switchList", __dirname + "/views/switch-list.ejs", "text/html");
98
124
  this.addFile("linkDetail", __dirname + "/views/link-details.ejs", "text/html");
99
125
  this.addFile("linkList", __dirname + "/views/link-list.ejs", "text/html");
126
+ this.addFile("deviceDetail", __dirname + "/views/device-details.ejs", "text/html");
127
+ this.addFile("deviceList", __dirname + "/views/device-list.ejs", "text/html");
100
128
  this.addFile("powerRule", __dirname + "/views/power-rule.ejs", "text/html");
101
129
  this.addFile("powerBinding", __dirname + "/views/power-binding.ejs", "text/html");
102
130
  this.addFile("materializeCSS", __dirname + "/views/assets/materialize.min.css", "text/css");
103
131
  this.addFile("materializeJS", __dirname + "/views/assets/materialize.min.js", "text/javascript");
104
132
  this.addFile("favicon", __dirname + "/views/assets/favicon.ico", "image/x-icon");
133
+ this.addFile("logoWhite", __dirname + "/views/assets/Duotecno_logo_white.svg", "image/svg+xml");
105
134
 
106
135
  this.addFile("proxy", __dirname + "/views/proxy.ejs", "text/html");
107
136
  }
108
137
 
138
+ // Override to use config.system.debug for template caching control
139
+ protected isDebugMode(): boolean {
140
+ return this.system?.config?.debug === true;
141
+ }
142
+
143
+ setupMDNS() {
144
+ // Read settings to get configured mDNS name
145
+ const settings = this.read("settings") as SettingsConfig;
146
+ const mdnsName = settings?.mdnsName || "duotecno-gateway";
147
+ const version = require('../package.json').version;
148
+
149
+ // Initialize mDNS through parent class
150
+ this.initMDNS(mdnsName, version);
151
+ }
152
+
153
+ serve(onConnected?: () => void) {
154
+ // Initialize WebSocket servers array
155
+ this.wsServers = [];
156
+
157
+ // Call parent serve method
158
+ super.serve(() => {
159
+ // Set up WebSocket servers on the HTTP servers
160
+ this.setupWebSocketServers();
161
+
162
+ // Register mDNS service after server is listening
163
+ this.registerMDNS();
164
+
165
+ // Call the original callback if provided
166
+ if (onConnected) {
167
+ onConnected();
168
+ }
169
+ });
170
+ }
171
+
172
+ setupWebSocketServers() {
173
+ // Get all HTTP servers
174
+ this.servers.forEach((httpServer, index) => {
175
+ const wss = new WebSocket.Server({ server: httpServer, path: '/devices' });
176
+ this.wsServers.push(wss);
177
+
178
+ wss.on('connection', (ws: WebSocket) => {
179
+ log("smartapp", `WebSocket client connected on server ${index}`);
180
+
181
+ // Send initial device list
182
+ this.sendDeviceList(ws);
183
+
184
+ ws.on('message', (message: string) => {
185
+ try {
186
+ const data = JSON.parse(message.toString());
187
+ this.handleDeviceMessage(ws, data);
188
+ } catch (e) {
189
+ err("smartapp", `Invalid WebSocket message: ${e.message}`);
190
+ }
191
+ });
192
+
193
+ ws.on('close', () => {
194
+ log("smartapp", `WebSocket client disconnected from server ${index}`);
195
+ });
196
+
197
+ ws.on('error', (error) => {
198
+ err("smartapp", `WebSocket error on server ${index}: ${error.message}`);
199
+ });
200
+ });
201
+
202
+ log("smartapp", `WebSocket server ${index} set up on path /devices`);
203
+ });
204
+ }
205
+
206
+
109
207
  writeConfig() {
110
208
  // copy switches into config, eliminate the runtime stuff (like unit)
111
209
  this.config.switches = this.switches.map(s => Sanitizers.makeSwitchConfig(s));
112
210
  this.config.links = this.links.map(s => Sanitizers.makeLinkConfig(s));
211
+ this.config.devices = this.devices.map(d => Sanitizers.makeDeviceConfig(d));
113
212
 
114
213
  super.writeConfig();
115
214
  }
@@ -121,6 +220,7 @@ export class SmartApp extends WebApp {
121
220
  // copy switches from config
122
221
  this.switches = this.config.switches.map(s => Sanitizers.switchConfig(s));
123
222
  this.links = this.config.links.map(s => Sanitizers.linkConfig(s));
223
+ this.devices = (this.config.devices || []).map(d => Sanitizers.deviceConfig(d));
124
224
  }
125
225
 
126
226
 
@@ -170,7 +270,7 @@ export class SmartApp extends WebApp {
170
270
  //////////////////////////////
171
271
 
172
272
  needsLogin(context: Context): boolean {
173
- const noNeed = ((context.request === "units") || (context.request === "switches") || (context.request === "links")) &&
273
+ const noNeed = ((context.request === "units") || (context.request === "switches")) &&
174
274
  ((context.action == "press") || (context.action == "get") || (context.action == "set"));
175
275
  return super.needsLogin(context) && (context.request != "files") && (!noNeed);
176
276
  }
@@ -210,6 +310,9 @@ export class SmartApp extends WebApp {
210
310
  } else if (context.request === "links") {
211
311
  return this.doLinks(context);
212
312
 
313
+ } else if (context.request === "devices") {
314
+ return this.doDevices(context);
315
+
213
316
  } else if (context.request === "switches") {
214
317
  return this.doSwitches(context);
215
318
 
@@ -290,35 +393,61 @@ export class SmartApp extends WebApp {
290
393
  // Settings //
291
394
  //////////////
292
395
  async doSettings(context: Context): Promise<HttpResponse> {
293
- let config = this.read("settings");
396
+ let config: SettingsConfig = this.read("settings") as SettingsConfig;
397
+ let message = "";
294
398
 
295
399
  if (context.action === "save") {
296
400
  config = this.scrapeSettings(context);
297
401
  this.write("settings", config);
402
+ message = "Settings saved successfully. Restart the server for changes to take effect.";
403
+
404
+ // Re-register mDNS with new name if changed
405
+ if (this.mdnsService) {
406
+ // Note: Full restart recommended for mDNS changes to take effect properly
407
+ log("smartapp", "Settings saved. mDNS name updated to: " + config.mdnsName);
408
+ }
298
409
 
299
410
  } else if (context.action === "restart") {
300
411
  context.request = "restart";
301
412
  return this.doRestart(false)
302
413
 
303
414
  } else if (context.action === "install") {
415
+ // Network IP configuration - temporarily disabled
416
+ // TODO: Re-implement DHCP configuration
417
+ message = "Network IP configuration is temporarily disabled. Use 'Save Settings' for mDNS configuration.";
418
+
419
+ /* Commented out until DHCP implementation is fixed
304
420
  config = this.scrapeSettings(context);
305
421
  context.request = "install";
306
422
  this.write("settings", config);
307
423
  this.writeDHCP(context, <SettingsConfig>config);
424
+ */
308
425
 
309
426
  } else if (context.action === "reset") {
427
+ // Reset to DHCP - temporarily disabled
428
+ // TODO: Re-implement DHCP reset
429
+ message = "DHCP reset is temporarily disabled.";
430
+
431
+ /* Commented out until DHCP implementation is fixed
310
432
  config = { ...kEmptySettings };
311
433
  this.resetDHCP(context);
312
434
  this.write("settings", config);
313
435
  return this.doReboot(context, false);
436
+ */
314
437
 
315
438
  } else if (context.action === "reboot") {
439
+ // System reboot - temporarily disabled
440
+ // TODO: Re-enable after testing
441
+ message = "System reboot is temporarily disabled.";
442
+
443
+ /* Commented out until we want to enable system reboot
316
444
  return this.doReboot(context, false);
445
+ */
317
446
 
318
447
  } else {
319
-
448
+ // Just display the settings form
320
449
  }
321
- return this.ejs("settings", context, { config });
450
+ return this.ejs("settings", context, { config, message });
322
451
  }
323
452
 
324
453
  scrapeSettings(context: Context): SettingsConfig {
@@ -329,7 +458,8 @@ export class SmartApp extends WebApp {
329
458
  nameservers: context.getParam({name: "nameservers", type: "string"}),
330
459
  ip2: context.getParam({name: "ip2", type: "string"}),
331
460
  gateway2: context.getParam({name: "gateway2", type: "string"})
332
- }
461
+ },
462
+ mdnsName: context.getParam({name: "mdnsName", type: "string", default: "duotecno-gateway"})
333
463
  });
334
464
  }
335
465
 
@@ -518,18 +648,22 @@ export class SmartApp extends WebApp {
518
648
  const hbValue = dt2hbvalue(u, link.hbValue as number, link.min, link.max);
519
649
 
520
650
  if (typeof hbValue != "undefined") {
521
- if (hbValue === -1) {
522
- log("smartapp", "-> Virtual unit send 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
523
- const val = await hbStop(this.config.apiHost +":"+ this.config.apiPort, link.accId, link.accService, this.token);
524
- if (typeof val != "undefined") link.hbValue = val;
525
-
526
- } else if (hbValue !== link.hbValue) {
527
- log("smartapp", "-> Setting HB value for accessory: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName() + " with value " + u.value);
528
- link.hbValue = hbValue;
529
- await hbSetState(this.config.apiHost +":"+ this.config.apiPort, link.accId, link.accService, hbValue, this.token);
530
-
531
- } else {
532
- debug("smartapp", "-> No change for accessory: " + link.accName + " to " + u.value + " / " + u.status);
651
+ try {
652
+ if (hbValue === -1) {
653
+ log("smartapp", "-> Virtual unit send 'stop' for accessory: " + link.accName + " of Duotecno unit: " + u.getDisplayName());
654
+ const val = await hbStop(this.config.apiHost +":"+ this.config.apiPort, link.accId, link.accService, this.token);
655
+ if (typeof val != "undefined") link.hbValue = val;
656
+
657
+ } else if (hbValue !== link.hbValue) {
658
+ log("smartapp", "-> Setting HB value for accessory: " + link.accName + " to " + hbValue + " of Duotecno unit: " + u.getDisplayName() + " with value " + u.value);
659
+ link.hbValue = hbValue;
660
+ await hbSetState(this.config.apiHost +":"+ this.config.apiPort, link.accId, link.accService, hbValue, this.token);
661
+
662
+ } else {
663
+ debug("smartapp", "-> No change for accessory: " + link.accName + " to " + u.value + " / " + u.status);
664
+ }
665
+ } catch(e) {
666
+ err("smartapp", "informLinks: failed to set HB value for accessory " + link.accName + " -> " + e.toString());
533
667
  }
534
668
  }
535
669
  }
@@ -629,6 +763,281 @@ export class SmartApp extends WebApp {
629
763
  }
630
764
 
631
765
 
766
+ /////////////
767
+ // Devices //
768
+ /////////////
769
+ initDeviceUnits() {
770
+ log("smartapp", "Init " + this.devices.length + " Devices -> add units");
771
+
772
+ this.devices.forEach(device => {
773
+ device.unit ||= this.system.findUnit(this.system.findMaster(device.masterAddress, device.masterPort),
774
+ device.logicalNodeAddress, device.logicalAddress);
775
+ if (device.unit) {
776
+ device.relay = device.unit.status > 0;
777
+ } else {
778
+ log("smartapp", "** error ** missing device unit: " + hex(device.logicalNodeAddress)+"/"+hex(device.logicalAddress) + " **");
779
+ }
780
+ });
781
+ }
782
+
783
+ async doDevices(context: Context): Promise<HttpResponse> {
784
+ let inx: number = parseInt(context.id);
785
+ const units = this.system.allUsedUnits();
786
+
787
+ let message: string;
788
+ try {
789
+ if (context.action === "add") {
790
+ this.devices.push({...kEmptyDevice, measured: false});
791
+ inx = this.devices.length-1;
792
+ return this.ejs("deviceDetail", context, { device: this.devices[inx], id: inx,
793
+ masters: this.system.masters, units });
794
+
795
+ } else if (context.action === "edit") {
796
+ return this.ejs("deviceDetail", context, { device: this.devices[inx], id: inx,
797
+ masters: this.system.masters, units });
798
+
799
+ } else if (context.action === "delete") {
800
+ this.deleteDevice(inx);
801
+
802
+ } else if (context.action === "change") {
803
+ this.updateDevice(inx, this.scrapeDevice(context));
804
+
805
+ } else {
806
+ // possible new IP Nodes, hence Units could be online
807
+ this.initDeviceUnits();
808
+ }
809
+
810
+ } catch(e) {
811
+ message = e.toString();
812
+ }
813
+ return this.ejs("deviceList", context, { masters: this.system.masters, devices: this.devices, message });
814
+ }
815
+
816
+ scrapeDevice(context: Context): Device {
817
+ const { masterAddress, masterPort, logicalAddress, logicalNodeAddress } = this.scrapeUnit(context, '');
818
+ const name = context.getParam({name: "name", type: "string", default: "Device" });
819
+ const powerSource = context.getParam({name: "powerSource", type: "string", default: "none" });
820
+ const powerChannel = context.getParam({name: "powerChannel", type: "string", default: "" });
821
+ const estimatedPower = context.getParam({name: "estimatedPower", type: "integer", default: 0 });
822
+
823
+ // Calculate ID from node/unit
824
+ const id = logicalNodeAddress * 256 + logicalAddress;
825
+
826
+ // Measured is true if powerSource is not "none"
827
+ const measured = powerSource !== "none";
828
+
829
+ return {
830
+ id,
831
+ name,
832
+ masterAddress,
833
+ masterPort,
834
+ logicalAddress,
835
+ logicalNodeAddress,
836
+ measured,
837
+ powerSource: powerSource as any,
838
+ powerChannel,
839
+ estimatedPower,
840
+ power: 0,
841
+ energy: 0,
842
+ voltage: 230.0,
843
+ current: 0,
844
+ relay: false
845
+ };
846
+ }
847
+
848
+ updateDevice(inx: number, device: Device) {
849
+ if ((inx >= 0) && (inx < this.devices.length)) {
850
+ this.devices[inx] = device;
851
+ this.initDeviceUnits();
852
+ this.writeConfig();
853
+ }
854
+ }
855
+
856
+ deleteDevice(inx: number) {
857
+ if ((inx >= 0) && (inx < this.devices.length)) {
858
+ this.devices.splice(inx, 1);
859
+ this.initDeviceUnits();
860
+ this.writeConfig();
861
+ }
862
+ }
863
+
864
+ //////////////////////
865
+ // Device - sockets //
866
+ //////////////////////
867
+ sendDeviceList(ws: WebSocket) {
868
+ const devices = this.getDeviceList();
869
+ if (ws.readyState === WebSocket.OPEN) {
870
+ ws.send(JSON.stringify(devices));
871
+ }
872
+ }
873
+
874
+ getDeviceList(): Array<any> {
875
+ return this.devices.map(device => {
876
+ // Get current relay state from unit if available
877
+ let relayState = device.relay || false;
878
+ if (device.unit) {
879
+ relayState = device.unit.status > 0;
880
+ }
881
+
882
+ // Get power measurements based on power source
883
+ let power = 0;
884
+ let energy = 0;
885
+ let voltage = 230.0;
886
+ let current = 0;
887
+ let measured = false;
888
+
889
+ if (device.powerSource === PowerSource.kSmappee && this.power.smappee) {
890
+ // Get power from Smappee
891
+ const smappeePower = this.getPowerFromSmappee(device.powerChannel);
892
+ if (smappeePower !== null) {
893
+ power = smappeePower;
894
+ measured = true;
895
+ current = power / voltage;
896
+ }
897
+ } else if (device.powerSource === PowerSource.kP1 && this.power.p1) {
898
+ // Get power from P1 meter
899
+ const p1Power = this.getPowerFromP1(device.powerChannel);
900
+ if (p1Power !== null) {
901
+ power = p1Power;
902
+ measured = true;
903
+ current = power / voltage;
904
+ }
905
+ } else if (device.powerSource === PowerSource.kShelly && this.power.shelly) {
906
+ // Get power from Shelly
907
+ const shellyPower = this.getPowerFromShelly(device.powerChannel);
908
+ if (shellyPower !== null) {
909
+ power = shellyPower;
910
+ measured = true;
911
+ current = power / voltage;
912
+ }
913
+ } else if (device.powerSource === PowerSource.kNone || !device.powerSource) {
914
+ // Use estimated power when relay is on
915
+ power = relayState ? (device.estimatedPower || 0) : 0;
916
+ measured = false;
917
+ }
918
+
919
+ return {
920
+ id: device.id,
921
+ name: device.name,
922
+ power: power,
923
+ energy: energy,
924
+ voltage: voltage,
925
+ current: current,
926
+ relay: relayState,
927
+ measured: measured
928
+ };
929
+ });
930
+ }
931
+
932
+ getPowerFromSmappee(channel: string): number | null {
933
+ if (!this.power.smappee) return null;
934
+ try {
935
+ // Channel format could be "0", "0+1", "0+1+2", etc.
936
+ const channels = channel.split('+').map(c => parseInt(c.trim()));
937
+ let totalPower = 0;
938
+ for (const ch of channels) {
939
+ const binding = this.power.smappee.bindings.find(b => b.channel.includes(ch.toString()));
940
+ if (binding && binding.value !== undefined) {
941
+ totalPower += binding.value;
942
+ }
943
+ }
944
+ return totalPower;
945
+ } catch (e) {
946
+ err("smartapp", `Error getting Smappee power for channel ${channel}: ${e.message}`);
947
+ return null;
948
+ }
949
+ }
950
+
951
+ getPowerFromP1(channel: string): number | null {
952
+ if (!this.power.p1) return null;
953
+ try {
954
+ // P1 meter typically has single power value
955
+ const binding = this.power.p1.bindings.find(b => b.channel.includes(channel));
956
+ if (binding && binding.value !== undefined) {
957
+ return binding.value;
958
+ }
959
+ return null;
960
+ } catch (e) {
961
+ err("smartapp", `Error getting P1 power for channel ${channel}: ${e.message}`);
962
+ return null;
963
+ }
964
+ }
965
+
966
+ getPowerFromShelly(channel: string): number | null {
967
+ if (!this.power.shelly) return null;
968
+ try {
969
+ // Shelly channel format similar to Smappee
970
+ const channels = channel.split('+').map(c => parseInt(c.trim()));
971
+ let totalPower = 0;
972
+ for (const ch of channels) {
973
+ const binding = this.power.shelly.bindings.find(b => b.channel.includes(ch.toString()));
974
+ if (binding && binding.value !== undefined) {
975
+ totalPower += binding.value;
976
+ }
977
+ }
978
+ return totalPower;
979
+ } catch (e) {
980
+ err("smartapp", `Error getting Shelly power for channel ${channel}: ${e.message}`);
981
+ return null;
982
+ }
983
+ }
984
+
985
+ async handleDeviceMessage(ws: WebSocket, data: any) {
986
+ if (!Array.isArray(data)) {
987
+ err("smartapp", "WebSocket message must be an array");
988
+ return;
989
+ }
990
+
991
+ // Empty array = keep-alive / poll
992
+ if (data.length === 0) {
993
+ this.sendDeviceList(ws);
994
+ return;
995
+ }
996
+
997
+ // Process relay control commands
998
+ for (const cmd of data) {
999
+ if (cmd.id && cmd.relay !== undefined) {
1000
+ await this.setDeviceRelay(cmd.id, cmd.relay);
1001
+ }
1002
+ }
1003
+
1004
+ // Send updated device list
1005
+ this.sendDeviceList(ws);
1006
+ }
1007
+
1008
+ async setDeviceRelay(id: number, state: boolean) {
1009
+ // Convert ID to node/unit (id = node * 256 + unit)
1010
+ const node = Math.floor(id / 256);
1011
+ const unit = id % 256;
1012
+
1013
+ log("smartapp", `Setting device ${id} (node=${node}, unit=${unit}) to ${state}`);
1014
+
1015
+ // Get the first master
1016
+ const master = this.system.masters[0];
1017
+ if (!master) {
1018
+ err("smartapp", "No master available");
1019
+ return;
1020
+ }
1021
+
1022
+ // Find the unit
1023
+ const nodeObj = master.nodes.find(n => n.logicalAddress === node);
1024
+ if (!nodeObj) {
1025
+ err("smartapp", `Node ${node} not found`);
1026
+ return;
1027
+ }
1028
+
1029
+ const unitObj = nodeObj.units.find(u => u.logicalAddress === unit);
1030
+ if (!unitObj) {
1031
+ err("smartapp", `Unit ${unit} in node ${node} not found`);
1032
+ return;
1033
+ }
1034
+
1035
+ // Set the unit state (convert boolean to 0/100 for dimmer/switch)
1036
+ await master.setUnitStatus(unitObj, state ? 100 : 0);
1037
+ }
1038
+
1039
+
1040
+
632
1041
  // not used / tested for the moment
633
1042
  async setLink(inx: number, newstate?: boolean, newvalue?: number)
634
1043
  async setLink(inx: Link, newstate?: boolean, newvalue?: number)
@@ -1268,6 +1677,9 @@ export class SmartApp extends WebApp {
1268
1677
  } else if (file === "favicon.ico") {
1269
1678
  return this.file("favicon");
1270
1679
 
1680
+ } else if (file === "Duotecno_logo_white.svg") {
1681
+ return this.file("logoWhite");
1682
+
1271
1683
  } else
1272
1684
  return this.notFound();
1273
1685
  }
package/server/socapp.js CHANGED
@@ -66,7 +66,8 @@ class SocApp extends webapp_1.WebApp {
66
66
  serve() {
67
67
  (0, logger_1.log)("socapp", "SocApp - Start http server on port " + this.port);
68
68
  super.serve(() => {
69
- const wsServer = new ws_1.Server({ server: this.server });
69
+ // Create WebSocket server on the first HTTP server
70
+ const wsServer = new ws_1.Server({ server: this.servers[0] });
70
71
  wsServer.on('connection', this.handleClient.bind(this));
71
72
  });
72
73
  }
package/server/socapp.ts CHANGED
@@ -63,7 +63,8 @@ export class SocApp extends WebApp {
63
63
  serve() {
64
64
  log("socapp", "SocApp - Start http server on port " + this.port);
65
65
  super.serve(() => {
66
- const wsServer = new WSServer({server: this.server});
66
+ // Create WebSocket server on the first HTTP server
67
+ const wsServer = new WSServer({server: this.servers[0]});
67
68
  wsServer.on('connection', this.handleClient.bind(this));
68
69
  });
69
70
  }
@@ -0,0 +1,10 @@
1
+ <svg width="1550" height="1599" viewBox="0 0 1550 1599" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M54.6003 5.06515C49.9542 9.71127 46.4696 243.179 46.4696 523.107C46.4696 1023.73 46.4696 1031.86 23.239 1031.86C-6.96081 1031.86 -8.12233 1078.32 22.0774 1086.45C41.8235 1091.1 44.1465 1107.36 48.7926 1279.26C54.6003 1515.05 73.1847 1558.03 177.722 1582.42C206.761 1589.39 209.084 1587.07 209.084 1548.74C209.084 1513.89 205.599 1508.08 181.207 1508.08C163.784 1508.08 142.877 1495.31 128.938 1477.89C106.869 1450.01 104.546 1431.42 104.546 1268.81V1089.93H156.815C204.438 1089.93 209.084 1087.61 209.084 1060.9C209.084 1034.18 204.438 1031.86 156.815 1031.86H104.546L102.223 518.461C98.7384 65.4647 96.4154 3.90362 81.3155 0.419028C70.8617 -0.742501 59.2464 0.419028 54.6003 5.06515Z" fill="black"/>
3
+ <path d="M778.233 216.463V340.747L721.318 361.655C649.303 387.208 586.581 456.9 563.35 537.046C541.281 609.061 541.281 641.583 562.189 711.275C593.55 816.974 679.503 890.151 775.91 895.958L818.887 898.281L822.371 494.069C824.694 96.8259 824.694 91.0182 801.464 91.0182C779.395 91.0182 778.233 97.9874 778.233 216.463ZM778.233 626.483V835.559L752.68 827.428C670.211 803.036 627.234 756.575 604.004 669.46C590.065 618.353 604.004 541.692 636.527 498.715C664.403 460.385 729.449 416.247 756.164 416.247C777.072 416.247 778.233 429.023 778.233 626.483Z" fill="black"/>
4
+ <path d="M1324.15 358.171C1246.33 441.801 1221.94 659.007 1277.69 785.614C1346.22 942.421 1461.21 938.936 1522.77 777.483C1554.14 695.015 1557.62 553.308 1529.74 462.709C1491.41 332.617 1393.84 283.833 1324.15 358.171ZM1450.76 423.217C1487.93 470.84 1501.87 532.401 1496.06 632.292C1489.09 776.322 1439.14 854.144 1374.1 823.945C1284.66 783.291 1269.56 509.17 1350.87 418.571C1382.23 384.886 1421.72 386.048 1450.76 423.217Z" fill="black"/>
5
+ <path d="M917.617 619.516C917.617 884.344 918.778 892.475 940.848 892.475C958.27 892.475 964.078 884.344 964.078 861.114V829.753L996.601 859.952C1063.97 924.998 1138.31 908.737 1160.38 826.268C1167.35 801.876 1173.15 684.561 1173.15 563.762C1173.15 354.687 1171.99 346.556 1149.92 346.556C1127.85 346.556 1126.69 354.687 1126.69 560.278C1126.69 678.754 1122.05 787.938 1115.08 804.199C1101.14 842.529 1062.81 843.691 1009.38 807.684L969.886 780.968L966.401 563.762C964.078 355.848 962.917 346.556 940.848 346.556C918.778 346.556 917.617 355.848 917.617 619.516Z" fill="black"/>
6
+ <path d="M343.821 1048.12C277.614 1117.81 250.899 1265.33 283.422 1391.93C313.621 1509.25 372.859 1570.81 464.62 1580.1C502.951 1583.58 505.274 1581.26 508.758 1545.25C512.243 1510.41 509.92 1508.09 476.236 1508.09C396.09 1508.09 336.852 1438.39 320.591 1324.56L313.621 1275.78H429.774H545.927V1237.45C545.927 1186.34 515.728 1089.93 490.174 1059.73C439.067 999.335 392.605 994.689 343.821 1048.12ZM448.359 1098.07C468.105 1117.81 499.466 1184.02 499.466 1206.09C499.466 1213.06 464.62 1217.7 412.351 1217.7C318.267 1217.7 313.621 1214.22 338.013 1151.5C364.729 1081.8 408.867 1059.73 448.359 1098.07Z" fill="black"/>
7
+ <path d="M1370.61 1016.76C1282.34 1049.28 1225.42 1230.48 1253.3 1387.29C1270.72 1486.02 1305.57 1552.22 1357.84 1582.42C1397.33 1604.49 1397.33 1604.49 1439.14 1582.42C1582.01 1504.6 1588.98 1116.65 1447.27 1030.7C1410.11 1007.47 1399.65 1006.3 1370.61 1016.76ZM1442.63 1107.36C1483.28 1148.01 1501.87 1223.51 1496.06 1330.37C1491.41 1433.75 1465.86 1497.63 1421.72 1518.54C1353.19 1548.74 1298.6 1454.65 1298.6 1307.14C1298.6 1204.93 1316.02 1144.53 1357.84 1102.71C1389.2 1071.35 1407.78 1072.51 1442.63 1107.36Z" fill="black"/>
8
+ <path d="M752.68 1031.86C651.627 1062.06 587.743 1167.76 587.743 1304.82C587.743 1454.65 667.888 1569.65 778.234 1580.1C816.564 1583.58 818.887 1581.26 822.372 1545.25C825.856 1511.57 823.533 1508.09 793.334 1508.09C708.542 1508.09 638.85 1417.49 638.85 1304.82C638.85 1195.63 691.119 1121.3 781.718 1102.71C813.08 1095.74 818.887 1089.93 818.887 1060.9C818.887 1022.57 803.787 1015.6 752.68 1031.86Z" fill="black"/>
9
+ <path d="M1045.39 1033.02C1029.12 1039.99 1004.73 1056.25 990.793 1069.03L964.078 1093.42V1063.22C964.078 1039.99 958.27 1031.86 940.848 1031.86C918.778 1031.86 917.617 1039.99 917.617 1304.82C917.617 1569.65 918.778 1577.78 940.848 1577.78C962.917 1577.78 964.078 1569.65 964.078 1368.7C964.078 1161.95 964.078 1158.46 991.955 1130.59C1034.93 1087.61 1079.07 1080.64 1105.78 1113.16C1124.37 1136.39 1126.69 1163.11 1126.69 1358.25C1126.69 1569.65 1127.85 1577.78 1149.92 1577.78C1171.99 1577.78 1173.15 1569.65 1173.15 1359.41C1173.15 1145.69 1163.86 1069.03 1134.82 1045.8C1110.43 1026.05 1075.59 1021.4 1045.39 1033.02Z" fill="black"/>
10
+ </svg>
@@ -0,0 +1,10 @@
1
+ <svg width="1550" height="1599" viewBox="0 0 1550 1599" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M54.6003 5.06515C49.9542 9.71127 46.4696 243.179 46.4696 523.107C46.4696 1023.73 46.4696 1031.86 23.239 1031.86C-6.96081 1031.86 -8.12233 1078.32 22.0774 1086.45C41.8235 1091.1 44.1465 1107.36 48.7926 1279.26C54.6003 1515.05 73.1847 1558.03 177.722 1582.42C206.761 1589.39 209.084 1587.07 209.084 1548.74C209.084 1513.89 205.599 1508.08 181.207 1508.08C163.784 1508.08 142.877 1495.31 128.938 1477.89C106.869 1450.01 104.546 1431.42 104.546 1268.81V1089.93H156.815C204.438 1089.93 209.084 1087.61 209.084 1060.9C209.084 1034.18 204.438 1031.86 156.815 1031.86H104.546L102.223 518.461C98.7384 65.4647 96.4154 3.90362 81.3155 0.419028C70.8617 -0.742501 59.2464 0.419028 54.6003 5.06515Z" fill="white"/>
3
+ <path d="M778.233 216.463V340.747L721.318 361.655C649.304 387.208 586.581 456.9 563.35 537.046C541.281 609.061 541.281 641.583 562.189 711.275C593.55 816.974 679.503 890.151 775.91 895.958L818.887 898.281L822.371 494.069C824.695 96.8259 824.695 91.0182 801.464 91.0182C779.395 91.0182 778.233 97.9874 778.233 216.463ZM778.233 626.483V835.559L752.68 827.428C670.211 803.036 627.234 756.575 604.004 669.46C590.066 618.353 604.004 541.692 636.527 498.715C664.403 460.385 729.449 416.247 756.164 416.247C777.072 416.247 778.233 429.023 778.233 626.483Z" fill="white"/>
4
+ <path d="M1324.15 358.171C1246.33 441.801 1221.94 659.007 1277.69 785.614C1346.22 942.421 1461.21 938.936 1522.77 777.483C1554.14 695.015 1557.62 553.308 1529.74 462.709C1491.41 332.617 1393.84 283.833 1324.15 358.171ZM1450.76 423.217C1487.93 470.84 1501.87 532.401 1496.06 632.292C1489.09 776.322 1439.14 854.144 1374.1 823.945C1284.66 783.291 1269.56 509.17 1350.87 418.571C1382.23 384.886 1421.72 386.048 1450.76 423.217Z" fill="white"/>
5
+ <path d="M917.617 619.516C917.617 884.344 918.778 892.475 940.848 892.475C958.27 892.475 964.078 884.344 964.078 861.114V829.753L996.601 859.952C1063.97 924.998 1138.31 908.737 1160.38 826.268C1167.35 801.876 1173.15 684.561 1173.15 563.762C1173.15 354.687 1171.99 346.556 1149.92 346.556C1127.85 346.556 1126.69 354.687 1126.69 560.278C1126.69 678.754 1122.05 787.938 1115.08 804.199C1101.14 842.529 1062.81 843.691 1009.38 807.684L969.886 780.968L966.401 563.762C964.078 355.848 962.917 346.556 940.848 346.556C918.778 346.556 917.617 355.848 917.617 619.516Z" fill="white"/>
6
+ <path d="M343.821 1048.12C277.614 1117.81 250.899 1265.33 283.422 1391.93C313.621 1509.25 372.859 1570.81 464.62 1580.1C502.951 1583.58 505.274 1581.26 508.758 1545.25C512.243 1510.41 509.92 1508.09 476.236 1508.09C396.09 1508.09 336.852 1438.39 320.591 1324.56L313.621 1275.78H429.774H545.927V1237.45C545.927 1186.34 515.728 1089.93 490.174 1059.73C439.067 999.335 392.605 994.689 343.821 1048.12ZM448.359 1098.07C468.105 1117.81 499.466 1184.02 499.466 1206.09C499.466 1213.06 464.62 1217.7 412.351 1217.7C318.267 1217.7 313.621 1214.22 338.013 1151.5C364.729 1081.8 408.867 1059.73 448.359 1098.07Z" fill="white"/>
7
+ <path d="M1370.61 1016.76C1282.34 1049.28 1225.42 1230.48 1253.3 1387.29C1270.72 1486.02 1305.57 1552.22 1357.84 1582.42C1397.33 1604.49 1397.33 1604.49 1439.14 1582.42C1582.01 1504.6 1588.98 1116.65 1447.27 1030.7C1410.11 1007.47 1399.65 1006.3 1370.61 1016.76ZM1442.63 1107.36C1483.28 1148.01 1501.87 1223.51 1496.06 1330.37C1491.41 1433.75 1465.86 1497.63 1421.72 1518.54C1353.19 1548.74 1298.6 1454.65 1298.6 1307.14C1298.6 1204.93 1316.02 1144.53 1357.84 1102.71C1389.2 1071.35 1407.78 1072.51 1442.63 1107.36Z" fill="white"/>
8
+ <path d="M752.68 1031.86C651.627 1062.06 587.743 1167.76 587.743 1304.82C587.743 1454.65 667.888 1569.65 778.234 1580.1C816.564 1583.58 818.887 1581.26 822.372 1545.25C825.856 1511.57 823.533 1508.09 793.333 1508.09C708.542 1508.09 638.85 1417.49 638.85 1304.82C638.85 1195.63 691.119 1121.3 781.718 1102.71C813.079 1095.74 818.887 1089.93 818.887 1060.9C818.887 1022.57 803.787 1015.6 752.68 1031.86Z" fill="white"/>
9
+ <path d="M1045.39 1033.02C1029.12 1039.99 1004.73 1056.25 990.793 1069.03L964.078 1093.42V1063.22C964.078 1039.99 958.27 1031.86 940.848 1031.86C918.778 1031.86 917.617 1039.99 917.617 1304.82C917.617 1569.65 918.778 1577.78 940.848 1577.78C962.917 1577.78 964.078 1569.65 964.078 1368.7C964.078 1161.95 964.078 1158.46 991.955 1130.59C1034.93 1087.61 1079.07 1080.64 1105.78 1113.16C1124.37 1136.39 1126.69 1163.11 1126.69 1358.25C1126.69 1569.65 1127.85 1577.78 1149.92 1577.78C1171.99 1577.78 1173.15 1569.65 1173.15 1359.41C1173.15 1145.69 1163.86 1069.03 1134.82 1045.8C1110.43 1026.05 1075.59 1021.4 1045.39 1033.02Z" fill="white"/>
10
+ </svg>