nodejs-poolcontroller 8.1.2 → 8.4.0
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/.eslintrc.json +36 -36
- package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -84
- package/.github/ISSUE_TEMPLATE/2-docs.md +12 -12
- package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -28
- package/.github/ISSUE_TEMPLATE/config.yml +8 -8
- package/.github/copilot-instructions.md +63 -0
- package/.github/workflows/ghcr-publish.yml +67 -0
- package/AGENTS.md +597 -0
- package/CONTRIBUTING.md +74 -74
- package/Changelog +292 -257
- package/Dockerfile +62 -19
- package/Gruntfile.js +40 -40
- package/LICENSE +661 -661
- package/README.md +318 -191
- package/anslq25/MessagesMock.ts +221 -221
- package/anslq25/boards/MockBoardFactory.ts +49 -49
- package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
- package/anslq25/boards/MockSystemBoard.ts +216 -216
- package/anslq25/chemistry/MockChlorinator.ts +98 -98
- package/anslq25/pumps/MockPump.ts +83 -83
- package/app.ts +115 -115
- package/config/Config.ts +57 -7
- package/config/VersionCheck.ts +63 -35
- package/controller/Constants.ts +809 -805
- package/controller/Equipment.ts +2688 -2664
- package/controller/Errors.ts +181 -181
- package/controller/Lockouts.ts +549 -549
- package/controller/State.ts +3738 -3690
- package/controller/boards/AquaLinkBoard.ts +1003 -1003
- package/controller/boards/BoardFactory.ts +53 -53
- package/controller/boards/EasyTouchBoard.ts +3202 -3202
- package/controller/boards/IntelliCenterBoard.ts +4393 -3899
- package/controller/boards/IntelliComBoard.ts +69 -69
- package/controller/boards/IntelliTouchBoard.ts +382 -382
- package/controller/boards/NixieBoard.ts +1944 -1929
- package/controller/boards/SunTouchBoard.ts +400 -400
- package/controller/boards/SystemBoard.ts +5268 -5268
- package/controller/comms/Comms.ts +1272 -1214
- package/controller/comms/ScreenLogic.ts +1665 -1665
- package/controller/comms/messages/Messages.ts +1433 -1243
- package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
- package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
- package/controller/comms/messages/config/CircuitMessage.ts +0 -0
- package/controller/comms/messages/config/ConfigMessage.ts +6 -0
- package/controller/comms/messages/config/CoverMessage.ts +0 -0
- package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
- package/controller/comms/messages/config/EquipmentMessage.ts +216 -210
- package/controller/comms/messages/config/ExternalMessage.ts +96 -10
- package/controller/comms/messages/config/FeatureMessage.ts +0 -0
- package/controller/comms/messages/config/GeneralMessage.ts +0 -0
- package/controller/comms/messages/config/HeaterMessage.ts +0 -0
- package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
- package/controller/comms/messages/config/OptionsMessage.ts +194 -174
- package/controller/comms/messages/config/PumpMessage.ts +0 -0
- package/controller/comms/messages/config/RemoteMessage.ts +0 -0
- package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
- package/controller/comms/messages/config/SecurityMessage.ts +0 -0
- package/controller/comms/messages/config/ValveMessage.ts +0 -0
- package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
- package/controller/comms/messages/status/EquipmentStateMessage.ts +1158 -822
- package/controller/comms/messages/status/HeaterStateMessage.ts +135 -135
- package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
- package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
- package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
- package/controller/comms/messages/status/RegalModbusStateMessage.ts +411 -0
- package/controller/comms/messages/status/VersionMessage.ts +103 -41
- package/controller/nixie/Nixie.ts +173 -173
- package/controller/nixie/NixieEquipment.ts +104 -104
- package/controller/nixie/bodies/Body.ts +120 -120
- package/controller/nixie/bodies/Filter.ts +135 -135
- package/controller/nixie/chemistry/ChemController.ts +2724 -2724
- package/controller/nixie/chemistry/ChemDoser.ts +806 -806
- package/controller/nixie/chemistry/Chlorinator.ts +367 -367
- package/controller/nixie/circuits/Circuit.ts +478 -478
- package/controller/nixie/heaters/Heater.ts +834 -834
- package/controller/nixie/pumps/Pump.ts +1194 -996
- package/controller/nixie/schedules/Schedule.ts +401 -401
- package/controller/nixie/valves/Valve.ts +170 -170
- package/defaultConfig.json +352 -347
- package/docker-compose.yml +32 -0
- package/logger/DataLogger.ts +448 -448
- package/logger/Logger.ts +448 -436
- package/package.json +58 -60
- package/sendSocket.js +32 -32
- package/tsconfig.json +25 -25
- package/types/express-multer.d.ts +32 -0
- package/web/Server.ts +1937 -1927
- package/web/bindings/aqualinkD.json +559 -559
- package/web/bindings/influxDB.json +1066 -1066
- package/web/bindings/mqtt.json +721 -721
- package/web/bindings/mqttAlt.json +746 -746
- package/web/bindings/rulesManager.json +54 -54
- package/web/bindings/smartThings-Hubitat.json +31 -31
- package/web/bindings/valveRelays.json +20 -20
- package/web/bindings/vera.json +25 -25
- package/web/interfaces/baseInterface.ts +188 -188
- package/web/interfaces/httpInterface.ts +148 -148
- package/web/interfaces/influxInterface.ts +283 -283
- package/web/interfaces/mqttInterface.ts +695 -695
- package/web/interfaces/ruleInterface.ts +101 -87
- package/web/services/config/Config.ts +1063 -1053
- package/web/services/config/ConfigSocket.ts +0 -0
- package/web/services/state/State.ts +0 -0
- package/web/services/state/StateSocket.ts +0 -0
- package/web/services/utilities/Utilities.ts +233 -233
- package/.github/workflows/docker-publish-njsPC-linux.yml +0 -50
|
@@ -1,822 +1,1158 @@
|
|
|
1
|
-
/* nodejs-poolController. An application to control pool equipment.
|
|
2
|
-
Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
|
|
3
|
-
Russell Goldin, tagyoureit. russ.goldin@gmail.com
|
|
4
|
-
|
|
5
|
-
This program is free software: you can redistribute it and/or modify
|
|
6
|
-
it under the terms of the GNU Affero General Public License as
|
|
7
|
-
published by the Free Software Foundation, either version 3 of the
|
|
8
|
-
License, or (at your option) any later version.
|
|
9
|
-
|
|
10
|
-
This program is distributed in the hope that it will be useful,
|
|
11
|
-
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
-
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
-
GNU Affero General Public License for more details.
|
|
14
|
-
|
|
15
|
-
You should have received a copy of the GNU Affero General Public License
|
|
16
|
-
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
-
*/
|
|
18
|
-
import { IntelliCenterBoard } from 'controller/boards/IntelliCenterBoard';
|
|
19
|
-
import { EasyTouchBoard } from 'controller/boards/EasyTouchBoard';
|
|
20
|
-
import { IntelliTouchBoard } from 'controller/boards/IntelliTouchBoard';
|
|
21
|
-
import { SunTouchBoard } from "controller/boards/SunTouchBoard";
|
|
22
|
-
|
|
23
|
-
import { logger } from '../../../../logger/Logger';
|
|
24
|
-
import { ControllerType } from '../../../Constants';
|
|
25
|
-
import { Body, Circuit, ExpansionPanel, Feature, Heater, sys } from '../../../Equipment';
|
|
26
|
-
import { BodyTempState, ScheduleState, State, state } from '../../../State';
|
|
27
|
-
import { ExternalMessage } from '../config/ExternalMessage';
|
|
28
|
-
import { Inbound, Message } from '../Messages';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
//
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
logger.info(`
|
|
127
|
-
sys.controllerType = ControllerType.
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
if (sys.bodies.length > 2 || sys.equipment.dual)
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
sys.
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
state.
|
|
598
|
-
state.
|
|
599
|
-
state.time.
|
|
600
|
-
state.time.
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
//
|
|
615
|
-
//
|
|
616
|
-
//
|
|
617
|
-
//
|
|
618
|
-
//
|
|
619
|
-
//
|
|
620
|
-
//
|
|
621
|
-
//
|
|
622
|
-
//
|
|
623
|
-
//
|
|
624
|
-
//
|
|
625
|
-
//
|
|
626
|
-
//
|
|
627
|
-
//
|
|
628
|
-
//
|
|
629
|
-
//
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
let
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
sys.
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
//
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
case
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
}
|
|
1
|
+
/* nodejs-poolController. An application to control pool equipment.
|
|
2
|
+
Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
|
|
3
|
+
Russell Goldin, tagyoureit. russ.goldin@gmail.com
|
|
4
|
+
|
|
5
|
+
This program is free software: you can redistribute it and/or modify
|
|
6
|
+
it under the terms of the GNU Affero General Public License as
|
|
7
|
+
published by the Free Software Foundation, either version 3 of the
|
|
8
|
+
License, or (at your option) any later version.
|
|
9
|
+
|
|
10
|
+
This program is distributed in the hope that it will be useful,
|
|
11
|
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
12
|
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
13
|
+
GNU Affero General Public License for more details.
|
|
14
|
+
|
|
15
|
+
You should have received a copy of the GNU Affero General Public License
|
|
16
|
+
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
17
|
+
*/
|
|
18
|
+
import { IntelliCenterBoard } from 'controller/boards/IntelliCenterBoard';
|
|
19
|
+
import { EasyTouchBoard } from 'controller/boards/EasyTouchBoard';
|
|
20
|
+
import { IntelliTouchBoard } from 'controller/boards/IntelliTouchBoard';
|
|
21
|
+
import { SunTouchBoard } from "controller/boards/SunTouchBoard";
|
|
22
|
+
|
|
23
|
+
import { logger } from '../../../../logger/Logger';
|
|
24
|
+
import { ControllerType } from '../../../Constants';
|
|
25
|
+
import { Body, Circuit, ExpansionPanel, Feature, Heater, sys } from '../../../Equipment';
|
|
26
|
+
import { BodyTempState, ScheduleState, State, state } from '../../../State';
|
|
27
|
+
import { ExternalMessage } from '../config/ExternalMessage';
|
|
28
|
+
import { Inbound, Message, Outbound } from '../Messages';
|
|
29
|
+
|
|
30
|
+
// Cache for pending Wireless Action 184 commands to correlate with state changes
|
|
31
|
+
// Key: targetId, Value: { state: 0|1, timestamp: number }
|
|
32
|
+
const pendingAction184Commands: Map<number, { state: number, timestamp: number }> = new Map();
|
|
33
|
+
const PENDING_COMMAND_TTL_MS = 10000; // 10 second TTL for pending commands
|
|
34
|
+
|
|
35
|
+
function findActiveCircuitTargetIdOwner(targetId: number, excludeCircuitId?: number): Circuit | null {
|
|
36
|
+
if (typeof targetId !== 'number' || targetId <= 0) return null;
|
|
37
|
+
for (let i = 0; i < sys.circuits.length; i++) {
|
|
38
|
+
const circ = sys.circuits.getItemByIndex(i);
|
|
39
|
+
if (!circ || !circ.isActive) continue;
|
|
40
|
+
if (typeof excludeCircuitId === 'number' && circ.id === excludeCircuitId) continue;
|
|
41
|
+
// `targetId` may be undefined on circuits that haven't learned it yet.
|
|
42
|
+
if (typeof (circ as any).targetId === 'number' && (circ as any).targetId === targetId) return circ;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export class EquipmentStateMessage {
|
|
48
|
+
// Called when Action 2 status shows a circuit state change
|
|
49
|
+
// Checks if there's a pending Wireless command that matches
|
|
50
|
+
public static checkPendingAction184Learning(circuitId: number, isOn: boolean): void {
|
|
51
|
+
const now = Date.now();
|
|
52
|
+
const targetState = isOn ? 1 : 0;
|
|
53
|
+
|
|
54
|
+
// Clean up expired entries
|
|
55
|
+
for (const [targetId, entry] of pendingAction184Commands.entries()) {
|
|
56
|
+
if (now - entry.timestamp > PENDING_COMMAND_TTL_MS) {
|
|
57
|
+
pendingAction184Commands.delete(targetId);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Look for pending commands that match this state
|
|
62
|
+
for (const [targetId, entry] of pendingAction184Commands.entries()) {
|
|
63
|
+
if (entry.state === targetState) {
|
|
64
|
+
// This pending command matches the state change
|
|
65
|
+
// Check if this circuit needs a targetId
|
|
66
|
+
const circ = sys.circuits.getItemById(circuitId, false);
|
|
67
|
+
if (circ && circ.isActive && (typeof circ.targetId === 'undefined' || circ.targetId === 0)) {
|
|
68
|
+
const owner = findActiveCircuitTargetIdOwner(targetId, circuitId);
|
|
69
|
+
if (owner) {
|
|
70
|
+
logger.warn(
|
|
71
|
+
`v3.004+ Action 184: Refusing to learn duplicate targetId ${targetId} for circuit ${circuitId} (${circ.name || 'unnamed'}); ` +
|
|
72
|
+
`already owned by circuit ${owner.id} (${owner.name || 'unnamed'}).`
|
|
73
|
+
);
|
|
74
|
+
pendingAction184Commands.delete(targetId);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
logger.debug(`v3.004+ Action 184: Learned Target ID ${targetId} for circuit ${circuitId} (${circ.name || 'unnamed'}) [Wireless command correlation]`);
|
|
78
|
+
circ.targetId = targetId;
|
|
79
|
+
pendingAction184Commands.delete(targetId);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
private static initIntelliCenter(msg: Inbound) {
|
|
86
|
+
sys.controllerType = ControllerType.IntelliCenter;
|
|
87
|
+
sys.equipment.maxSchedules = 100;
|
|
88
|
+
sys.equipment.maxFeatures = 32;
|
|
89
|
+
// Always get equipment since this is volatile between loads. Everything else takes care of itself.
|
|
90
|
+
sys.configVersion.equipment = 0;
|
|
91
|
+
}
|
|
92
|
+
public static initDefaults() {
|
|
93
|
+
// defaults; set to lowest possible values. Each *Touch will extend this once we know the model.
|
|
94
|
+
sys.equipment.maxBodies = 1;
|
|
95
|
+
sys.equipment.maxCircuits = 6;
|
|
96
|
+
sys.equipment.maxSchedules = 12;
|
|
97
|
+
sys.equipment.maxPumps = 2;
|
|
98
|
+
sys.equipment.maxSchedules = 12;
|
|
99
|
+
sys.equipment.maxValves = 2;
|
|
100
|
+
sys.equipment.maxCircuitGroups = 0;
|
|
101
|
+
sys.equipment.maxLightGroups = 1;
|
|
102
|
+
sys.equipment.maxIntelliBrites = 8;
|
|
103
|
+
sys.equipment.maxChemControllers = sys.equipment.maxChlorinators = 1;
|
|
104
|
+
sys.equipment.maxCustomNames = 10;
|
|
105
|
+
sys.equipment.maxChemControllers = 4;
|
|
106
|
+
sys.equipment.maxFeatures = 8;
|
|
107
|
+
sys.equipment.model = 'Unknown';
|
|
108
|
+
}
|
|
109
|
+
private static initTouch(msg: Inbound) {
|
|
110
|
+
let model1 = msg.extractPayloadByte(27);
|
|
111
|
+
let model2 = msg.extractPayloadByte(28);
|
|
112
|
+
switch (model2) {
|
|
113
|
+
case 0:
|
|
114
|
+
case 1:
|
|
115
|
+
case 2:
|
|
116
|
+
case 3:
|
|
117
|
+
case 4:
|
|
118
|
+
case 5:
|
|
119
|
+
logger.info(`Found IntelliTouch Controller`);
|
|
120
|
+
sys.controllerType = ControllerType.IntelliTouch;
|
|
121
|
+
model1 = msg.extractPayloadByte(28);
|
|
122
|
+
model2 = msg.extractPayloadByte(9);
|
|
123
|
+
(sys.board as IntelliTouchBoard).initExpansionModules(model1, model2);
|
|
124
|
+
break;
|
|
125
|
+
case 11:
|
|
126
|
+
logger.info(`Found SunTouch Controller`);
|
|
127
|
+
sys.controllerType = ControllerType.SunTouch;
|
|
128
|
+
(sys.board as SunTouchBoard).initExpansionModules(model1, model2);
|
|
129
|
+
break;
|
|
130
|
+
case 13:
|
|
131
|
+
case 14:
|
|
132
|
+
logger.info(`Found EasyTouch Controller`);
|
|
133
|
+
sys.controllerType = ControllerType.EasyTouch;
|
|
134
|
+
(sys.board as EasyTouchBoard).initExpansionModules(model1, model2);
|
|
135
|
+
break;
|
|
136
|
+
default:
|
|
137
|
+
logger.error(`Unknown Touch Controller ${msg.extractPayloadByte(28)}:${msg.extractPayloadByte(27)}`);
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
private static initController(msg: Inbound) {
|
|
142
|
+
const model1 = msg.extractPayloadByte(27);
|
|
143
|
+
const model2 = msg.extractPayloadByte(28);
|
|
144
|
+
// RKS: 06-15-20 -- While this works for now the way we are detecting seems a bit dubious. First, the 2 status message
|
|
145
|
+
// contains two model bytes. Right now the ones witness in the wild include 23 = fw1.023, 40 = fw1.040, 47 = fw1.047.
|
|
146
|
+
// RKS: 07-21-22 -- Pentair is about to release fw1.232. Unfortunately, the byte mapping for this has changed such that
|
|
147
|
+
// the bytes [27,28] are [0,2] respectively. This looks like it might be in conflict with IntelliTouch but it is not. Below
|
|
148
|
+
// are the combinations of 27,28 we have seen for IntelliTouch
|
|
149
|
+
// [1,0] = i5+3
|
|
150
|
+
// [0,1] = i7+3
|
|
151
|
+
// [1,3] = i5+3s
|
|
152
|
+
// [1,4] = i9+3s
|
|
153
|
+
// [1,5] = i10+3d
|
|
154
|
+
// IntelliCenter v3.004 reports [3,2] for bytes [27,28]
|
|
155
|
+
if ((model2 === 0 && (model1 === 23 || model1 >= 40)) ||
|
|
156
|
+
(model2 === 2 && model1 == 0) ||
|
|
157
|
+
(model2 === 2 && model1 == 3)) {
|
|
158
|
+
state.equipment.controllerType = 'intellicenter';
|
|
159
|
+
sys.board.modulesAcquired = false;
|
|
160
|
+
sys.controllerType = ControllerType.IntelliCenter;
|
|
161
|
+
logger.info(`Found Controller Board ${state.equipment.model || 'IntelliCenter'}, awaiting installed modules.`);
|
|
162
|
+
EquipmentStateMessage.initIntelliCenter(msg);
|
|
163
|
+
}
|
|
164
|
+
else {
|
|
165
|
+
EquipmentStateMessage.initTouch(msg);
|
|
166
|
+
sys.board.needsConfigChanges = true;
|
|
167
|
+
setTimeout(function () { sys.checkConfiguration(); }, 300);
|
|
168
|
+
}
|
|
169
|
+
// Set status = 1 AFTER controllerType change, because the controllerType setter
|
|
170
|
+
// resets state.status = 0 during RESETTING DATA
|
|
171
|
+
state.status = 1;
|
|
172
|
+
}
|
|
173
|
+
public static process(msg: Inbound) {
|
|
174
|
+
Message.headerSubByte = msg.header[1];
|
|
175
|
+
//console.log(process.memoryUsage());
|
|
176
|
+
if (msg.action === 2 && state.isInitialized && sys.controllerType === ControllerType.Nixie) {
|
|
177
|
+
// Start over because we didn't have communication before but we now do.
|
|
178
|
+
// Close the nixie board first, then initialize with the new controller type.
|
|
179
|
+
// Fix: Call initController AFTER the async close completes to avoid race condition.
|
|
180
|
+
(async () => {
|
|
181
|
+
await sys.board.closeAsync();
|
|
182
|
+
logger.info(`Closed ${sys.controllerType} board`);
|
|
183
|
+
sys.controllerType = ControllerType.Unknown;
|
|
184
|
+
state.status = 0;
|
|
185
|
+
// Now initialize the correct controller type after nixie is closed
|
|
186
|
+
EquipmentStateMessage.initController(msg);
|
|
187
|
+
})();
|
|
188
|
+
return; // Don't continue processing until async close/init completes
|
|
189
|
+
}
|
|
190
|
+
// If controller type is unknown (e.g., after a replay/system reset), we must re-detect the controller on Action 2
|
|
191
|
+
// even if state has been initialized from disk.
|
|
192
|
+
if (!state.isInitialized || sys.controllerType === ControllerType.Unknown) {
|
|
193
|
+
msg.isProcessed = true;
|
|
194
|
+
if (msg.action === 2) EquipmentStateMessage.initController(msg);
|
|
195
|
+
else return;
|
|
196
|
+
}
|
|
197
|
+
else if (!sys.board.modulesAcquired) {
|
|
198
|
+
msg.isProcessed = true;
|
|
199
|
+
if (msg.action === 204) {
|
|
200
|
+
// We have determined that the 204 message now contains the information
|
|
201
|
+
// related to the installed expansion boards.
|
|
202
|
+
logger.info(`INTELLICENTER MODULES DETECTED, REQUESTING STATUS!`);
|
|
203
|
+
|
|
204
|
+
// IMPORTANT: v3 module decoding depends on `sys.equipment.isIntellicenterV3`, which is gated by controller firmware.
|
|
205
|
+
// During the "modules not acquired yet" bootstrap we must set firmware BEFORE calling `initExpansionModules()`
|
|
206
|
+
// so v3 systems (e.g. i10PS shared) are decoded with the correct nibble order.
|
|
207
|
+
if (msg.payload.length >= 44) {
|
|
208
|
+
sys.equipment.controllerFirmware = (msg.extractPayloadByte(42) + (msg.extractPayloadByte(43) / 1000)).toString();
|
|
209
|
+
}
|
|
210
|
+
// Master = 13-14
|
|
211
|
+
// EXP1 = 15-16
|
|
212
|
+
// EXP2 = 17-18
|
|
213
|
+
let pc = msg.extractPayloadByte(40);
|
|
214
|
+
(sys.board as IntelliCenterBoard).initExpansionModules(msg.extractPayloadByte(13), msg.extractPayloadByte(14),
|
|
215
|
+
pc & 0x01 ? msg.extractPayloadByte(15) : 0x00, pc & 0x01 ? msg.extractPayloadByte(16) : 0x00,
|
|
216
|
+
pc & 0x02 ? msg.extractPayloadByte(17) : 0x00, pc & 0x02 ? msg.extractPayloadByte(18) : 0x00,
|
|
217
|
+
pc & 0x04 ? msg.extractPayloadByte(19) : 0x00, pc & 0x04 ? msg.extractPayloadByte(20) : 0x00);
|
|
218
|
+
sys.equipment.setEquipmentIds();
|
|
219
|
+
// v3.004+: As soon as firmware>=3.0 is known, seed known targetIds so the UI has stable defaults
|
|
220
|
+
// before any user toggles occur. These are best-effort and will be overridden by learned values.
|
|
221
|
+
if (sys.equipment.isIntellicenterV3 && typeof (sys.board.circuits as any)?.seedKnownV3TargetIds === 'function') {
|
|
222
|
+
(sys.board.circuits as any).seedKnownV3TargetIds();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
else return;
|
|
226
|
+
}
|
|
227
|
+
switch (msg.action) {
|
|
228
|
+
case 2:
|
|
229
|
+
{
|
|
230
|
+
let fnTempFromByte = function (byte) {
|
|
231
|
+
return byte;
|
|
232
|
+
//return (byte & 0x007F) * (((byte & 0x0080) > 0) ? -1 : 1); // RKS: 09-26-20 Not sure how negative temps are represented but this aint it. Temps > 127 have been witnessed.
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Shared
|
|
236
|
+
let dt = new Date();
|
|
237
|
+
// RKS: This was moved to the ChemControllerState message. This is flawed in that it incorrectly sets IntelliChem to no comms.
|
|
238
|
+
//if (state.chemControllers.length > 0) {
|
|
239
|
+
// // TODO: move this to chemController when we understand the packets better
|
|
240
|
+
// for (let i = 0; i < state.chemControllers.length; i++) {
|
|
241
|
+
// let ccontroller = state.chemControllers.getItemByIndex(i);
|
|
242
|
+
// if (sys.board.valueMaps.chemControllerTypes.getName(ccontroller.type) === 'intellichem') {
|
|
243
|
+
// if (dt.getTime() - ccontroller.lastComm > 60000) ccontroller.status = 1;
|
|
244
|
+
// }
|
|
245
|
+
// }
|
|
246
|
+
//}
|
|
247
|
+
state.time.hours = msg.extractPayloadByte(0);
|
|
248
|
+
state.time.minutes = msg.extractPayloadByte(1);
|
|
249
|
+
state.time.seconds = dt.getSeconds();
|
|
250
|
+
state.mode = sys.controllerType !== ControllerType.IntelliCenter ? (msg.extractPayloadByte(9) & 0x81) : (msg.extractPayloadByte(9) & 0x01);
|
|
251
|
+
|
|
252
|
+
// RKS: The units have been normalized for English and Metric for the overall panel. It is important that the val numbers match for at least the temp units since
|
|
253
|
+
// the only unit of measure native to the Touch controllers is temperature they chose to name these C or F. However, with the njsPC extensions this is non-semantic
|
|
254
|
+
// since pressure, volume, and length have been introduced.
|
|
255
|
+
sys.general.options.units = state.temps.units = msg.extractPayloadByte(9) & 0x04;
|
|
256
|
+
state.valve = msg.extractPayloadByte(10);
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
// RSG - added 7/8/2020
|
|
260
|
+
// Every 30 mins, check the timezone and adjust DST settings
|
|
261
|
+
if (dt.getMinutes() % 30 === 0) {
|
|
262
|
+
sys.board.system.setTZ();
|
|
263
|
+
sys.board.schedules.updateSunriseSunsetAsync().then((updated: boolean)=>{
|
|
264
|
+
if (updated) {logger.debug(`Sunrise/sunset times updated on schedules.`);}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
// Check and update clock when it is off by >5 mins (just for a small buffer) and:
|
|
268
|
+
// 1. IntelliCenter has "manual" time set (Internet will automatically adjust) and autoAdjustDST is enabled
|
|
269
|
+
// 2. *Touch is "manual" (only option) and autoAdjustDST is enabled - (same as #1)
|
|
270
|
+
// 3. clock source is "server" isn't an OCP option but can be enabled on the clients
|
|
271
|
+
if (dt.getMinutes() % 5 === 0 && dt.getSeconds() <= 10 && sys.general.options.clockSource === 'server') {
|
|
272
|
+
if ((Math.abs(dt.getTime() - state.time.getTime()) > 60 * 2 * 1000) && !state.time.isUpdating) {
|
|
273
|
+
state.time.isUpdating = true;
|
|
274
|
+
sys.board.system.setDateTimeAsync({ dt, dst: sys.general.options.adjustDST || 0, })
|
|
275
|
+
.then(() => {
|
|
276
|
+
logger.info(`njsPC automatically updated OCP time. You're welcome.`);
|
|
277
|
+
})
|
|
278
|
+
.catch((err) => {
|
|
279
|
+
logger.error(`Error automatically setting system time. ${JSON.stringify(err)}`)
|
|
280
|
+
})
|
|
281
|
+
.finally(() => {
|
|
282
|
+
state.time.isUpdating = false;
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
state.delay = msg.extractPayloadByte(12) & 63; // not sure what 64 val represents
|
|
287
|
+
state.freeze = (msg.extractPayloadByte(9) & 0x08) === 0x08;
|
|
288
|
+
if (sys.controllerType === ControllerType.IntelliCenter) {
|
|
289
|
+
state.temps.waterSensor1 = fnTempFromByte(msg.extractPayloadByte(14));
|
|
290
|
+
// IntelliCenter: for 2-body non-shared systems, byte(15) is Body2 (Spa) water sensor.
|
|
291
|
+
// Previously gated behind (>2 bodies || dual), which left Spa temp undefined and rendered as "--" in dashPanel.
|
|
292
|
+
if (sys.bodies.length > 1 || sys.equipment.dual) state.temps.waterSensor2 = fnTempFromByte(msg.extractPayloadByte(15));
|
|
293
|
+
// We are making an assumption here in that the circuits are always labeled the same.
|
|
294
|
+
// 1=Spa/Body2
|
|
295
|
+
// 6=Pool/Body1
|
|
296
|
+
// 12=Body3
|
|
297
|
+
// 22=Body4 -- Really not sure about this one.
|
|
298
|
+
if (sys.bodies.length > 0) {
|
|
299
|
+
// We will not go in here if this is not a shared body.
|
|
300
|
+
const tbody: BodyTempState = state.temps.bodies.getItemById(1, true);
|
|
301
|
+
const cbody: Body = sys.bodies.getItemById(1);
|
|
302
|
+
tbody.heatMode = cbody.heatMode;
|
|
303
|
+
tbody.setPoint = cbody.setPoint;
|
|
304
|
+
tbody.name = cbody.name;
|
|
305
|
+
tbody.circuit = cbody.circuit = 6;
|
|
306
|
+
tbody.heatStatus = msg.extractPayloadByte(11) & 0x0F;
|
|
307
|
+
// With the IntelliCenter i10D, bit 6 is not reliable. It is not set properly and requires the 204 message
|
|
308
|
+
// to process the data.
|
|
309
|
+
if (!sys.equipment.dual) {
|
|
310
|
+
if ((msg.extractPayloadByte(2) & 0x20) === 32) {
|
|
311
|
+
tbody.temp = state.temps.waterSensor1;
|
|
312
|
+
tbody.isOn = true;
|
|
313
|
+
} else tbody.isOn = false;
|
|
314
|
+
}
|
|
315
|
+
else if (state.circuits.getItemById(6).isOn === true) {
|
|
316
|
+
tbody.temp = state.temps.waterSensor1;
|
|
317
|
+
tbody.isOn = true;
|
|
318
|
+
}
|
|
319
|
+
else tbody.isOn = false;
|
|
320
|
+
}
|
|
321
|
+
if (sys.bodies.length > 1) {
|
|
322
|
+
const tbody: BodyTempState = state.temps.bodies.getItemById(2, true);
|
|
323
|
+
const cbody: Body = sys.bodies.getItemById(2);
|
|
324
|
+
tbody.heatMode = cbody.heatMode;
|
|
325
|
+
tbody.setPoint = cbody.setPoint;
|
|
326
|
+
tbody.name = cbody.name;
|
|
327
|
+
tbody.circuit = cbody.circuit = 1;
|
|
328
|
+
tbody.heatStatus = (msg.extractPayloadByte(11) & 0xF0) >> 4;
|
|
329
|
+
if (!sys.equipment.dual) {
|
|
330
|
+
if ((msg.extractPayloadByte(2) & 0x01) === 1) {
|
|
331
|
+
tbody.temp = sys.equipment.shared ? state.temps.waterSensor1 : state.temps.waterSensor2;
|
|
332
|
+
tbody.isOn = true;
|
|
333
|
+
} else tbody.isOn = false;
|
|
334
|
+
} else if (state.circuits.getItemById(1).isOn === true) {
|
|
335
|
+
tbody.temp = sys.equipment.shared ? state.temps.waterSensor1 : state.temps.waterSensor2;
|
|
336
|
+
tbody.isOn = true;
|
|
337
|
+
}
|
|
338
|
+
else tbody.isOn = false;
|
|
339
|
+
}
|
|
340
|
+
if (sys.bodies.length > 2) {
|
|
341
|
+
state.temps.waterSensor3 = fnTempFromByte(msg.extractPayloadByte(20));
|
|
342
|
+
// const tbody: BodyTempState = state.temps.bodies.getItemById(10, true);
|
|
343
|
+
const tbody: BodyTempState = state.temps.bodies.getItemById(3, true);
|
|
344
|
+
const cbody: Body = sys.bodies.getItemById(3);
|
|
345
|
+
tbody.name = cbody.name;
|
|
346
|
+
tbody.heatMode = cbody.heatMode;
|
|
347
|
+
tbody.setPoint = cbody.setPoint;
|
|
348
|
+
tbody.heatStatus = msg.extractPayloadByte(11) & 0x0F;
|
|
349
|
+
tbody.circuit = cbody.circuit = 12;
|
|
350
|
+
if ((msg.extractPayloadByte(3) & 0x08) === 8) {
|
|
351
|
+
// This is the first circuit on the second body.
|
|
352
|
+
tbody.temp = state.temps.waterSensor3;
|
|
353
|
+
tbody.isOn = true;
|
|
354
|
+
} else tbody.isOn = false;
|
|
355
|
+
}
|
|
356
|
+
if (sys.bodies.length > 3) {
|
|
357
|
+
state.temps.waterSensor4 = fnTempFromByte(msg.extractPayloadByte(21));
|
|
358
|
+
// const tbody: BodyTempState = state.temps.bodies.getItemById(19, true);
|
|
359
|
+
const tbody: BodyTempState = state.temps.bodies.getItemById(4, true);
|
|
360
|
+
const cbody: Body = sys.bodies.getItemById(4);
|
|
361
|
+
tbody.name = cbody.name;
|
|
362
|
+
tbody.heatMode = cbody.heatMode;
|
|
363
|
+
tbody.setPoint = cbody.setPoint;
|
|
364
|
+
tbody.heatStatus = (msg.extractPayloadByte(11) & 0xF0) >> 4;
|
|
365
|
+
tbody.circuit = cbody.circuit = 22;
|
|
366
|
+
if ((msg.extractPayloadByte(5) & 0x20) === 32) {
|
|
367
|
+
// This is the first circuit on the third body or the first circuit on the second expansion.
|
|
368
|
+
tbody.temp = state.temps.waterSensor2;
|
|
369
|
+
tbody.isOn = true;
|
|
370
|
+
} else tbody.isOn = false;
|
|
371
|
+
}
|
|
372
|
+
state.temps.air = fnTempFromByte(msg.extractPayloadByte(18)); // 18
|
|
373
|
+
state.temps.solarSensor1 = fnTempFromByte(msg.extractPayloadByte(19)); // 19
|
|
374
|
+
if (sys.bodies.length > 2 || sys.equipment.dual)
|
|
375
|
+
state.temps.solarSensor2 = fnTempFromByte(msg.extractPayloadByte(17));
|
|
376
|
+
if ((sys.bodies.length > 2))
|
|
377
|
+
state.temps.solarSensor3 = fnTempFromByte(msg.extractPayloadByte(22));
|
|
378
|
+
if ((sys.bodies.length > 3))
|
|
379
|
+
state.temps.solarSensor4 = fnTempFromByte(msg.extractPayloadByte(23));
|
|
380
|
+
|
|
381
|
+
if (sys.general.options.clockSource !== 'server' || typeof sys.general.options.adjustDST === 'undefined') sys.general.options.adjustDST = (msg.extractPayloadByte(23) & 0x01) === 0x0; //23
|
|
382
|
+
}
|
|
383
|
+
else {
|
|
384
|
+
state.temps.waterSensor1 = fnTempFromByte(msg.extractPayloadByte(14));
|
|
385
|
+
state.temps.air = fnTempFromByte(msg.extractPayloadByte(18));
|
|
386
|
+
let solar: Heater = sys.heaters.getItemById(2);
|
|
387
|
+
if (solar.isActive) state.temps.solar = fnTempFromByte(msg.extractPayloadByte(19));
|
|
388
|
+
//[15, 34, 32, 0, 0, 0, 0, 0, 0, 0, 83, 0, 0, 0, 81, 81, 32, 91, 82, 91, 0, 0, 7, 4, 0, 77, 163, 1, 0][4, 78]
|
|
389
|
+
// byte | val |
|
|
390
|
+
// 0 | 15 | Hours
|
|
391
|
+
// 1 | 34 | Minutes
|
|
392
|
+
// 2 | 32 | Circuits 1-8 bit 6 = Pool on.
|
|
393
|
+
// 3 | 0 | Circuits 9-16
|
|
394
|
+
// 4 | 0 | Circuits 17-24
|
|
395
|
+
// 5 | 0 | Circuits 24-32
|
|
396
|
+
// 6 | 0 | Circuits 33-40
|
|
397
|
+
// 7 | 0 | Unknown
|
|
398
|
+
// 8 | 0 | Unknown
|
|
399
|
+
// 9 | 0 | Panel Mode bit flags
|
|
400
|
+
// 10 | 83 | Heat status for body 1 & 2 (This says solar is on for the pool and spa because this is the body that is running)
|
|
401
|
+
// 11 | 0 | Unknown (This could be the heat status for body 3 & 4)
|
|
402
|
+
// 12 | 0 | Unknown
|
|
403
|
+
// 13 | 0 | Unknown
|
|
404
|
+
// 14 | 81 | Water sensor 1 temperature
|
|
405
|
+
// 15 | 81 | Water sensor 2 temperature (This mirrors water sensor 1 in shared system)
|
|
406
|
+
// 16 | 32 | Unknown
|
|
407
|
+
// 17 | 91 | Solar sensor 1 temperature
|
|
408
|
+
// 18 | 82 | Air temp
|
|
409
|
+
// 19 | 91 | Solar sensor 2 temperature (this mirrors solar sensor 1 in shared system)
|
|
410
|
+
// 20 | 0 | Unknown (this could be water sensor 3)
|
|
411
|
+
// 21 | 0 | Unknown (this could be water sensor 4)
|
|
412
|
+
// 22 | 7 | Body 1 & 2 heat mode (body 1 = Solar Only body 2 = Heater)
|
|
413
|
+
// 23 | 4 | Body 3 & 4 heat mode
|
|
414
|
+
// 24 | 0 | Unknown
|
|
415
|
+
// 25 | 77 | Unknown
|
|
416
|
+
// 26 | 163 | Unknown
|
|
417
|
+
// 27 | 1 | Byte 2 of OCP identifier
|
|
418
|
+
// 28 | 0 | Byte 1 of OCP identifier
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
// Heat Modes
|
|
422
|
+
// 1 = Heater
|
|
423
|
+
// 2 = Solar Preferred
|
|
424
|
+
// 3 = Solar Only
|
|
425
|
+
|
|
426
|
+
// Heat Status
|
|
427
|
+
// 0 = Off
|
|
428
|
+
// 1 = Heater
|
|
429
|
+
// 2 = Cooling
|
|
430
|
+
// 3 = Solar/Heat Pump
|
|
431
|
+
|
|
432
|
+
// Pool Heat Mode/Status.
|
|
433
|
+
// When temp setpoint and pool in heater mode went above the current pool temp byte 10 went from 67 to 71. The upper two bits of the
|
|
434
|
+
// lower nibble changed on bit 3. So 0100 0111 from 0100 0011
|
|
435
|
+
|
|
436
|
+
// Spa Heat Mode/Status
|
|
437
|
+
// When switching from pool to spa with both heat modes set to off byte 10 went from 67 to 75 and byte(16) changed from 0 to 32. The upper two bits of the lower nibble
|
|
438
|
+
// changed on byte(10) bit 4. So to 0100 1011 from 0100 0011. Interestingly this seems to indicate that the spa turned on. This almost appears as if the heater engaged
|
|
439
|
+
// automatically like the spa has manual heat turned off.
|
|
440
|
+
// When the heat mode was changed to solar only byte 10 went to 75 from 67 so bit 4 switched off and byte(16) changed to 0. At this point the water temp is 86 and the
|
|
441
|
+
// solar temp is 79 so the solar should not be coming on.
|
|
442
|
+
// When the setpoint was dropped below the water temp bit 5 on byte(10) swiched back off and byte(16) remained at 0. I think there is no bearing here on this.
|
|
443
|
+
// When the heat mode was changed to solar preferred and the setpoint was raised to 104F the heater kicked on and bit 5 changed from 0 to 1. So byte(10) went from
|
|
444
|
+
// 0100 0011 to 0100 1011 this is consistent with the heater coming on for the spa. In this instance byte(16) also changed back to 32 which would be consistent with
|
|
445
|
+
// an OCP where the manual heat was turned off.
|
|
446
|
+
|
|
447
|
+
// RKS: Added check for i10d for water sensor 2.
|
|
448
|
+
if (sys.bodies.length > 2 || sys.equipment.dual) state.temps.waterSensor2 = fnTempFromByte(msg.extractPayloadByte(15));
|
|
449
|
+
if (sys.bodies.length > 0) {
|
|
450
|
+
// const tbody: BodyTempState = state.temps.bodies.getItemById(6, true);
|
|
451
|
+
const tbody: BodyTempState = state.temps.bodies.getItemById(1, true);
|
|
452
|
+
const cbody: Body = sys.bodies.getItemById(1);
|
|
453
|
+
if ((msg.extractPayloadByte(2) & 0x20) === 32) {
|
|
454
|
+
tbody.temp = state.temps.waterSensor1;
|
|
455
|
+
tbody.isOn = true;
|
|
456
|
+
} else tbody.isOn = false;
|
|
457
|
+
tbody.setPoint = cbody.setPoint;
|
|
458
|
+
tbody.name = cbody.name;
|
|
459
|
+
tbody.circuit = cbody.circuit = 6;
|
|
460
|
+
|
|
461
|
+
//RKS: This heat mode did not include all the bits necessary for hybrid heaters
|
|
462
|
+
//tbody.heatMode = cbody.heatMode = msg.extractPayloadByte(22) & 0x03;
|
|
463
|
+
tbody.heatMode = cbody.heatMode = msg.extractPayloadByte(22) & 0x33;
|
|
464
|
+
let heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
|
|
465
|
+
if (tbody.isOn) {
|
|
466
|
+
if (tbody.heaterOptions.hybrid > 0) {
|
|
467
|
+
// ETi When heating with
|
|
468
|
+
// Heatpump (1) = 12 H:true S:false C:false
|
|
469
|
+
// Gas (2) = 48 H:false S:true C:false
|
|
470
|
+
// Hybrid (3) = 48 H:true S:false C:false
|
|
471
|
+
// Dual (16) = 60 H:true S:true C:false
|
|
472
|
+
// What this means is that Touch actually treats the heat status as either heating with
|
|
473
|
+
// the primary heater for the body or the secondary. In the case of a hybrid heater
|
|
474
|
+
// the primary is a heatpump and the secondary is gas. In the case of gas + solar or gas + heatpump
|
|
475
|
+
// the gas heater is the primary and solar or heatpump is the secondary. So we need to dance a little bit
|
|
476
|
+
// here. We do this by checking the heater options.
|
|
477
|
+
if (tbody.heatMode > 0) { // Turns out that ET sometimes reports the last heat status when off.
|
|
478
|
+
// This can be the only heater solar cannot be installed with this.
|
|
479
|
+
let byte = msg.extractPayloadByte(10);
|
|
480
|
+
// Either the primary, secondary, or both is engaged.
|
|
481
|
+
if ((byte & 0x14) === 0x14) heatStatus = sys.board.valueMaps.heatStatus.getValue('dual');
|
|
482
|
+
// else if ((byte & 0x0c) === 0x0c) heatStatus = sys.board.valueMaps.heatStatus.getValue('off'); // don't need since we test for heatMode>0
|
|
483
|
+
else if (byte & 0x10) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
484
|
+
else if (byte & 0x04) heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
else {
|
|
488
|
+
//const heaterActive = (msg.extractPayloadByte(10) & 0x0C) === 12;
|
|
489
|
+
//const solarActive = (msg.extractPayloadByte(10) & 0x30) === 48;
|
|
490
|
+
const heaterActive = (msg.extractPayloadByte(10) & 0x04) === 0x04;
|
|
491
|
+
const solarActive = (msg.extractPayloadByte(10) & 0x10) === 0x10;
|
|
492
|
+
const cooling = solarActive && tbody.temp > tbody.setPoint;
|
|
493
|
+
if (heaterActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
494
|
+
if (cooling) heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
|
|
495
|
+
else if (solarActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
tbody.heatStatus = heatStatus;
|
|
499
|
+
sys.board.schedules.syncScheduleHeatSourceAndSetpoint(cbody, tbody);
|
|
500
|
+
}
|
|
501
|
+
if (sys.bodies.length > 1) {
|
|
502
|
+
// const tbody: BodyTempState = state.temps.bodies.getItemById(1, true);
|
|
503
|
+
const tbody: BodyTempState = state.temps.bodies.getItemById(2, true);
|
|
504
|
+
const cbody: Body = sys.bodies.getItemById(2);
|
|
505
|
+
if ((msg.extractPayloadByte(2) & 0x01) === 1) {
|
|
506
|
+
tbody.temp = sys.equipment.shared ? state.temps.waterSensor1 : state.temps.waterSensor2;
|
|
507
|
+
tbody.isOn = true;
|
|
508
|
+
} else tbody.isOn = false;
|
|
509
|
+
//RKS: This heat mode did not include all the bits necessary for hybrid heaters
|
|
510
|
+
//tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(22) & 0x0C) >> 2;
|
|
511
|
+
tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(22) & 0xCC) >> 2;
|
|
512
|
+
tbody.setPoint = cbody.setPoint;
|
|
513
|
+
tbody.name = cbody.name;
|
|
514
|
+
tbody.circuit = cbody.circuit = 1;
|
|
515
|
+
let heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
|
|
516
|
+
if (tbody.isOn) {
|
|
517
|
+
if (tbody.heaterOptions.hybrid > 0) {
|
|
518
|
+
// This can be the only heater solar cannot be installed with this.
|
|
519
|
+
if (tbody.heatMode > 0) {
|
|
520
|
+
let byte = msg.extractPayloadByte(10);
|
|
521
|
+
// Either the primary, secondary, or both is engaged.
|
|
522
|
+
if ((byte & 0x28) === 0x28) heatStatus = sys.board.valueMaps.heatStatus.getValue('dual');
|
|
523
|
+
else if (byte & 0x20) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
524
|
+
else if (byte & 0x08) heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
else {
|
|
528
|
+
//const heaterActive = (msg.extractPayloadByte(10) & 0x0C) === 12;
|
|
529
|
+
//const solarActive = (msg.extractPayloadByte(10) & 0x30) === 48;
|
|
530
|
+
const heaterActive = (msg.extractPayloadByte(10) & 0x08) === 0x08;
|
|
531
|
+
const solarActive = (msg.extractPayloadByte(10) & 0x20) === 0x20;
|
|
532
|
+
const cooling = solarActive && tbody.temp > tbody.setPoint;
|
|
533
|
+
if (heaterActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
534
|
+
if (cooling) heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
|
|
535
|
+
else if (solarActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
tbody.heatStatus = heatStatus;
|
|
539
|
+
sys.board.schedules.syncScheduleHeatSourceAndSetpoint(cbody, tbody);
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
switch (sys.controllerType) {
|
|
543
|
+
case ControllerType.IntelliCenter:
|
|
544
|
+
{
|
|
545
|
+
EquipmentStateMessage.processCircuitState(msg);
|
|
546
|
+
// v3.004+: DISABLED - Action 2 bytes 7-8 use non-bitmask encoding
|
|
547
|
+
// Feature state for v3.004+ comes from Action 30 case 15 responses only
|
|
548
|
+
// See: https://github.com/tagyoureit/nodejs-poolController/issues/XXX
|
|
549
|
+
// if (sys.equipment.isIntellicenterV3) {
|
|
550
|
+
// EquipmentStateMessage.processFeatureStateV3(msg);
|
|
551
|
+
// }
|
|
552
|
+
sys.board.circuits.syncCircuitRelayStates();
|
|
553
|
+
sys.board.circuits.syncVirtualCircuitStates();
|
|
554
|
+
sys.board.valves.syncValveStates();
|
|
555
|
+
sys.board.filters.syncFilterStates();
|
|
556
|
+
state.emitControllerChange();
|
|
557
|
+
state.emitEquipmentChanges();
|
|
558
|
+
sys.board.heaters.syncHeaterStates();
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
case ControllerType.SunTouch:
|
|
562
|
+
EquipmentStateMessage.processSunTouchCircuits(msg);
|
|
563
|
+
sys.board.circuits.syncCircuitRelayStates();
|
|
564
|
+
sys.board.features.syncGroupStates();
|
|
565
|
+
sys.board.circuits.syncVirtualCircuitStates();
|
|
566
|
+
sys.board.valves.syncValveStates();
|
|
567
|
+
sys.board.filters.syncFilterStates();
|
|
568
|
+
state.emitControllerChange();
|
|
569
|
+
state.emitEquipmentChanges();
|
|
570
|
+
sys.board.heaters.syncHeaterStates();
|
|
571
|
+
sys.board.schedules.syncScheduleStates();
|
|
572
|
+
break;
|
|
573
|
+
case ControllerType.EasyTouch:
|
|
574
|
+
case ControllerType.IntelliCom:
|
|
575
|
+
case ControllerType.IntelliTouch:
|
|
576
|
+
{
|
|
577
|
+
EquipmentStateMessage.processTouchCircuits(msg);
|
|
578
|
+
// This will toggle the group states depending on the state of the individual circuits.
|
|
579
|
+
sys.board.circuits.syncCircuitRelayStates();
|
|
580
|
+
sys.board.features.syncGroupStates();
|
|
581
|
+
sys.board.circuits.syncVirtualCircuitStates();
|
|
582
|
+
sys.board.valves.syncValveStates();
|
|
583
|
+
sys.board.filters.syncFilterStates();
|
|
584
|
+
state.emitControllerChange();
|
|
585
|
+
state.emitEquipmentChanges();
|
|
586
|
+
sys.board.heaters.syncHeaterStates();
|
|
587
|
+
sys.board.schedules.syncScheduleStates();
|
|
588
|
+
break;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
break;
|
|
593
|
+
case 5: // Intellitouch only. Date/Time packet
|
|
594
|
+
// [255,0,255][165,1,15,16,5,8][15,10,8,1,8,18,0,1][1,15]
|
|
595
|
+
state.time.hours = msg.extractPayloadByte(0);
|
|
596
|
+
state.time.minutes = msg.extractPayloadByte(1);
|
|
597
|
+
// state.time.dayOfWeek = msg.extractPayloadByte(2);
|
|
598
|
+
state.time.date = msg.extractPayloadByte(3);
|
|
599
|
+
state.time.month = msg.extractPayloadByte(4);
|
|
600
|
+
state.time.year = msg.extractPayloadByte(5);
|
|
601
|
+
if (sys.general.options.clockSource !== 'server' || typeof sys.general.options.adjustDST === 'undefined') sys.general.options.adjustDST = msg.extractPayloadByte(7) === 0x01;
|
|
602
|
+
setTimeout(function () { sys.board.checkConfiguration(); }, 100);
|
|
603
|
+
msg.isProcessed = true;
|
|
604
|
+
break;
|
|
605
|
+
case 8: {
|
|
606
|
+
// IntelliTouch only. Heat status
|
|
607
|
+
// [165,x,15,16,8,13],[75,75,64,87,101,11,0, 0 ,62 ,0 ,0 ,0 ,0] ,[2,190]
|
|
608
|
+
// Heat Modes
|
|
609
|
+
// 1 = Heater
|
|
610
|
+
// 2 = Solar Preferred
|
|
611
|
+
// 3 = Solar Only
|
|
612
|
+
//[81, 81, 82, 85, 97, 7, 0, 0, 0, 100, 100, 4, 0][3, 87]
|
|
613
|
+
// byte | val |
|
|
614
|
+
// 0 | 81 | Water sensor 1
|
|
615
|
+
// 1 | 81 | Unknown (Probably water sensor 2 on a D)
|
|
616
|
+
// 2 | 82 | Air sensor
|
|
617
|
+
// 3 | 85 | Body 1 setpoint
|
|
618
|
+
// 4 | 97 | Body 2 setpoint
|
|
619
|
+
// 5 | 7 | Body 1 & 2 heat mode. (0111) (Pool = 11 Solar only/Spa = 01 Heater)
|
|
620
|
+
// 6 | 0 | Unknown (Water Sensor 3)
|
|
621
|
+
// 7 | 0 | Unknown (Water Sensor 4)
|
|
622
|
+
// 8 | 0 | Unknown -- Reserved air sensor
|
|
623
|
+
// 9 | 100 | Unknown (Body 3 setpoint)
|
|
624
|
+
// 10 | 100 | Unknown (Body 4 setpoint)
|
|
625
|
+
// 11 | 4 | Unknown (Body 3 & 4 head mode. (0010) (Pool = 00 = Off/ 10 = Solar Preferred)
|
|
626
|
+
// 12 | 0 | Unknown
|
|
627
|
+
// There are two messages sent when the OCP tries to tse a heat mode in IntelliTouch. The first one on the action 136 is for the first 2 bodies and the second
|
|
628
|
+
// is for the remaining 2 bodies. The second half of this message mirrors the values for the second 136 message.
|
|
629
|
+
// [255, 0, 255][165, 1, 16, 32, 136, 4][100, 100, 4, 1][2, 47]
|
|
630
|
+
state.temps.waterSensor1 = msg.extractPayloadByte(0);
|
|
631
|
+
state.temps.air = msg.extractPayloadByte(2);
|
|
632
|
+
let solar: Heater = sys.heaters.getItemById(2);
|
|
633
|
+
// RKS: 05-18-22 - This is not correct the solar temp is not stored on this message. It is always 0
|
|
634
|
+
// on an intelliTouch system with solar.
|
|
635
|
+
//if (solar.isActive) state.temps.solar = msg.extractPayloadByte(8);
|
|
636
|
+
// pool
|
|
637
|
+
let tbody: BodyTempState = state.temps.bodies.getItemById(1, true);
|
|
638
|
+
let cbody: Body = sys.bodies.getItemById(1);
|
|
639
|
+
// RKS: 02-26-22 - See communications doc for explanation of bits. This needs to support UltraTemp ETi heatpumps.
|
|
640
|
+
tbody.heatMode = cbody.heatMode = msg.extractPayloadByte(5) & 0x33;
|
|
641
|
+
tbody.setPoint = cbody.setPoint = msg.extractPayloadByte(3);
|
|
642
|
+
tbody.coolSetpoint = cbody.coolSetpoint = msg.extractPayloadByte(9);
|
|
643
|
+
if (tbody.isOn) tbody.temp = state.temps.waterSensor1;
|
|
644
|
+
cbody = sys.bodies.getItemById(2);
|
|
645
|
+
if (cbody.isActive) {
|
|
646
|
+
// spa
|
|
647
|
+
tbody = state.temps.bodies.getItemById(2, true);
|
|
648
|
+
tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(5) & 0xCC) >> 2;
|
|
649
|
+
//tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(5) & 12) >> 2;
|
|
650
|
+
tbody.setPoint = cbody.setPoint = msg.extractPayloadByte(4);
|
|
651
|
+
if (tbody.isOn) tbody.temp = state.temps.waterSensor2 = msg.extractPayloadByte(1);
|
|
652
|
+
}
|
|
653
|
+
state.emitEquipmentChanges();
|
|
654
|
+
msg.isProcessed = true;
|
|
655
|
+
break;
|
|
656
|
+
}
|
|
657
|
+
case 96:
|
|
658
|
+
EquipmentStateMessage.processIntelliBriteMode(msg);
|
|
659
|
+
break;
|
|
660
|
+
case 179: {
|
|
661
|
+
// v3.004+ Action 179 - Heartbeat REQUEST from OCP
|
|
662
|
+
// OCP sends Action 179 TO specific device (dest=33 for njsPC, dest=36 for wireless)
|
|
663
|
+
// Device must respond with Action 180 TO OCP (dest=16)
|
|
664
|
+
if (msg.dest === Message.pluginAddress) {
|
|
665
|
+
// OCP is pinging us specifically - respond with Action 180
|
|
666
|
+
logger.silly(`Received heartbeat request (Action 179) from OCP, responding with Action 180`);
|
|
667
|
+
const response: Outbound = Outbound.create({
|
|
668
|
+
dest: 16, // Respond to OCP (16)
|
|
669
|
+
action: 180, // Action 180 = heartbeat response
|
|
670
|
+
payload: Array(16).fill(0), // 16 zeros (observed from wireless remote)
|
|
671
|
+
retries: 0 // Don't retry heartbeat responses
|
|
672
|
+
});
|
|
673
|
+
response.sendAsync().catch(err => {
|
|
674
|
+
// Log but don't fail on heartbeat errors
|
|
675
|
+
logger.silly(`Heartbeat response error: ${err.message}`);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
msg.isProcessed = true;
|
|
679
|
+
break;
|
|
680
|
+
}
|
|
681
|
+
case 184: {
|
|
682
|
+
// v3.004+ Action 184 - Circuit state broadcast / control
|
|
683
|
+
// OCP broadcasts this for circuit state changes AND periodically for status.
|
|
684
|
+
// Wireless remote sends this to control circuits.
|
|
685
|
+
//
|
|
686
|
+
// Payload structure (10 bytes):
|
|
687
|
+
// Bytes 0-1: Channel/context ID (104,143 = default, or circuit-specific like 108,225)
|
|
688
|
+
// Byte 2: Sequence number
|
|
689
|
+
// Byte 3: Format (255 = command, 0 = status)
|
|
690
|
+
// Bytes 4-5: Target ID (unique circuit identifier, e.g., 168,237 = Spa, 108,225 = Pool)
|
|
691
|
+
// Byte 6: State (0=OFF, 1=ON, 255=idle for body status)
|
|
692
|
+
// Bytes 7-9: Additional data (usually 0)
|
|
693
|
+
//
|
|
694
|
+
// KEY PATTERN: When channel ID (bytes 0-1) equals Target ID (bytes 4-5),
|
|
695
|
+
// this identifies a specific circuit (e.g., Pool uses 108,225 for both).
|
|
696
|
+
//
|
|
697
|
+
// Learning strategies:
|
|
698
|
+
// 1. Channel = Target pattern: strong identification
|
|
699
|
+
// 2. State correlation: match broadcast state with circuit states
|
|
700
|
+
// 3. Only ONE circuit matches: definitive mapping
|
|
701
|
+
if (sys.controllerType === ControllerType.IntelliCenter &&
|
|
702
|
+
sys.equipment.isIntellicenterV3 &&
|
|
703
|
+
msg.payload.length >= 10) {
|
|
704
|
+
|
|
705
|
+
const channelIdHi = msg.extractPayloadByte(0);
|
|
706
|
+
const channelIdLo = msg.extractPayloadByte(1);
|
|
707
|
+
const channelId = channelIdHi * 256 + channelIdLo;
|
|
708
|
+
const targetIdHi = msg.extractPayloadByte(4);
|
|
709
|
+
const targetIdLo = msg.extractPayloadByte(5);
|
|
710
|
+
const targetId = targetIdHi * 256 + targetIdLo;
|
|
711
|
+
const circuitState = msg.extractPayloadByte(6);
|
|
712
|
+
|
|
713
|
+
// Process from OCP broadcasts (source=16) AND Wireless commands (source=36)
|
|
714
|
+
// Skip idle status (byte 6 = 255) and body status target (212,182 = 0xD4B6 = 54454)
|
|
715
|
+
// Learning from Wireless: When Wireless sends targetId X to OCP with state Y,
|
|
716
|
+
// and OCP ACKs, we know targetId X controls the circuit that changed to state Y.
|
|
717
|
+
const isFromOCP = msg.source === 16;
|
|
718
|
+
const isFromWireless = msg.source === 36 && msg.dest === 16; // Wireless→OCP command
|
|
719
|
+
|
|
720
|
+
// Cache Wireless commands for later correlation with state changes
|
|
721
|
+
// This helps learn targetId when OCP doesn't broadcast the state=1 message
|
|
722
|
+
if (isFromWireless && circuitState !== 255 && targetId !== 54454) {
|
|
723
|
+
pendingAction184Commands.set(targetId, { state: circuitState, timestamp: Date.now() });
|
|
724
|
+
logger.debug(`v3.004+ Action 184: Cached pending Wireless command - Target ${targetId}, State=${circuitState === 1 ? 'ON' : 'OFF'}`);
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
if ((isFromOCP || isFromWireless) && circuitState !== 255 && targetId !== 54454) {
|
|
728
|
+
const sourceDesc = isFromOCP ? 'OCP broadcast' : 'Wireless command';
|
|
729
|
+
const isOn = circuitState === 1;
|
|
730
|
+
|
|
731
|
+
// Strategy 1: Channel = Target pattern (e.g., Pool circuit 6 uses 108,225 for both)
|
|
732
|
+
// This is a strong signal - the circuit "owns" this channel
|
|
733
|
+
if (channelId === targetId) {
|
|
734
|
+
// Find body circuit with this pattern
|
|
735
|
+
for (let i = 0; i < sys.bodies.length; i++) {
|
|
736
|
+
const body = sys.bodies.getItemByIndex(i);
|
|
737
|
+
if (body.isActive && typeof body.circuit === 'number' && body.circuit > 0) {
|
|
738
|
+
const sbody = state.temps.bodies.getItemById(body.id);
|
|
739
|
+
// For channel=target, trust it even if states don't match perfectly
|
|
740
|
+
// (OCP might broadcast before state is updated)
|
|
741
|
+
const circ = sys.circuits.getItemById(body.circuit, false);
|
|
742
|
+
if (circ && circ.isActive && (typeof circ.targetId === 'undefined' || circ.targetId === 0)) {
|
|
743
|
+
// Only learn if we don't have one yet - channel=target is reliable
|
|
744
|
+
if (sbody && sbody.isOn === isOn) {
|
|
745
|
+
const owner = findActiveCircuitTargetIdOwner(targetId, body.circuit);
|
|
746
|
+
if (!owner) {
|
|
747
|
+
logger.silly(`v3.004+ Action 184: Learned Target ID ${targetId} (${targetIdHi},${targetIdLo}) for body ${body.id} circuit ${body.circuit} (${circ.name || 'unnamed'}) [channel=target pattern]`);
|
|
748
|
+
circ.targetId = targetId;
|
|
749
|
+
} else {
|
|
750
|
+
logger.warn(
|
|
751
|
+
`v3.004+ Action 184: Refusing to learn duplicate targetId ${targetId} for body circuit ${body.circuit} (${circ.name || 'unnamed'}); ` +
|
|
752
|
+
`already owned by circuit ${owner.id} (${owner.name || 'unnamed'}).`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// Strategy 2: State correlation - find circuits matching this state
|
|
762
|
+
// Count how many circuits match to avoid ambiguity
|
|
763
|
+
// PRIORITY: Circuits without targetId that match state
|
|
764
|
+
let matchingCircuitsNoTargetId: { id: number, name: string }[] = [];
|
|
765
|
+
let matchingCircuitsWithTargetId: { id: number, name: string }[] = [];
|
|
766
|
+
|
|
767
|
+
// Check body circuits first (Spa=circuit 1, Pool=circuit 6 in shared systems)
|
|
768
|
+
for (let i = 0; i < sys.bodies.length; i++) {
|
|
769
|
+
const body = sys.bodies.getItemByIndex(i);
|
|
770
|
+
if (body.isActive && typeof body.circuit === 'number' && body.circuit > 0) {
|
|
771
|
+
const sbody = state.temps.bodies.getItemById(body.id);
|
|
772
|
+
const circ = sys.circuits.getItemById(body.circuit, false);
|
|
773
|
+
if (sbody && sbody.isOn === isOn && circ && circ.isActive) {
|
|
774
|
+
if (typeof circ.targetId === 'undefined' || circ.targetId === 0) {
|
|
775
|
+
matchingCircuitsNoTargetId.push({ id: body.circuit, name: circ.name || `Body ${body.id}` });
|
|
776
|
+
} else if (circ.targetId !== targetId) {
|
|
777
|
+
matchingCircuitsWithTargetId.push({ id: body.circuit, name: circ.name || `Body ${body.id}` });
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
// Also check regular circuits
|
|
784
|
+
for (let i = 0; i < sys.circuits.length; i++) {
|
|
785
|
+
const circ = sys.circuits.getItemByIndex(i);
|
|
786
|
+
if (circ.isActive) {
|
|
787
|
+
const cstate = state.circuits.getItemById(circ.id);
|
|
788
|
+
if (cstate && cstate.isOn === isOn) {
|
|
789
|
+
// Avoid duplicate if already counted as body circuit
|
|
790
|
+
if (!matchingCircuitsNoTargetId.find(m => m.id === circ.id) &&
|
|
791
|
+
!matchingCircuitsWithTargetId.find(m => m.id === circ.id)) {
|
|
792
|
+
if (typeof circ.targetId === 'undefined' || circ.targetId === 0) {
|
|
793
|
+
matchingCircuitsNoTargetId.push({ id: circ.id, name: circ.name || `Circuit ${circ.id}` });
|
|
794
|
+
} else if (circ.targetId !== targetId) {
|
|
795
|
+
matchingCircuitsWithTargetId.push({ id: circ.id, name: circ.name || `Circuit ${circ.id}` });
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// Strategy 3: Only ONE circuit without targetId matches - definitive mapping
|
|
803
|
+
// This is the best case: we know exactly which circuit needs learning
|
|
804
|
+
if (matchingCircuitsNoTargetId.length === 1) {
|
|
805
|
+
const match = matchingCircuitsNoTargetId[0];
|
|
806
|
+
const circ = sys.circuits.getItemById(match.id, false);
|
|
807
|
+
if (circ) {
|
|
808
|
+
const owner = findActiveCircuitTargetIdOwner(targetId, match.id);
|
|
809
|
+
if (!owner) {
|
|
810
|
+
logger.silly(`v3.004+ Action 184: Learned Target ID ${targetId} (${targetIdHi},${targetIdLo}) for circuit ${match.id} (${match.name}) [unique unassigned match]`);
|
|
811
|
+
circ.targetId = targetId;
|
|
812
|
+
} else {
|
|
813
|
+
logger.warn(
|
|
814
|
+
`v3.004+ Action 184: Refusing to learn duplicate targetId ${targetId} for circuit ${match.id} (${match.name}); ` +
|
|
815
|
+
`already owned by circuit ${owner.id} (${owner.name || 'unnamed'}).`
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
} else if (matchingCircuitsNoTargetId.length > 1) {
|
|
820
|
+
// Multiple unassigned circuits match - can't determine which
|
|
821
|
+
logger.silly(`v3.004+ Action 184: Target ${targetId} (state=${isOn ? 'ON' : 'OFF'}) matches ${matchingCircuitsNoTargetId.length} unassigned circuits (${matchingCircuitsNoTargetId.map(m => m.name).join(', ')}) - waiting for unique match`);
|
|
822
|
+
} else if (matchingCircuitsNoTargetId.length === 0 && matchingCircuitsWithTargetId.length === 0) {
|
|
823
|
+
// No matching circuits - might be a feature or virtual circuit
|
|
824
|
+
logger.debug(`v3.004+ Action 184: Target ${targetId} (state=${isOn ? 'ON' : 'OFF'}) has no matching circuits - possibly feature or group`);
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
logger.debug(`v3.004+ Action 184 (${sourceDesc}): Channel=${channelId}, Target=${targetId} (${targetIdHi},${targetIdLo}), State=${isOn ? 'ON' : 'OFF'}`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
msg.isProcessed = true;
|
|
831
|
+
break;
|
|
832
|
+
}
|
|
833
|
+
case 217: {
|
|
834
|
+
// v3.004+ Action 217 - Device list broadcast
|
|
835
|
+
// OCP broadcasts registered devices after Action 251→253 handshake
|
|
836
|
+
// Each packet contains info for ONE device
|
|
837
|
+
// Check if this packet is for njsPC (device 33) and update registration status
|
|
838
|
+
if (msg.payload.length > 2 && msg.extractPayloadByte(0) === Message.pluginAddress) {
|
|
839
|
+
const registrationStatus = msg.extractPayloadByte(2);
|
|
840
|
+
// status: 0=unknown, 1=registered, 4=stale/needs-reauth (NOT rejection)
|
|
841
|
+
if (sys.controllerType === ControllerType.IntelliCenter) {
|
|
842
|
+
(sys.board as IntelliCenterBoard).setRegistrationStatus(registrationStatus);
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
msg.isProcessed = true;
|
|
846
|
+
break;
|
|
847
|
+
}
|
|
848
|
+
case 197: {
|
|
849
|
+
// request for date/time on *Touch. Use this as an indicator
|
|
850
|
+
// that SL has requested config and update lastUpdated date/time
|
|
851
|
+
/* let ver: ConfigVersion =
|
|
852
|
+
typeof (sys.configVersion) === 'undefined' ? new ConfigVersion({}) : sys.configVersion;
|
|
853
|
+
ver.lastUpdated = new Date();
|
|
854
|
+
sys.processVersionChanges(ver); */
|
|
855
|
+
sys.configVersion.lastUpdated = new Date();
|
|
856
|
+
msg.isProcessed = true;
|
|
857
|
+
break;
|
|
858
|
+
}
|
|
859
|
+
case 204: // IntelliCenter only.
|
|
860
|
+
state.batteryVoltage = msg.extractPayloadByte(2) / 50;
|
|
861
|
+
state.comms.keepAlives = msg.extractPayloadInt(4);
|
|
862
|
+
state.time.year = msg.extractPayloadByte(8);
|
|
863
|
+
state.time.month = msg.extractPayloadByte(7);
|
|
864
|
+
state.time.date = msg.extractPayloadByte(6);
|
|
865
|
+
sys.equipment.controllerFirmware = (msg.extractPayloadByte(42) + (msg.extractPayloadByte(43) / 1000)).toString();
|
|
866
|
+
// v3.004+: Seed known targetIds immediately once firmware>=3.0 is confirmed (before UI interaction).
|
|
867
|
+
if (sys.equipment.isIntellicenterV3 && typeof (sys.board.circuits as any)?.seedKnownV3TargetIds === 'function') {
|
|
868
|
+
(sys.board.circuits as any).seedKnownV3TargetIds();
|
|
869
|
+
}
|
|
870
|
+
// v3.004 adds 4 additional bytes (44-46) that are the time of day
|
|
871
|
+
// Byte 44: Hour (0-23)
|
|
872
|
+
// Byte 45: Minute (0-59)
|
|
873
|
+
// Byte 46: Second (0-59)
|
|
874
|
+
// Byte 47: Unknown - possibly DST indicator or status flag
|
|
875
|
+
if (sys.chlorinators.length > 0) {
|
|
876
|
+
if (msg.extractPayloadByte(37, 255) !== 255) {
|
|
877
|
+
const chlor = state.chlorinators.getItemById(1);
|
|
878
|
+
chlor.superChlorRemaining = msg.extractPayloadByte(37) * 3600 + msg.extractPayloadByte(38) * 60;
|
|
879
|
+
} else {
|
|
880
|
+
const chlor = state.chlorinators.getItemById(1);
|
|
881
|
+
chlor.superChlorRemaining = 0;
|
|
882
|
+
chlor.superChlor = false;
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
// v3.004+: Do NOT process feature states from Action 204!
|
|
886
|
+
// Evidence from packet captures shows Action 204 byte 19 contains STALE feature state
|
|
887
|
+
// that doesn't update when features change. The authoritative source for v3 feature
|
|
888
|
+
// state is Action 30 case 15 (config response to Action 222 [15,0] request).
|
|
889
|
+
// Action 204 continuously broadcasts stale data and overwrites the correct state.
|
|
890
|
+
//
|
|
891
|
+
// v1.x: Feature states at offset 9 - this was deemed reliable in 2020.
|
|
892
|
+
if (!sys.equipment.isIntellicenterV3) {
|
|
893
|
+
ExternalMessage.processFeatureState(9, msg);
|
|
894
|
+
}
|
|
895
|
+
//if (sys.equipment.dual === true) {
|
|
896
|
+
// // For IntelliCenter i10D the body state is on byte 26 of the 204. This impacts circuit 6.
|
|
897
|
+
// let byte = msg.extractPayloadByte(26);
|
|
898
|
+
// let pstate = state.circuits.getItemById(6, true);
|
|
899
|
+
// let oldstate = pstate.isOn;
|
|
900
|
+
// pstate.isOn = ((byte & 0x0010) === 0x0010);
|
|
901
|
+
// logger.info(`Checking i10D pool state ${byte} old:${oldstate} new: ${pstate.isOn}`);
|
|
902
|
+
// //if (oldstate !== pstate.isOn) {
|
|
903
|
+
// state.temps.bodies.getItemById(1, true).isOn = pstate.isOn;
|
|
904
|
+
// sys.board.circuits.syncCircuitRelayStates();
|
|
905
|
+
// sys.board.circuits.syncVirtualCircuitStates();
|
|
906
|
+
// sys.board.valves.syncValveStates();
|
|
907
|
+
// sys.board.filters.syncFilterStates();
|
|
908
|
+
// sys.board.heaters.syncHeaterStates();
|
|
909
|
+
// //}
|
|
910
|
+
// if (oldstate !== pstate.isOn) pstate.emitEquipmentChange();
|
|
911
|
+
//}
|
|
912
|
+
// At this point normally on is ignored. Not sure what this does.
|
|
913
|
+
let cover1 = sys.covers.getItemById(1);
|
|
914
|
+
let cover2 = sys.covers.getItemById(2);
|
|
915
|
+
if (cover1.isActive) {
|
|
916
|
+
let scover1 = state.covers.getItemById(1, true);
|
|
917
|
+
scover1.name = cover1.name;
|
|
918
|
+
state.temps.bodies.getItemById(cover1.body + 1).isCovered = scover1.isClosed = (msg.extractPayloadByte(30) & 0x0001) > 0;
|
|
919
|
+
}
|
|
920
|
+
if (cover2.isActive) {
|
|
921
|
+
let scover2 = state.covers.getItemById(2, true);
|
|
922
|
+
scover2.name = cover2.name;
|
|
923
|
+
state.temps.bodies.getItemById(cover2.body + 1).isCovered = scover2.isClosed = (msg.extractPayloadByte(30) & 0x0002) > 0;
|
|
924
|
+
}
|
|
925
|
+
sys.board.schedules.syncScheduleStates();
|
|
926
|
+
msg.isProcessed = true;
|
|
927
|
+
state.emitEquipmentChanges();
|
|
928
|
+
break;
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
private static processCircuitState(msg: Inbound) {
|
|
932
|
+
// The way this works is that there is one byte per 8 circuits for a total of 5 bytes or 40 circuits. The
|
|
933
|
+
// configuration already determined how many available circuits we have by querying the model of the panel
|
|
934
|
+
// and any installed expansion panel models. Only the number of available circuits will appear in this
|
|
935
|
+
// array.
|
|
936
|
+
let circuitId = 1;
|
|
937
|
+
let maxCircuitId = sys.board.equipmentIds.circuits.end;
|
|
938
|
+
for (let i = 2; i < msg.payload.length && circuitId <= maxCircuitId; i++) {
|
|
939
|
+
const byte = msg.extractPayloadByte(i);
|
|
940
|
+
// Shift each bit getting the circuit identified by each value.
|
|
941
|
+
for (let j = 0; j < 8; j++) {
|
|
942
|
+
let circuit = sys.circuits.getItemById(circuitId, false, { isActive: false });
|
|
943
|
+
if (circuit.isActive !== false) {
|
|
944
|
+
let cstate = state.circuits.getItemById(circuitId, circuit.isActive);
|
|
945
|
+
const wasOn = cstate.isOn;
|
|
946
|
+
// For IntelliCenter i10D body circuits are not reported here.
|
|
947
|
+
let isOn = ((circuitId === 6 || circuitId === 1) && sys.equipment.dual === true) ? cstate.isOn : (byte & (1 << j)) > 0;
|
|
948
|
+
//let isOn = (byte & (1 << j)) > 0;
|
|
949
|
+
cstate.isOn = isOn;
|
|
950
|
+
// v3.004+ learning: When circuit state changes, check for pending Wireless commands
|
|
951
|
+
if (sys.controllerType === ControllerType.IntelliCenter &&
|
|
952
|
+
sys.equipment.isIntellicenterV3 &&
|
|
953
|
+
wasOn !== isOn &&
|
|
954
|
+
(typeof circuit.targetId === 'undefined' || circuit.targetId === 0)) {
|
|
955
|
+
EquipmentStateMessage.checkPendingAction184Learning(circuitId, isOn);
|
|
956
|
+
}
|
|
957
|
+
cstate.name = circuit.name;
|
|
958
|
+
cstate.nameId = circuit.nameId;
|
|
959
|
+
cstate.showInFeatures = circuit.showInFeatures;
|
|
960
|
+
cstate.type = circuit.type;
|
|
961
|
+
sys.board.circuits.setEndTime(circuit, cstate, isOn);
|
|
962
|
+
if (sys.controllerType === ControllerType.IntelliCenter) {
|
|
963
|
+
// intellitouch sends a separate msg with themes
|
|
964
|
+
switch (circuit.type) {
|
|
965
|
+
case 6: // Globrite
|
|
966
|
+
case 5: // Magicstream
|
|
967
|
+
case 8: // Intellibrite
|
|
968
|
+
case 10: // Colorcascade
|
|
969
|
+
cstate.lightingTheme = circuit.lightingTheme;
|
|
970
|
+
break;
|
|
971
|
+
case 9:
|
|
972
|
+
cstate.level = circuit.level || 0;
|
|
973
|
+
break;
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
circuitId++;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
msg.isProcessed = true;
|
|
981
|
+
}
|
|
982
|
+
private static processSunTouchCircuits(msg: Inbound) {
|
|
983
|
+
// SunTouch has really twisted bit mapping for its
|
|
984
|
+
// circuit states. Features are intertwined within the
|
|
985
|
+
// features.
|
|
986
|
+
let byte = msg.extractPayloadByte(2);
|
|
987
|
+
for (let i = 0; i < 8; i++) {
|
|
988
|
+
let id = i === 4 ? 7 : i > 5 ? i + 2 : i + 1;
|
|
989
|
+
let circ = sys.circuits.getInterfaceById(id, false, { isActive: false });
|
|
990
|
+
if (circ.isActive) {
|
|
991
|
+
let isOn = ((1 << i) & byte) > 0;
|
|
992
|
+
let cstate = state.circuits.getInterfaceById(id, circ.isActive);
|
|
993
|
+
if (isOn !== cstate.isOn) {
|
|
994
|
+
sys.board.circuits.setEndTime(circ, cstate, isOn);
|
|
995
|
+
cstate.isOn = isOn;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
byte = msg.extractPayloadByte(3);
|
|
1000
|
+
{
|
|
1001
|
+
let circ = sys.circuits.getInterfaceById(10, false, { isActive: false });
|
|
1002
|
+
if (circ.isActive) {
|
|
1003
|
+
let isOn = (byte & 1) > 0;
|
|
1004
|
+
let cstate = state.circuits.getInterfaceById(circ.id, circ.isActive);
|
|
1005
|
+
if (isOn !== cstate.isOn) {
|
|
1006
|
+
sys.board.circuits.setEndTime(circ, cstate, isOn);
|
|
1007
|
+
cstate.isOn = isOn;
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
}
|
|
1011
|
+
state.emitEquipmentChanges();
|
|
1012
|
+
msg.isProcessed = true;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
private static processFeatureStateV3(msg: Inbound) {
|
|
1016
|
+
// DISABLED: v3.004+ Action 2 bytes 7-8 do NOT use simple bitmask encoding!
|
|
1017
|
+
//
|
|
1018
|
+
// Analysis from replay 76 (Dec 2024):
|
|
1019
|
+
// - F1 on → byte7=16 (bit 4), byte8=0
|
|
1020
|
+
// - F1+F2 → byte7=32 (bit 5), byte8=1
|
|
1021
|
+
// - F1+F2+F3 → byte7=64 (bit 6), byte8=2
|
|
1022
|
+
//
|
|
1023
|
+
// This is NOT a bitmask - it appears to be some kind of encoded state.
|
|
1024
|
+
// Using this data corrupts feature state and causes wrong features to display.
|
|
1025
|
+
//
|
|
1026
|
+
// For v3.004+, feature state must come from:
|
|
1027
|
+
// 1. Action 30 case 15 responses (when njsPC requests via Action 222)
|
|
1028
|
+
// 2. TODO: Snoop on Action 30/15 going to Wireless (dest=36) for real-time updates
|
|
1029
|
+
//
|
|
1030
|
+
// DO NOT ENABLE THIS FUNCTION until the encoding is fully understood.
|
|
1031
|
+
msg.isProcessed = true;
|
|
1032
|
+
return;
|
|
1033
|
+
|
|
1034
|
+
// Original broken code kept for reference:
|
|
1035
|
+
/*
|
|
1036
|
+
const byte7 = msg.extractPayloadByte(7);
|
|
1037
|
+
const byte8 = msg.extractPayloadByte(8);
|
|
1038
|
+
const featureStateBits = byte7 | (byte8 << 8);
|
|
1039
|
+
|
|
1040
|
+
let featureId = sys.board.equipmentIds.features.start;
|
|
1041
|
+
let maxFeatureId = sys.features.getMaxId(true, 0);
|
|
1042
|
+
|
|
1043
|
+
for (let j = 0; featureId <= maxFeatureId && j < 16; j++) {
|
|
1044
|
+
let feature = sys.features.getItemById(featureId, false, { isActive: false });
|
|
1045
|
+
if (feature.isActive !== false) {
|
|
1046
|
+
let fstate = state.features.getItemById(featureId, true);
|
|
1047
|
+
let isOn = (featureStateBits & (1 << j)) > 0;
|
|
1048
|
+
sys.board.circuits.setEndTime(feature, fstate, isOn);
|
|
1049
|
+
fstate.isOn = isOn;
|
|
1050
|
+
fstate.name = feature.name;
|
|
1051
|
+
}
|
|
1052
|
+
else {
|
|
1053
|
+
state.features.removeItemById(featureId);
|
|
1054
|
+
}
|
|
1055
|
+
featureId++;
|
|
1056
|
+
}
|
|
1057
|
+
state.emitEquipmentChanges();
|
|
1058
|
+
*/
|
|
1059
|
+
msg.isProcessed = true;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
private static processTouchCircuits(msg: Inbound) {
|
|
1063
|
+
let circuitId = 1;
|
|
1064
|
+
let maxCircuitId = sys.board.equipmentIds.features.end;
|
|
1065
|
+
for (let i = 2; i < msg.payload.length && circuitId <= maxCircuitId; i++) {
|
|
1066
|
+
const byte = msg.extractPayloadByte(i);
|
|
1067
|
+
// Shift each bit getting the circuit identified by each value.
|
|
1068
|
+
for (let j = 0; j < 8; j++) {
|
|
1069
|
+
const circ = sys.circuits.getInterfaceById(circuitId, false, { isActive: false });
|
|
1070
|
+
if (!sys.board.equipmentIds.invalidIds.isValidId(circuitId)) {
|
|
1071
|
+
circ.isActive = false;
|
|
1072
|
+
}
|
|
1073
|
+
if (circ.isActive) {
|
|
1074
|
+
const cstate = state.circuits.getInterfaceById(circuitId, circ.isActive);
|
|
1075
|
+
cstate.showInFeatures = circ.showInFeatures;
|
|
1076
|
+
let isOn = (byte & 1 << j) >> j > 0;
|
|
1077
|
+
if (isOn !== cstate.isOn) {
|
|
1078
|
+
sys.board.circuits.setEndTime(circ, cstate, isOn);
|
|
1079
|
+
cstate.isOn = isOn;
|
|
1080
|
+
}
|
|
1081
|
+
cstate.name = circ.name;
|
|
1082
|
+
cstate.type = circ.type;
|
|
1083
|
+
cstate.nameId = circ.nameId;
|
|
1084
|
+
}
|
|
1085
|
+
else {
|
|
1086
|
+
if (circ instanceof Circuit) {
|
|
1087
|
+
sys.circuits.removeItemById(circuitId);
|
|
1088
|
+
// don't forget to remove from state #257
|
|
1089
|
+
state.circuits.removeItemById(circuitId);
|
|
1090
|
+
}
|
|
1091
|
+
else if (circ instanceof Feature) {
|
|
1092
|
+
sys.features.removeItemById(circuitId);
|
|
1093
|
+
// don't forget to remove from state #257
|
|
1094
|
+
state.features.removeItemById(circuitId);
|
|
1095
|
+
}
|
|
1096
|
+
}
|
|
1097
|
+
circuitId++;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
// state.body = body;
|
|
1101
|
+
//state.emitControllerChange();
|
|
1102
|
+
state.emitEquipmentChanges();
|
|
1103
|
+
msg.isProcessed = true;
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
private static processIntelliBriteMode(msg: Inbound) {
|
|
1107
|
+
// eg RED: [165,16,16,34,96,2],[195,0],[2,12]
|
|
1108
|
+
// data[0] = color
|
|
1109
|
+
const theme = msg.extractPayloadByte(0);
|
|
1110
|
+
switch (theme) {
|
|
1111
|
+
case 0: // off
|
|
1112
|
+
case 1: // on
|
|
1113
|
+
case 190: // save
|
|
1114
|
+
// case 191: // recall
|
|
1115
|
+
// RKS: TODO hold may be in this list since I see the all on and all off command here. Sync is probably in the colorset message that includes the timings.
|
|
1116
|
+
// do nothing as these don't actually change the state.
|
|
1117
|
+
break;
|
|
1118
|
+
|
|
1119
|
+
default:
|
|
1120
|
+
{
|
|
1121
|
+
// intellibrite themes
|
|
1122
|
+
// This is an observed message in that no-one asked for it. *Touch does not report the theme and in fact, it is not even
|
|
1123
|
+
// stored. Once the message is sent then it throws away the data. When you turn the light
|
|
1124
|
+
// on again it will be on at whatever theme happened to be set at the time it went off. We keep this
|
|
1125
|
+
// as a best guess so when the user turns on the light it will likely be the last theme observed.
|
|
1126
|
+
const grp = sys.lightGroups.getItemById(sys.board.equipmentIds.circuitGroups.start);
|
|
1127
|
+
const sgrp = state.lightGroups.getItemById(sys.board.equipmentIds.circuitGroups.start);
|
|
1128
|
+
grp.lightingTheme = sgrp.lightingTheme = theme;
|
|
1129
|
+
for (let i = 0; i < grp.circuits.length; i++) {
|
|
1130
|
+
let c = grp.circuits.getItemByIndex(i);
|
|
1131
|
+
let cstate = state.circuits.getItemById(c.circuit);
|
|
1132
|
+
let circuit = sys.circuits.getInterfaceById(c.circuit);
|
|
1133
|
+
if (cstate.isOn) cstate.lightingTheme = circuit.lightingTheme = theme;
|
|
1134
|
+
}
|
|
1135
|
+
switch (theme) {
|
|
1136
|
+
case 128: // sync
|
|
1137
|
+
sys.board.circuits.sequenceLightGroupAsync(grp.id, 'sync');
|
|
1138
|
+
break;
|
|
1139
|
+
case 144: // swim
|
|
1140
|
+
sys.board.circuits.sequenceLightGroupAsync(grp.id, 'swim');
|
|
1141
|
+
break;
|
|
1142
|
+
case 160: // set
|
|
1143
|
+
sys.board.circuits.sequenceLightGroupAsync(grp.id, 'set');
|
|
1144
|
+
break;
|
|
1145
|
+
case 190: // save
|
|
1146
|
+
case 191: // recall
|
|
1147
|
+
sys.board.circuits.sequenceLightGroupAsync(grp.id, 'other');
|
|
1148
|
+
break;
|
|
1149
|
+
default:
|
|
1150
|
+
sys.board.circuits.sequenceLightGroupAsync(grp.id, 'color');
|
|
1151
|
+
// other themes for magicstream?
|
|
1152
|
+
}
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
msg.isProcessed = true;
|
|
1157
|
+
}
|
|
1158
|
+
}
|