homebridge-smartsystem 7.0.1 → 7.0.4

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/README.md CHANGED
@@ -319,6 +319,7 @@ Homebridge UI version
319
319
 
320
320
  ### v7.0.0 - 12/11/2025
321
321
  - 0: added mDNS/Bonjour + always port 80 + don't preload EJS files if system.debug == true
322
+ - 4: fixed a bug when shelly has empty list of ip addresses
322
323
 
323
324
  ## Hardware
324
325
  A Raspberry Pi that connects to a Duotecno IP Node
@@ -630,4 +631,57 @@ Accessory.Categories = {
630
631
  SHOWER_HEAD: 30,
631
632
  TELEVISION: 31,
632
633
  TARGET_CONTROLLER: 32
633
- };
634
+ };
635
+
636
+
637
+ Small example of config.
638
+
639
+ {
640
+ "manufacturer": "Duotecno-Coppieters",
641
+ "platform": "DuotecnoPlatform",
642
+ "debug": false,
643
+ "smappee": {
644
+ "rules": [],
645
+ "address": "",
646
+ "uid": "57e3e0d8-bb05-4b04-8662-1a9871998f3f",
647
+ "debug": false,
648
+ "bindings": []
649
+ },
650
+ "smartapp": {
651
+ "port": 5002,
652
+ "switches": [],
653
+ "debug": true,
654
+ "links": [],
655
+ "apiHost": "localhost",
656
+ "apiPort": 8581,
657
+ "username": "duotecno",
658
+ "password": "#Duotecno@1",
659
+ "devices": []
660
+ },
661
+ "system": {
662
+ "language": "EN",
663
+ "stores": false,
664
+ "multiple": false,
665
+ "socketserver": "gateway.duotecno.com",
666
+ "socketport": 9999,
667
+ "mood": "sfeer",
668
+ "cmasters": [],
669
+ "cunits": [],
670
+ "debug": false
671
+ },
672
+ "socapp": false,
673
+ "p1": {
674
+ "debug": false,
675
+ "rules": [],
676
+ "bindings": [],
677
+ "address": ""
678
+ },
679
+ "shelly": {
680
+ "debug": false,
681
+ "address": "",
682
+ "addresses": "",
683
+ "rules": [],
684
+ "bindings": []
685
+ },
686
+ "proxy": false
687
+ }
package/config.json CHANGED
@@ -1,509 +1,49 @@
1
1
  {
2
- "manufacturer": "Duotecno-Coppieterscc",
3
- "platform": "DuotecnoPlatform",
4
- "smappee": {
2
+ "manufacturer": "Duotecno-Coppieters",
3
+ "platform": "DuotecnoPlatform",
5
4
  "debug": false,
6
- "rules": [
7
- {
8
- "type": "sun",
9
- "channel": "0",
10
- "low": 30,
11
- "high": 3000,
12
- "actions": [
13
- {
14
- "name": "Lamp-Bottom",
15
- "value": false,
16
- "masterAddress": "192.168.0.97",
17
- "masterPort": 5001,
18
- "logicalAddress": 34,
19
- "logicalNodeAddress": 3
20
- },
21
- {
22
- "name": "Lamp-Bottom",
23
- "value": false,
24
- "masterAddress": "192.168.0.97",
25
- "masterPort": 5001,
26
- "logicalAddress": 34,
27
- "logicalNodeAddress": 3
28
- },
29
- {
30
- "name": "Lamp-Bottom",
31
- "value": true,
32
- "masterAddress": "192.168.0.97",
33
- "masterPort": 5001,
34
- "logicalAddress": 34,
35
- "logicalNodeAddress": 3
36
- }
37
- ],
38
- "power": -758,
39
- "current": 0
40
- },
41
- {
42
- "type": "power",
43
- "channel": "7+8+9",
44
- "low": 500,
45
- "high": 10000,
46
- "actions": [
47
- {
48
- "name": "Blue LED",
49
- "value": false,
50
- "masterAddress": "192.168.0.97",
51
- "masterPort": 5001,
52
- "logicalAddress": 30,
53
- "logicalNodeAddress": 3
54
- },
55
- {
56
- "name": "Blue LED",
57
- "value": true,
58
- "masterAddress": "192.168.0.97",
59
- "masterPort": 5001,
60
- "logicalAddress": 30,
61
- "logicalNodeAddress": 3
62
- },
63
- {
64
- "name": "--",
65
- "value": true,
66
- "masterAddress": "192.168.0.97",
67
- "masterPort": 5001,
68
- "logicalAddress": 0,
69
- "logicalNodeAddress": 0
70
- }
71
- ]
72
- },
73
- {
74
- "type": "power",
75
- "channel": "7+8+9",
76
- "low": 400,
77
- "high": 900,
78
- "actions": [
79
- {
80
- "name": "Lamp-Top",
81
- "value": false,
82
- "masterAddress": "192.168.0.97",
83
- "masterPort": 5001,
84
- "logicalAddress": 33,
85
- "logicalNodeAddress": 3
86
- },
87
- {
88
- "name": "Lamp-Top",
89
- "value": true,
90
- "masterAddress": "192.168.0.97",
91
- "masterPort": 5001,
92
- "logicalAddress": 33,
93
- "logicalNodeAddress": 3
94
- },
95
- {
96
- "name": "--",
97
- "value": true,
98
- "masterAddress": "192.168.0.97",
99
- "masterPort": 5001,
100
- "logicalAddress": 0,
101
- "logicalNodeAddress": 0
102
- }
103
- ],
104
- "power": 266,
105
- "current": 0
106
- }
107
- ],
108
- "address": "192.168.0.183",
109
- "uid": "57e3e0d8-bb05-4b04-8662-1a9871998f3f",
110
- "bindings": [
111
- {
112
- "channel": "0+1",
113
- "masterAddress": "192.168.0.97",
114
- "masterPort": 5001,
115
- "register": 1
116
- },
117
- {
118
- "channel": "2+3",
119
- "masterAddress": "192.168.0.97",
120
- "masterPort": 5001,
121
- "register": 2
122
- },
123
- {
124
- "channel": "4+5",
125
- "masterAddress": "192.168.0.97",
126
- "masterPort": 5001,
127
- "register": 7
128
- },
129
- {
130
- "channel": "8",
131
- "masterAddress": "192.168.0.97",
132
- "masterPort": 5001,
133
- "register": 8
134
- }
135
- ]
136
- },
137
- "p11": {
138
- "debug": false,
139
- "rules": [],
140
- "bindings": [
141
- {
142
- "channel": "0+1+2",
143
- "masterAddress": "192.168.0.97",
144
- "masterPort": 5001,
145
- "register": 11
146
- },
147
- {
148
- "channel": "3+4+5",
149
- "masterAddress": "192.168.0.97",
150
- "masterPort": 5001,
151
- "register": 12
152
- }
153
- ],
154
- "address": "192.168.0.206"
155
- },
156
- "shelly": {
157
- "debug": false,
158
- "address": "192.168.0.95",
159
- "addresses": "192.168.0.95,192.168.0.95",
160
- "rules": [],
161
- "bindings": [
162
- {
163
- "channel": "0+1+2+3+4+5+6+7+8",
164
- "masterAddress": "192.168.0.97",
165
- "masterPort": 5001,
166
- "register": 5
167
- }
168
- ]
169
- },
170
- "xsocapp": {
171
- "debug": true,
172
- "port": 9999
173
- },
174
- "smartapp": {
175
- "apiPort": 8581,
176
- "apiHost": "192.168.0.99",
177
- "port": 5002,
178
- "switches": [
179
- {
180
- "logicalNodeAddress": 252,
181
- "logicalAddress": 107,
182
- "masterAddress": "192.168.0.97",
183
- "masterPort": 5001,
184
- "name": "Shelly Valies",
185
- "displayName": "Shelly Valies",
186
- "extendedType": 0,
187
- "nomacro": "Y",
188
- "nostop": "Y",
189
- "unitName": "Red",
190
- "value": 0,
191
- "data": "",
192
- "header": "",
193
- "method": "GET",
194
- "type": "httpdim",
195
- "plug": "http://192.168.0.96/light/0?brightness=$&turn=#"
196
- },
197
- {
198
- "logicalNodeAddress": 16,
199
- "logicalAddress": 35,
200
- "masterAddress": "gm.coppieters.be",
201
- "masterPort": 5005,
202
- "name": "Test Plug",
203
- "displayName": "Test Plug",
204
- "extendedType": 0,
205
- "nomacro": "N",
206
- "nostop": "N",
207
- "unitName": "Plug",
208
- "value": 0,
209
- "data": "",
210
- "header": "",
211
- "method": "GET",
212
- "type": "smappee",
213
- "plug": "1"
214
- },
215
- {
216
- "logicalNodeAddress": 3,
217
- "logicalAddress": 34,
218
- "masterAddress": "192.168.0.97",
219
- "masterPort": 5001,
220
- "name": "Notification Sender",
221
- "displayName": "Notification Sender",
222
- "extendedType": 0,
223
- "nomacro": "N",
224
- "nostop": "N",
225
- "unitName": "Lamp-Bottom",
226
- "value": 0,
227
- "data": "",
228
- "header": "",
229
- "method": "GET",
230
- "type": "http",
231
- "plug": "https://duotecno.eu/nl/notification/send?title=Lamp+changed&body=Turned+#&proof=MwJDIgEUOgYNFQsmHEMjC15yXllXXmZVKgw6ASkBBw=="
232
- }
233
- ],
234
- "debug": false,
235
- "username": "pi",
236
- "password": "GMTecno",
237
- "links": [],
238
- "devices": [
239
- {
240
- "logicalNodeAddress": 3,
241
- "logicalAddress": 34,
242
- "masterAddress": "192.168.0.97",
243
- "masterPort": 5001,
244
- "name": "Test Lamp",
245
- "displayName": "Test Lamp",
246
- "extendedType": 0,
247
- "id": 802,
248
- "power": 0,
249
- "energy": 0,
250
- "voltage": 230,
251
- "current": 0,
252
- "relay": false,
253
- "measured": true,
254
- "powerSource": "shelly",
255
- "powerChannel": "1+2",
256
- "estimatedPower": 100
257
- },
258
- {
259
- "logicalNodeAddress": 3,
260
- "logicalAddress": 30,
261
- "masterAddress": "192.168.0.97",
262
- "masterPort": 5001,
263
- "name": "Blue LED",
264
- "displayName": "Blue LED",
265
- "extendedType": 0,
266
- "id": 798,
267
- "power": 0,
268
- "energy": 0,
269
- "voltage": 230,
270
- "current": 0,
271
- "relay": false,
272
- "measured": false,
273
- "powerSource": "none",
274
- "powerChannel": "",
275
- "estimatedPower": 50
276
- }
277
- ]
278
- },
279
- "system": {
280
- "language": "EN",
281
- "stores": false,
282
- "multiple": false,
283
- "socketserver": "ws.duotecno.eu",
284
- "socketport": 9999,
285
- "mood": "sfeer",
286
- "debug": false,
287
- "cmasters": [
288
- {
289
- "address": "192.168.0.97",
290
- "port": 5001,
291
- "password": "GMTecno",
292
- "name": "Valies",
293
- "active": true,
294
- "nodenames": {
295
- "3": "SB 1",
296
- "4": "oled",
297
- "252": " 20211003"
298
- },
5
+ "smappee": {
6
+ "rules": [],
7
+ "address": "",
8
+ "uid": "57e3e0d8-bb05-4b04-8662-1a9871998f3f",
9
+ "debug": false,
10
+ "bindings": []
11
+ },
12
+ "smartapp": {
13
+ "port": 5002,
14
+ "switches": [],
15
+ "debug": true,
16
+ "links": [],
17
+ "apiHost": "localhost",
18
+ "apiPort": 8581,
19
+ "username": "duotecno",
20
+ "password": "#Duotecno@1",
21
+ "devices": []
22
+ },
23
+ "system": {
24
+ "language": "EN",
25
+ "stores": false,
26
+ "multiple": false,
27
+ "socketserver": "gateway.duotecno.com",
28
+ "socketport": 9999,
29
+ "mood": "sfeer",
30
+ "cmasters": [],
31
+ "cunits": [],
299
32
  "debug": false
300
- }
301
- ],
302
- "cunits": [
303
- {
304
- "logicalNodeAddress": 252,
305
- "logicalAddress": 102,
306
- "masterAddress": "192.168.0.97",
307
- "masterPort": 5001,
308
- "name": "<Solar Panels",
309
- "displayName": "<Solar Panels",
310
- "extendedType": 0,
311
- "active": "N",
312
- "used": "Y",
313
- "group": 0
314
- },
315
- {
316
- "logicalNodeAddress": 252,
317
- "logicalAddress": 103,
318
- "masterAddress": "192.168.0.97",
319
- "masterPort": 5001,
320
- "name": ">Cars",
321
- "displayName": ">Cars",
322
- "extendedType": 0,
323
- "active": "N",
324
- "used": "Y",
325
- "group": 0
326
- },
327
- {
328
- "logicalNodeAddress": 252,
329
- "logicalAddress": 100,
330
- "masterAddress": "192.168.0.97",
331
- "masterPort": 5001,
332
- "name": ">Grid",
333
- "displayName": ">Grid",
334
- "extendedType": 0,
335
- "active": "N",
336
- "used": "Y",
337
- "group": 0
338
- },
339
- {
340
- "logicalNodeAddress": 252,
341
- "logicalAddress": 101,
342
- "masterAddress": "192.168.0.97",
343
- "masterPort": 5001,
344
- "name": ">Swimmingpool",
345
- "displayName": ">Swimmingpool",
346
- "extendedType": 0,
347
- "active": "N",
348
- "used": "Y",
349
- "group": 0
350
- },
351
- {
352
- "logicalNodeAddress": 252,
353
- "logicalAddress": 104,
354
- "masterAddress": "192.168.0.97",
355
- "masterPort": 5001,
356
- "name": "Tim Small",
357
- "displayName": "Tim Small",
358
- "extendedType": 8,
359
- "active": "Y",
360
- "used": "Y",
361
- "group": 0
362
- },
363
- {
364
- "logicalNodeAddress": 252,
365
- "logicalAddress": 7,
366
- "masterAddress": "192.168.0.97",
367
- "masterPort": 5001,
368
- "name": "Bottom-Left",
369
- "displayName": "Bottom-Left",
370
- "extendedType": 102,
371
- "active": "Y",
372
- "used": "Y",
373
- "group": 0
374
- },
375
- {
376
- "logicalNodeAddress": 3,
377
- "logicalAddress": 28,
378
- "masterAddress": "192.168.0.97",
379
- "masterPort": 5001,
380
- "name": "Garage $",
381
- "displayName": "Garage ",
382
- "extendedType": 8,
383
- "active": "Y",
384
- "used": "Y",
385
- "group": 0
386
- },
387
- {
388
- "logicalNodeAddress": 3,
389
- "logicalAddress": 31,
390
- "masterAddress": "192.168.0.97",
391
- "masterPort": 5001,
392
- "name": "2|Dimmer",
393
- "displayName": "Somfy Dimmer",
394
- "extendedType": 1,
395
- "active": "Y",
396
- "used": "Y",
397
- "group": 0
398
- },
399
- {
400
- "logicalNodeAddress": 3,
401
- "logicalAddress": 30,
402
- "masterAddress": "192.168.0.97",
403
- "masterPort": 5001,
404
- "name": "Blue LED",
405
- "displayName": "Blue LED",
406
- "extendedType": 1,
407
- "active": "N",
408
- "used": "Y",
409
- "group": 0
410
- },
411
- {
412
- "logicalNodeAddress": 3,
413
- "logicalAddress": 29,
414
- "masterAddress": "192.168.0.97",
415
- "masterPort": 5001,
416
- "name": "Red LED",
417
- "displayName": "Red LED",
418
- "extendedType": 1,
419
- "active": "N",
420
- "used": "Y",
421
- "group": 0
422
- },
423
- {
424
- "logicalNodeAddress": 3,
425
- "logicalAddress": 34,
426
- "masterAddress": "192.168.0.97",
427
- "masterPort": 5001,
428
- "name": "Lamp-Bottom",
429
- "displayName": "Lamp-Bottom",
430
- "extendedType": 2,
431
- "active": "N",
432
- "used": "Y",
433
- "group": 0
434
- },
435
- {
436
- "logicalNodeAddress": 3,
437
- "logicalAddress": 33,
438
- "masterAddress": "192.168.0.97",
439
- "masterPort": 5001,
440
- "name": "Lamp-Top",
441
- "displayName": "Lamp-Top",
442
- "extendedType": 2,
443
- "active": "N",
444
- "used": "Y",
445
- "group": 0
446
- },
447
- {
448
- "logicalNodeAddress": 3,
449
- "logicalAddress": 4,
450
- "masterAddress": "192.168.0.97",
451
- "masterPort": 5001,
452
- "name": "All off",
453
- "displayName": "Alles uitje",
454
- "extendedType": 7,
455
- "active": "Y",
456
- "used": "Y",
457
- "group": 0
458
- },
459
- {
460
- "logicalNodeAddress": 3,
461
- "logicalAddress": 5,
462
- "masterAddress": "192.168.0.97",
463
- "masterPort": 5001,
464
- "name": "All on",
465
- "displayName": "Alles aantje",
466
- "extendedType": 7,
467
- "active": "Y",
468
- "used": "Y",
469
- "group": 0
470
- }
471
- ]
472
- },
473
- "scenes": [
474
- {
475
- "name": "Scene",
476
- "trigger": {
477
- "masterAddress": "0.0.0.0",
478
- "masterPort": 5001,
479
- "name": "unit",
480
- "logicalAddress": 0,
481
- "logicalNodeAddress": 0,
482
- "value": true
483
- },
484
- "order": 0,
485
- "units": []
486
- }
487
- ],
488
- "debug": true,
489
- "settings": {
490
- "network": {
491
- "ip1": "1.2.3.4",
492
- "gateway1": "1.2.3.1",
493
- "nameservers": "8.8.8.8",
494
- "ip2": "",
495
- "gateway2": ""
496
33
  },
497
- "mdnsName": "duotecno-gateway"
498
- },
499
- "p1": false,
500
- "proxy": {
501
- "cloudServer": "ws.duotecno.eu",
502
- "cloudPort": 5097,
503
- "masterAddress": "192.168.0.97",
504
- "masterPort": 5001,
505
- "uniqueId": "",
506
- "debug": false,
507
- "kind": "solo"
508
- }
34
+ "socapp": false,
35
+ "p1": {
36
+ "debug": false,
37
+ "rules": [],
38
+ "bindings": [],
39
+ "address": ""
40
+ },
41
+ "shelly": {
42
+ "debug": false,
43
+ "address": "",
44
+ "addresses": "",
45
+ "rules": [],
46
+ "bindings": []
47
+ },
48
+ "proxy": false
509
49
  }
@@ -576,23 +576,23 @@ export const Protocol = {
576
576
  .join("");
577
577
  },
578
578
 
579
- makeWord: function(arr: Array<number>, at: number) {
579
+ makeWord: function(arr: Array<number>, at: number): number {
580
580
  return arr[at+0] * 256 + arr[at+1];
581
581
  },
582
- makeSignedWord: function(arr: Array<number>, at: number) {
582
+ makeSignedWord: function(arr: Array<number>, at: number): number {
583
583
  if (arr[at+0] > 127)
584
584
  return (arr[at+0]-255) * 256 +
585
585
  (arr[at+1]-255) - 1;
586
586
  else
587
587
  return arr[at+0] * 256 + arr[at+1];
588
588
  },
589
- makeLong: function(arr: Array<number>, at: number) {
589
+ makeLong: function(arr: Array<number>, at: number): number {
590
590
  return arr[at+0] * 256 * 256 * 256 +
591
591
  arr[at+1] * 256 * 256 +
592
592
  arr[at+2] * 256 +
593
593
  arr[at+3];
594
594
  },
595
- makeSigned: function(arr: Array<number>, at: number) {
595
+ makeSigned: function(arr: Array<number>, at: number): number {
596
596
  if (arr[at+0] > 127)
597
597
  return (arr[at+0]-255) * 256 * 256 * 256 +
598
598
  (arr[at+1]-255) * 256 * 256 +
@@ -604,7 +604,7 @@ export const Protocol = {
604
604
  arr[at+2] * 256 +
605
605
  arr[at+3];
606
606
  },
607
- signedToArray: function(value: number) {
607
+ signedToArray: function(value: number): Array<number> {
608
608
  if (value < 0)
609
609
  return [256 + Math.floor(value / 256 / 256 / 256),
610
610
  256 + Math.floor(value / 256 / 256) % 256,
@@ -616,13 +616,13 @@ export const Protocol = {
616
616
  Math.floor(value / 256) % 256,
617
617
  Math.floor(value) % 256];
618
618
  },
619
- signedWordToArray: function(value: number) {
619
+ signedWordToArray: function(value: number): Array<number> {
620
620
  if (value < 0)
621
621
  return [256 + Math.floor(value / 256) % 256, 256 + Math.floor(value) % 256];
622
622
  else
623
623
  return [Math.floor(value / 256) % 256, Math.floor(value) % 256];
624
624
  },
625
- wordToArray: function(value: number) {
625
+ wordToArray: function(value: number): Array<number> {
626
626
  return [Math.floor(value / 256) % 256, Math.floor(value) % 256];
627
627
  },
628
628
 
package/duotecno/types.js CHANGED
@@ -413,6 +413,7 @@ exports.Sanitizers = {
413
413
  c.masterAddress = (config === null || config === void 0 ? void 0 : config.masterAddress) || ((_a = system === null || system === void 0 ? void 0 : system.cmasters[0]) === null || _a === void 0 ? void 0 : _a.address) || "";
414
414
  c.masterPort = (config === null || config === void 0 ? void 0 : config.masterPort) || ((_b = system === null || system === void 0 ? void 0 : system.cmasters[0]) === null || _b === void 0 ? void 0 : _b.port) || 5001;
415
415
  c.uniqueId = (config === null || config === void 0 ? void 0 : config.uniqueId) || proxy_1.kEmptyProxy.uniqueId;
416
+ c.kind = "gw";
416
417
  return c;
417
418
  },
418
419
  //////////////
package/duotecno/types.ts CHANGED
@@ -251,6 +251,10 @@ export interface Switch extends UnitDef {
251
251
  value?: number | boolean | string;
252
252
  nomacro: string;
253
253
  nostop: string;
254
+
255
+ // Track last sent status to prevent duplicate action to be triggered
256
+ lastSentStatus?: number | boolean;
257
+ lastSentValue?: number | boolean | string;
254
258
  }
255
259
 
256
260
  export const kEmptySwitch: Switch = {
@@ -667,6 +671,7 @@ export const Sanitizers = {
667
671
  c.masterAddress = config?.masterAddress || system?.cmasters[0]?.address || "";
668
672
  c.masterPort = config?.masterPort || system?.cmasters[0]?.port || 5001;
669
673
  c.uniqueId = config?.uniqueId || kEmptyProxy.uniqueId;
674
+ c.kind = "gw";
670
675
  return c;
671
676
  },
672
677
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "homebridge-smartsystem",
3
3
  "displayname": "Duotecno Bridge",
4
- "version": "7.0.1",
4
+ "version": "7.0.4",
5
5
  "description": "SmartServer (Proxy TCP sockets to the cloud, Smappee MQTT, Duotecno IP Nodes, Homekit interface)",
6
6
  "main": "index.js",
7
7
  "author": "Johan Coppieters",
package/server/mDNS.js CHANGED
@@ -40,26 +40,55 @@ class MDNSService {
40
40
  const ports = Array.isArray(port) ? port : [port];
41
41
  const primaryPort = ports[0];
42
42
  // Register the HTTP service using bonjour
43
- const service = this.bonjour.publish({
44
- name,
45
- type: 'http',
46
- port: primaryPort,
47
- txt: {
48
- version: version || '1.0.0',
49
- ports: ports.join(','),
50
- path: '/'
43
+ // Wrap in try-catch because bonjour can throw synchronously on conflicts
44
+ let service;
45
+ try {
46
+ service = this.bonjour.publish({
47
+ name,
48
+ type: 'http',
49
+ port: primaryPort,
50
+ txt: {
51
+ version: version || '1.0.0',
52
+ ports: ports.join(','),
53
+ path: '/'
54
+ }
55
+ });
56
+ (0, logger_1.log)("mDNS", `HTTP service registered: ${name}.local on port(s) ${ports.join(', ')}`);
57
+ }
58
+ catch (publishError) {
59
+ if (publishError.message && publishError.message.includes('already in use')) {
60
+ (0, logger_1.err)("mDNS", `Service name "${name}" is already in use. This is usually harmless - continuing anyway.`);
61
+ (0, logger_1.err)("mDNS", "The old service registration will expire in ~30 seconds.");
51
62
  }
52
- });
53
- (0, logger_1.log)("mDNS", `HTTP service registered: ${name}.local on port(s) ${ports.join(', ')}`);
54
- // Handle service errors
55
- service.on('error', (error) => {
56
- (0, logger_1.err)("mDNS", `Service error: ${error.message}`);
57
- });
63
+ else {
64
+ (0, logger_1.err)("mDNS", `Service publish error: ${publishError.message}`);
65
+ }
66
+ // Continue anyway - service will work once the old registration expires
67
+ }
68
+ // Handle service errors - especially "already in use" errors
69
+ if (service) {
70
+ service.on('error', (error) => {
71
+ if (error.message.includes('already in use')) {
72
+ (0, logger_1.err)("mDNS", `Service name "${name}" is already in use. Another instance may be running.`);
73
+ (0, logger_1.err)("mDNS", "To fix: 1) Stop other instances, or 2) Change the mdnsName in settings, or 3) Wait 30s for old service to expire");
74
+ }
75
+ else {
76
+ (0, logger_1.err)("mDNS", `Service error: ${error.message}`);
77
+ }
78
+ });
79
+ }
58
80
  // Publish the hostname A record (like ESP32 MDNS.begin())
59
81
  this.publishHostname(name);
60
82
  }
61
83
  catch (e) {
62
- (0, logger_1.err)("mDNS", `Failed to register service: ${e.message}`);
84
+ if (e.message.includes('already in use')) {
85
+ (0, logger_1.err)("mDNS", `Service name "${this.config.name}" is already in use on the network`);
86
+ (0, logger_1.err)("mDNS", "Possible causes: 1) Another instance is running, 2) Previous instance didn't clean up properly");
87
+ (0, logger_1.err)("mDNS", "Solutions: Stop other instances, or restart with a different service name in settings");
88
+ }
89
+ else {
90
+ (0, logger_1.err)("mDNS", `Failed to register service: ${e.message.split('\n')[0]}`);
91
+ }
63
92
  }
64
93
  }
65
94
  /**
package/server/mDNS.ts CHANGED
@@ -52,29 +52,54 @@ export class MDNSService {
52
52
  const primaryPort = ports[0];
53
53
 
54
54
  // Register the HTTP service using bonjour
55
- const service = this.bonjour.publish({
56
- name,
57
- type: 'http',
58
- port: primaryPort,
59
- txt: {
60
- version: version || '1.0.0',
61
- ports: ports.join(','),
62
- path: '/'
55
+ // Wrap in try-catch because bonjour can throw synchronously on conflicts
56
+ let service;
57
+ try {
58
+ service = this.bonjour.publish({
59
+ name,
60
+ type: 'http',
61
+ port: primaryPort,
62
+ txt: {
63
+ version: version || '1.0.0',
64
+ ports: ports.join(','),
65
+ path: '/'
66
+ }
67
+ });
68
+
69
+ log("mDNS", `HTTP service registered: ${name}.local on port(s) ${ports.join(', ')}`);
70
+ } catch (publishError: any) {
71
+ if (publishError.message && publishError.message.includes('already in use')) {
72
+ err("mDNS", `Service name "${name}" is already in use. This is usually harmless - continuing anyway.`);
73
+ err("mDNS", "The old service registration will expire in ~30 seconds.");
74
+ } else {
75
+ err("mDNS", `Service publish error: ${publishError.message}`);
63
76
  }
64
- });
65
-
66
- log("mDNS", `HTTP service registered: ${name}.local on port(s) ${ports.join(', ')}`);
77
+ // Continue anyway - service will work once the old registration expires
78
+ }
67
79
 
68
- // Handle service errors
69
- service.on('error', (error) => {
70
- err("mDNS", `Service error: ${error.message}`);
71
- });
80
+ // Handle service errors - especially "already in use" errors
81
+ if (service) {
82
+ service.on('error', (error: Error) => {
83
+ if (error.message.includes('already in use')) {
84
+ err("mDNS", `Service name "${name}" is already in use. Another instance may be running.`);
85
+ err("mDNS", "To fix: 1) Stop other instances, or 2) Change the mdnsName in settings, or 3) Wait 30s for old service to expire");
86
+ } else {
87
+ err("mDNS", `Service error: ${error.message}`);
88
+ }
89
+ });
90
+ }
72
91
 
73
92
  // Publish the hostname A record (like ESP32 MDNS.begin())
74
93
  this.publishHostname(name);
75
94
 
76
95
  } catch (e) {
77
- err("mDNS", `Failed to register service: ${e.message}`);
96
+ if (e.message.includes('already in use')) {
97
+ err("mDNS", `Service name "${this.config.name}" is already in use on the network`);
98
+ err("mDNS", "Possible causes: 1) Another instance is running, 2) Previous instance didn't clean up properly");
99
+ err("mDNS", "Solutions: Stop other instances, or restart with a different service name in settings");
100
+ } else {
101
+ err("mDNS", `Failed to register service: ${e.message.split('\n')[0]}`);
102
+ }
78
103
  }
79
104
  }
80
105
 
@@ -79,16 +79,17 @@ class PowerBase extends base_1.Base {
79
79
  });
80
80
  }
81
81
  evalPower(exp) {
82
- var _a, _b, _c, _d, _e, _f;
82
+ var _a, _b, _c, _d;
83
83
  let parsed = exp.split(/(\+|-)/);
84
84
  let result = (_b = (_a = this.channels[parseInt(parsed[0]) || 0]) === null || _a === void 0 ? void 0 : _a.power) !== null && _b !== void 0 ? _b : 0;
85
85
  for (let i = 1; i < parsed.length; i = i + 2) {
86
+ const channelIndex = parseInt(parsed[i + 1]) || 0;
87
+ const channelPower = (_d = (_c = this.channels[channelIndex]) === null || _c === void 0 ? void 0 : _c.power) !== null && _d !== void 0 ? _d : 0;
86
88
  if (parsed[i] === "-")
87
- result -= (_d = (_c = this.channels[parseInt(parsed[i + 1]) || 0]) === null || _c === void 0 ? void 0 : _c.power) !== null && _d !== void 0 ? _d : 0;
89
+ result -= channelPower;
88
90
  else if (parsed[i] === "+")
89
- result += (_f = (_e = this.channels[parseInt(parsed[i + 1]) || 0]) === null || _e === void 0 ? void 0 : _e.power) !== null && _f !== void 0 ? _f : 0;
91
+ result += channelPower;
90
92
  }
91
- (0, logger_1.debug)("power", "evalPower: " + exp + " -> " + parsed + " = " + result);
92
93
  return result;
93
94
  }
94
95
  //////////////////////////////////////////////
@@ -114,6 +115,7 @@ class PowerBase extends base_1.Base {
114
115
  // for all bindings
115
116
  this.bindings.forEach(b => {
116
117
  const power = this.evalPower(b.channel);
118
+ (0, logger_1.debug)("power", `Binding channel=${b.channel}, register=${b.register}, current_value=${b.value}, calculated_power=${power}`);
117
119
  if (this.significant(power, b.value)) {
118
120
  (0, logger_1.debug)("power", "APPLY to " + b.channel + " = " + b.value + " -> " + power + " for " + b.register +
119
121
  "): difference: " + Math.abs(b.value - power) +
@@ -124,10 +126,11 @@ class PowerBase extends base_1.Base {
124
126
  (0, logger_1.debug)("power", "set register " + b.register + " to " + power);
125
127
  })
126
128
  .catch((e) => (0, logger_1.err)(this.type, e.message || e));
127
- // } else {
128
- // debug("power", "DONT to " + b.channel + " = " + b.power + " -> " + power + " for " + b.register +
129
- // " : difference: " + Math.abs(b.power - power) +
130
- // " > threshold: 5% =" + Math.abs(b.power * 0.05) + " > threshold: 2% =" + Math.abs(b.power * 0.02));
129
+ }
130
+ else {
131
+ (0, logger_1.debug)("power", "NOT significant for " + b.channel + " = " + b.value + " -> " + power + " for register " + b.register +
132
+ ": difference: " + Math.abs(b.value - power) +
133
+ " <= threshold: 5% =" + Math.abs(b.value * 0.05) + " or 2% =" + Math.abs(b.value * 0.02));
131
134
  }
132
135
  });
133
136
  }
@@ -99,12 +99,13 @@ export class PowerBase extends Base {
99
99
  let parsed = exp.split(/(\+|-)/);
100
100
  let result = this.channels[parseInt(parsed[0])||0]?.power ?? 0;
101
101
  for (let i=1; i < parsed.length; i=i+2) {
102
+ const channelIndex = parseInt(parsed[i+1])||0;
103
+ const channelPower = this.channels[channelIndex]?.power ?? 0;
102
104
  if (parsed[i] === "-")
103
- result -= this.channels[parseInt(parsed[i+1])||0]?.power ?? 0;
105
+ result -= channelPower;
104
106
  else if (parsed[i] === "+")
105
- result += this.channels[parseInt(parsed[i+1])||0]?.power ?? 0;
107
+ result += channelPower;
106
108
  }
107
- debug("power", "evalPower: " + exp + " -> " + parsed + " = " + result);
108
109
  return result;
109
110
  }
110
111
 
@@ -136,6 +137,7 @@ export class PowerBase extends Base {
136
137
  // for all bindings
137
138
  this.bindings.forEach( b => {
138
139
  const power = this.evalPower(b.channel);
140
+ debug("power", `Binding channel=${b.channel}, register=${b.register}, current_value=${b.value}, calculated_power=${power}`);
139
141
  if (this.significant(power, b.value)) {
140
142
  debug("power", "APPLY to " + b.channel + " = " + b.value + " -> " + power + " for " + b.register +
141
143
  "): difference: " + Math.abs(b.value - power) +
@@ -147,10 +149,10 @@ export class PowerBase extends Base {
147
149
  })
148
150
  .catch((e) => err(this.type, e.message || e));
149
151
 
150
- // } else {
151
- // debug("power", "DONT to " + b.channel + " = " + b.power + " -> " + power + " for " + b.register +
152
- // " : difference: " + Math.abs(b.power - power) +
153
- // " > threshold: 5% =" + Math.abs(b.power * 0.05) + " > threshold: 2% =" + Math.abs(b.power * 0.02));
152
+ } else {
153
+ debug("power", "NOT significant for " + b.channel + " = " + b.value + " -> " + power + " for register " + b.register +
154
+ ": difference: " + Math.abs(b.value - power) +
155
+ " <= threshold: 5% =" + Math.abs(b.value * 0.05) + " or 2% =" + Math.abs(b.value * 0.02));
154
156
  }
155
157
  });
156
158
  }
package/server/proxy.js CHANGED
@@ -20,7 +20,7 @@ exports.kEmptyProxy = {
20
20
  masterPort: 5001,
21
21
  uniqueId: "",
22
22
  debug: false,
23
- kind: "solo" // default running under PM2 or other process manager -> don't restart yourself
23
+ kind: "gw" // default running under PM2 or other process manager -> don't restart yourself
24
24
  };
25
25
  const config = Object.assign({}, exports.kEmptyProxy);
26
26
  // Store reference to the webapp so we can close servers before restarting
@@ -117,7 +117,7 @@ exports.makeNewCloudConnection = makeNewCloudConnection;
117
117
  // Restarting //
118
118
  ////////////////
119
119
  function cleanStart(restart = false) {
120
- if (gCloudConnections.length) {
120
+ if (gCloudConnections === null || gCloudConnections === void 0 ? void 0 : gCloudConnections.length) {
121
121
  gCloudConnections.forEach(ctx => ctx.cleanupSockets());
122
122
  log(`[PROXY] → [CLOUD] Cleaned up all ${gCloudConnections.length} cloud connections.`);
123
123
  gCloudConnections.splice(0, gCloudConnections.length); // clear the array
@@ -127,6 +127,7 @@ function cleanStart(restart = false) {
127
127
  connectionChecker = null;
128
128
  }
129
129
  if (restart) {
130
+ log(`[PROXY] → [CLOUD] Restarting proxy... for ${config.uniqueId}.`);
130
131
  if (config.uniqueId) {
131
132
  log(`[PROXY] Starting proxy with config: ${JSON.stringify(config, null, 2)}`);
132
133
  makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
@@ -284,7 +285,7 @@ class Context {
284
285
  gCloudConnections.splice(index, 1);
285
286
  this.cleanupSockets();
286
287
  log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
287
- if (gCloudConnections.length === 0) {
288
+ if (gCloudConnections && gCloudConnections.length === 0) {
288
289
  log(`[PROXY] → [CLOUD] No more cloud connections, restarting proxy.`);
289
290
  // just to be sure: restart...
290
291
  if (config.kind === "pm2") {
@@ -345,8 +346,10 @@ function handleShutdown(signal) {
345
346
  connectionChecker = null;
346
347
  }
347
348
  // Close all cloud connections
348
- gCloudConnections.forEach(ctx => ctx.cleanupSockets());
349
- gCloudConnections.splice(0, gCloudConnections.length);
349
+ if (gCloudConnections && gCloudConnections.length > 0) {
350
+ gCloudConnections.forEach(ctx => ctx.cleanupSockets());
351
+ gCloudConnections.splice(0, gCloudConnections.length);
352
+ }
350
353
  // Close HTTP servers if available
351
354
  if (gWebApp && typeof gWebApp.closeServers === 'function') {
352
355
  log(`[PROXY] → [SYSTEM] Closing HTTP servers...`);
@@ -381,11 +384,24 @@ process.on('SIGTERM', (err) => {
381
384
  handleShutdown('SIGTERM');
382
385
  });
383
386
  process.on('uncaughtException', (err) => {
384
- error(`[SYSTEM] Uncaught Exception: ${err.message}`);
387
+ error(`[SYSTEM] Uncaught Exception: ${err.message || err}`);
385
388
  handleShutdown('uncaughtException');
386
389
  });
387
390
  process.on('unhandledRejection', (reason) => {
388
- error(`[SYSTEM] Unhandled Rejection: ${reason}`);
391
+ // Handle different types of rejection reasons
392
+ try {
393
+ const message = reason instanceof Error ? reason.message :
394
+ typeof reason === 'object' ? JSON.stringify(reason) :
395
+ String(reason);
396
+ error(`[SYSTEM] Unhandled Rejection: ${message}`);
397
+ // Also log the stack trace if available
398
+ if (reason instanceof Error && reason.stack) {
399
+ console.error('Stack trace:', reason.stack);
400
+ }
401
+ }
402
+ catch (e) {
403
+ error(`[SYSTEM] Unhandled Rejection: [Error converting reason to string]`);
404
+ }
389
405
  handleShutdown('unhandledRejection');
390
406
  });
391
407
  //# sourceMappingURL=proxy.js.map
package/server/proxy.ts CHANGED
@@ -23,7 +23,7 @@ export const kEmptyProxy: ProxyConfig = {
23
23
  masterPort: 5001,
24
24
  uniqueId: "", // no unique ID -> don't start the proxy
25
25
  debug: false, // default nodebug mode
26
- kind: "solo" // default running under PM2 or other process manager -> don't restart yourself
26
+ kind: "gw" // default running under PM2 or other process manager -> don't restart yourself
27
27
  }
28
28
 
29
29
  const config = {...kEmptyProxy};
@@ -137,7 +137,7 @@ export function makeNewCloudConnection(master: string, masterPort: number, serve
137
137
  // Restarting //
138
138
  ////////////////
139
139
  export function cleanStart(restart: boolean = false) {
140
- if (gCloudConnections.length) {
140
+ if (gCloudConnections?.length) {
141
141
  gCloudConnections.forEach(ctx => ctx.cleanupSockets());
142
142
  log(`[PROXY] → [CLOUD] Cleaned up all ${gCloudConnections.length} cloud connections.`);
143
143
 
@@ -150,6 +150,7 @@ export function cleanStart(restart: boolean = false) {
150
150
  }
151
151
 
152
152
  if (restart) {
153
+ log(`[PROXY] → [CLOUD] Restarting proxy... for ${config.uniqueId}.`);
153
154
  if (config.uniqueId) {
154
155
  log(`[PROXY] Starting proxy with config: ${JSON.stringify(config, null, 2)}`);
155
156
  makeNewCloudConnection(config.masterAddress, config.masterPort, config.cloudServer, config.cloudPort, config.uniqueId);
@@ -344,7 +345,7 @@ export class Context {
344
345
  this.cleanupSockets();
345
346
  log(`[PROXY] → [CLOUD] Closed socket to Cloud and removed context for device ${this.master}`);
346
347
 
347
- if (gCloudConnections.length === 0) {
348
+ if (gCloudConnections && gCloudConnections.length === 0) {
348
349
  log(`[PROXY] → [CLOUD] No more cloud connections, restarting proxy.`);
349
350
  // just to be sure: restart...
350
351
  if (config.kind === "pm2") {
@@ -415,8 +416,10 @@ async function handleShutdown(signal: string) {
415
416
  }
416
417
 
417
418
  // Close all cloud connections
418
- gCloudConnections.forEach(ctx => ctx.cleanupSockets());
419
- gCloudConnections.splice(0, gCloudConnections.length);
419
+ if (gCloudConnections && gCloudConnections.length > 0) {
420
+ gCloudConnections.forEach(ctx => ctx.cleanupSockets());
421
+ gCloudConnections.splice(0, gCloudConnections.length);
422
+ }
420
423
 
421
424
  // Close HTTP servers if available
422
425
  if (gWebApp && typeof gWebApp.closeServers === 'function') {
@@ -456,11 +459,24 @@ process.on('SIGTERM', (err) => {
456
459
  });
457
460
 
458
461
  process.on('uncaughtException', (err) => {
459
- error(`[SYSTEM] Uncaught Exception: ${err.message}`);
462
+ error(`[SYSTEM] Uncaught Exception: ${err.message || err}`);
460
463
  handleShutdown('uncaughtException');
461
464
  });
462
465
 
463
- process.on('unhandledRejection', (reason) => {
464
- error(`[SYSTEM] Unhandled Rejection: ${reason}`);
466
+ process.on('unhandledRejection', (reason: any) => {
467
+ // Handle different types of rejection reasons
468
+ try {
469
+ const message = reason instanceof Error ? reason.message :
470
+ typeof reason === 'object' ? JSON.stringify(reason) :
471
+ String(reason);
472
+ error(`[SYSTEM] Unhandled Rejection: ${message}`);
473
+
474
+ // Also log the stack trace if available
475
+ if (reason instanceof Error && reason.stack) {
476
+ console.error('Stack trace:', reason.stack);
477
+ }
478
+ } catch (e) {
479
+ error(`[SYSTEM] Unhandled Rejection: [Error converting reason to string]`);
480
+ }
465
481
  handleShutdown('unhandledRejection');
466
482
  });
package/server/shelly.js CHANGED
@@ -102,7 +102,7 @@ class Shelly extends powerbase_1.PowerBase {
102
102
  }
103
103
  }
104
104
  (0, logger_1.debug)("shelly", "meters: " + JSON.stringify(this.meters));
105
- if (this.meters.length) {
105
+ if (this.meters && this.meters.length) {
106
106
  this.timer = setInterval(() => __awaiter(this, void 0, void 0, function* () {
107
107
  this.realtimeCounter += kInterval;
108
108
  // fetch data
package/server/shelly.ts CHANGED
@@ -119,7 +119,7 @@ export class Shelly extends PowerBase {
119
119
  }
120
120
  debug("shelly", "meters: " + JSON.stringify(this.meters));
121
121
 
122
- if (this.meters.length) {
122
+ if (this.meters && this.meters.length) {
123
123
  this.timer = setInterval(async () => {
124
124
  this.realtimeCounter += kInterval;
125
125
 
@@ -1125,6 +1125,18 @@ class SmartApp extends webapp_1.WebApp {
1125
1125
  (0, logger_1.err)("smartapp", "Don't have unit for switch: " + swtch.unitname);
1126
1126
  }
1127
1127
  else {
1128
+ // Check if state has actually changed (to prevent duplicate notifications/actions)
1129
+ // Exception: Moods should always execute even if state hasn't changed (they are actions, not states)
1130
+ const currentStatus = !!swtch.unit.status;
1131
+ const currentValue = +swtch.unit.value;
1132
+ const isMood = (swtch.unit.type === types_1.UnitType.kMood);
1133
+ if (!isMood &&
1134
+ swtch.lastSentStatus === currentStatus &&
1135
+ swtch.lastSentValue === currentValue) {
1136
+ (0, logger_1.debug)("smartapp", "Switch state unchanged (status=" + currentStatus + ", value=" + currentValue + "), skipping action for " + swtch.name);
1137
+ return;
1138
+ }
1139
+ // Dispatch to appropriate handler
1128
1140
  if ((swtch.type === types_1.SwitchType.kSmappee) && (this.power.smappee)) {
1129
1141
  this.power.smappee.setPlug(parseInt(swtch.plug), swtch.unit.value);
1130
1142
  }
@@ -1152,6 +1164,9 @@ class SmartApp extends webapp_1.WebApp {
1152
1164
  else {
1153
1165
  (0, logger_1.err)("smartapp", "Don't know how to set a switch of type " + swtch.type);
1154
1166
  }
1167
+ // Remember the state we just sent (for all switch types, including moods)
1168
+ swtch.lastSentStatus = currentStatus;
1169
+ swtch.lastSentValue = currentValue;
1155
1170
  }
1156
1171
  }
1157
1172
  ///////////
@@ -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, Device, kEmptyDevice, PowerSource } from "../duotecno/types";
12
+ import { Sanitizers, Rule, kEmptyRule, actionValue, Action, kEmptySwitch, Switch, SwitchType, hex, Binding, NetworkConfig, SettingsConfig, kEmptySettings, Link, LinkType, kEmptyLink, Device, kEmptyDevice, PowerSource, UnitType } 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";
@@ -1237,6 +1237,20 @@ export class SmartApp extends WebApp {
1237
1237
  err("smartapp", "Don't have unit for switch: "+swtch.unitname);
1238
1238
 
1239
1239
  } else {
1240
+ // Check if state has actually changed (to prevent duplicate notifications/actions)
1241
+ // Exception: Moods should always execute even if state hasn't changed (they are actions, not states)
1242
+ const currentStatus = !!swtch.unit.status;
1243
+ const currentValue = +swtch.unit.value;
1244
+ const isMood = (swtch.unit.type === UnitType.kMood);
1245
+
1246
+ if (!isMood &&
1247
+ swtch.lastSentStatus === currentStatus &&
1248
+ swtch.lastSentValue === currentValue) {
1249
+ debug("smartapp", "Switch state unchanged (status=" + currentStatus + ", value=" + currentValue + "), skipping action for " + swtch.name);
1250
+ return;
1251
+ }
1252
+
1253
+ // Dispatch to appropriate handler
1240
1254
  if ((swtch.type === SwitchType.kSmappee) && (this.power.smappee)) {
1241
1255
  this.power.smappee.setPlug(parseInt(swtch.plug), swtch.unit.value);
1242
1256
 
@@ -1264,6 +1278,10 @@ export class SmartApp extends WebApp {
1264
1278
  } else {
1265
1279
  err("smartapp", "Don't know how to set a switch of type " + swtch.type);
1266
1280
  }
1281
+
1282
+ // Remember the state we just sent (for all switch types, including moods)
1283
+ swtch.lastSentStatus = currentStatus;
1284
+ swtch.lastSentValue = currentValue;
1267
1285
  }
1268
1286
  }
1269
1287
 
package/server/webapp.js CHANGED
@@ -246,6 +246,10 @@ class WebApp extends base_1.Base {
246
246
  return false; // Default: use cached templates for performance
247
247
  }
248
248
  serve(onConnected) {
249
+ // Ensure servers array is initialized (defensive coding)
250
+ if (!this.servers) {
251
+ this.servers = [];
252
+ }
249
253
  // Handle both single port and multiple ports
250
254
  const ports = Array.isArray(this.port) ? this.port : [this.port];
251
255
  (0, logger_1.log)("server", "WebApp - Start serving http on port(s): " + ports.join(", "));
@@ -297,8 +301,12 @@ class WebApp extends base_1.Base {
297
301
  */
298
302
  closeServers() {
299
303
  return __awaiter(this, void 0, void 0, function* () {
304
+ // First, cleanup mDNS to avoid "already in use" errors on restart
305
+ this.destroyMDNS();
300
306
  return new Promise((resolve) => {
301
- if (this.servers.length === 0) {
307
+ // Safety check: servers might be undefined if never initialized
308
+ if (!this.servers || this.servers.length === 0) {
309
+ (0, logger_1.log)("server", "No HTTP servers to close");
302
310
  resolve();
303
311
  return;
304
312
  }
package/server/webapp.ts CHANGED
@@ -301,6 +301,11 @@ export class WebApp extends Base {
301
301
  }
302
302
 
303
303
  serve(onConnected?: () => void) {
304
+ // Ensure servers array is initialized (defensive coding)
305
+ if (!this.servers) {
306
+ this.servers = [];
307
+ }
308
+
304
309
  // Handle both single port and multiple ports
305
310
  const ports = Array.isArray(this.port) ? this.port : [this.port];
306
311
 
@@ -365,8 +370,13 @@ export class WebApp extends Base {
365
370
  * Call this before restarting or shutting down
366
371
  */
367
372
  async closeServers(): Promise<void> {
373
+ // First, cleanup mDNS to avoid "already in use" errors on restart
374
+ this.destroyMDNS();
375
+
368
376
  return new Promise((resolve) => {
369
- if (this.servers.length === 0) {
377
+ // Safety check: servers might be undefined if never initialized
378
+ if (!this.servers || this.servers.length === 0) {
379
+ log("server", "No HTTP servers to close");
370
380
  resolve();
371
381
  return;
372
382
  }