nodejs-poolcontroller 8.3.0 → 8.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/.eslintrc.json +36 -36
  2. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -84
  3. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -12
  4. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -28
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  6. package/.github/copilot-instructions.md +63 -63
  7. package/.github/workflows/ghcr-publish.yml +67 -67
  8. package/157_issues.md +101 -0
  9. package/AGENTS.md +613 -0
  10. package/CONTRIBUTING.md +74 -74
  11. package/Changelog +292 -284
  12. package/Dockerfile +62 -62
  13. package/Gruntfile.js +40 -40
  14. package/LICENSE +661 -661
  15. package/README.md +329 -309
  16. package/anslq25/MessagesMock.ts +221 -221
  17. package/anslq25/boards/MockBoardFactory.ts +49 -49
  18. package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
  19. package/anslq25/boards/MockSystemBoard.ts +216 -216
  20. package/anslq25/chemistry/MockChlorinator.ts +98 -98
  21. package/anslq25/pumps/MockPump.ts +83 -83
  22. package/app.ts +115 -115
  23. package/config/Config.ts +0 -0
  24. package/config/VersionCheck.ts +0 -0
  25. package/controller/Constants.ts +809 -805
  26. package/controller/Equipment.ts +2737 -2664
  27. package/controller/Errors.ts +181 -181
  28. package/controller/Lockouts.ts +549 -549
  29. package/controller/State.ts +3746 -3701
  30. package/controller/boards/AquaLinkBoard.ts +1175 -1003
  31. package/controller/boards/BoardFactory.ts +53 -53
  32. package/controller/boards/EasyTouchBoard.ts +3246 -3202
  33. package/controller/boards/IntelliCenterBoard.ts +4581 -3899
  34. package/controller/boards/IntelliComBoard.ts +69 -69
  35. package/controller/boards/IntelliTouchBoard.ts +382 -382
  36. package/controller/boards/NixieBoard.ts +1947 -1944
  37. package/controller/boards/SunTouchBoard.ts +401 -400
  38. package/controller/boards/SystemBoard.ts +5303 -5268
  39. package/controller/comms/Comms.ts +1278 -1255
  40. package/controller/comms/ScreenLogic.ts +1665 -1665
  41. package/controller/comms/messages/Messages.ts +1627 -1406
  42. package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
  43. package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
  44. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  45. package/controller/comms/messages/config/ConfigMessage.ts +6 -0
  46. package/controller/comms/messages/config/CoverMessage.ts +0 -0
  47. package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
  48. package/controller/comms/messages/config/EquipmentMessage.ts +250 -210
  49. package/controller/comms/messages/config/ExternalMessage.ts +1051 -903
  50. package/controller/comms/messages/config/FeatureMessage.ts +0 -0
  51. package/controller/comms/messages/config/GeneralMessage.ts +65 -0
  52. package/controller/comms/messages/config/HeaterMessage.ts +0 -0
  53. package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
  54. package/controller/comms/messages/config/OptionsMessage.ts +207 -174
  55. package/controller/comms/messages/config/PumpMessage.ts +427 -421
  56. package/controller/comms/messages/config/RemoteMessage.ts +0 -0
  57. package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
  58. package/controller/comms/messages/config/SecurityMessage.ts +37 -13
  59. package/controller/comms/messages/config/ValveMessage.ts +0 -0
  60. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
  61. package/controller/comms/messages/status/EquipmentStateMessage.ts +940 -822
  62. package/controller/comms/messages/status/HeaterStateMessage.ts +147 -135
  63. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
  64. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
  65. package/controller/comms/messages/status/NeptuneModbusStateMessage.ts +217 -0
  66. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  67. package/controller/comms/messages/status/RegalModbusStateMessage.ts +410 -410
  68. package/controller/comms/messages/status/VersionMessage.ts +152 -41
  69. package/controller/nixie/Nixie.ts +173 -173
  70. package/controller/nixie/NixieEquipment.ts +104 -104
  71. package/controller/nixie/bodies/Body.ts +120 -120
  72. package/controller/nixie/bodies/Filter.ts +135 -135
  73. package/controller/nixie/chemistry/ChemController.ts +2756 -2724
  74. package/controller/nixie/chemistry/ChemDoser.ts +806 -806
  75. package/controller/nixie/chemistry/Chlorinator.ts +367 -367
  76. package/controller/nixie/circuits/Circuit.ts +478 -478
  77. package/controller/nixie/heaters/Heater.ts +843 -834
  78. package/controller/nixie/pumps/Pump.ts +1336 -1193
  79. package/controller/nixie/schedules/Schedule.ts +401 -401
  80. package/controller/nixie/valves/Valve.ts +170 -170
  81. package/defaultConfig.json +352 -352
  82. package/docker-compose.yml +32 -31
  83. package/logger/DataLogger.ts +448 -448
  84. package/logger/Logger.ts +459 -436
  85. package/package.json +58 -58
  86. package/sendSocket.js +32 -32
  87. package/tsconfig.json +26 -25
  88. package/types/express-multer.d.ts +32 -32
  89. package/web/Server.ts +1939 -1927
  90. package/web/bindings/aqualinkD.json +559 -559
  91. package/web/bindings/influxDB.json +1066 -1066
  92. package/web/bindings/mqtt.json +721 -721
  93. package/web/bindings/mqttAlt.json +746 -746
  94. package/web/bindings/rulesManager.json +54 -54
  95. package/web/bindings/smartThings-Hubitat.json +31 -31
  96. package/web/bindings/valveRelays.json +20 -20
  97. package/web/bindings/vera.json +25 -25
  98. package/web/interfaces/baseInterface.ts +188 -188
  99. package/web/interfaces/httpInterface.ts +148 -148
  100. package/web/interfaces/influxInterface.ts +283 -283
  101. package/web/interfaces/mqttInterface.ts +695 -695
  102. package/web/interfaces/ruleInterface.ts +101 -87
  103. package/web/services/config/Config.ts +1212 -1053
  104. package/web/services/config/ConfigSocket.ts +0 -0
  105. package/web/services/state/State.ts +21 -0
  106. package/web/services/state/StateSocket.ts +28 -0
  107. package/web/services/utilities/Utilities.ts +233 -233
@@ -1,822 +1,940 @@
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
- export class EquipmentStateMessage {
31
- private static initIntelliCenter(msg: Inbound) {
32
- sys.controllerType = ControllerType.IntelliCenter;
33
- sys.equipment.maxSchedules = 100;
34
- sys.equipment.maxFeatures = 32;
35
- // Always get equipment since this is volatile between loads. Everything else takes care of itself.
36
- sys.configVersion.equipment = 0;
37
- }
38
- public static initDefaults() {
39
- // defaults; set to lowest possible values. Each *Touch will extend this once we know the model.
40
- sys.equipment.maxBodies = 1;
41
- sys.equipment.maxCircuits = 6;
42
- sys.equipment.maxSchedules = 12;
43
- sys.equipment.maxPumps = 2;
44
- sys.equipment.maxSchedules = 12;
45
- sys.equipment.maxValves = 2;
46
- sys.equipment.maxCircuitGroups = 0;
47
- sys.equipment.maxLightGroups = 1;
48
- sys.equipment.maxIntelliBrites = 8;
49
- sys.equipment.maxChemControllers = sys.equipment.maxChlorinators = 1;
50
- sys.equipment.maxCustomNames = 10;
51
- sys.equipment.maxChemControllers = 4;
52
- sys.equipment.maxFeatures = 8;
53
- sys.equipment.model = 'Unknown';
54
- }
55
- private static initTouch(msg: Inbound) {
56
- let model1 = msg.extractPayloadByte(27);
57
- let model2 = msg.extractPayloadByte(28);
58
- switch (model2) {
59
- case 0:
60
- case 1:
61
- case 2:
62
- case 3:
63
- case 4:
64
- case 5:
65
- logger.info(`Found IntelliTouch Controller`);
66
- sys.controllerType = ControllerType.IntelliTouch;
67
- model1 = msg.extractPayloadByte(28);
68
- model2 = msg.extractPayloadByte(9);
69
- (sys.board as IntelliTouchBoard).initExpansionModules(model1, model2);
70
- break;
71
- case 11:
72
- logger.info(`Found SunTouch Controller`);
73
- sys.controllerType = ControllerType.SunTouch;
74
- (sys.board as SunTouchBoard).initExpansionModules(model1, model2);
75
- break;
76
- case 13:
77
- case 14:
78
- logger.info(`Found EasyTouch Controller`);
79
- sys.controllerType = ControllerType.EasyTouch;
80
- (sys.board as EasyTouchBoard).initExpansionModules(model1, model2);
81
- break;
82
- default:
83
- logger.error(`Unknown Touch Controller ${msg.extractPayloadByte(28)}:${msg.extractPayloadByte(27)}`);
84
- break;
85
- }
86
- //let board = sys.board as EasyTouchBoard;
87
- //board.initExpansionModules(model1, model2);
88
- }
89
- private static initController(msg: Inbound) {
90
- state.status = 1;
91
- const model1 = msg.extractPayloadByte(27);
92
- const model2 = msg.extractPayloadByte(28);
93
- // RKS: 06-15-20 -- While this works for now the way we are detecting seems a bit dubious. First, the 2 status message
94
- // contains two model bytes. Right now the ones witness in the wild include 23 = fw1.023, 40 = fw1.040, 47 = fw1.047.
95
- // RKS: 07-21-22 -- Pentair is about to release fw1.232. Unfortunately, the byte mapping for this has changed such that
96
- // the bytes [27,28] are [0,2] respectively. This looks like it might be in conflict with IntelliTouch but it is not. Below
97
- // are the combinations of 27,28 we have seen for IntelliTouch
98
- // [1,0] = i5+3
99
- // [0,1] = i7+3
100
- // [1,3] = i5+3s
101
- // [1,4] = i9+3s
102
- // [1,5] = i10+3d
103
- if ((model2 === 0 && (model1 === 23 || model1 >= 40)) ||
104
- (model2 === 2 && model1 == 0)) {
105
- state.equipment.controllerType = 'intellicenter';
106
- sys.board.modulesAcquired = false;
107
- sys.controllerType = ControllerType.IntelliCenter;
108
- logger.info(`Found Controller Board ${state.equipment.model || 'IntelliCenter'}, awaiting installed modules.`);
109
- EquipmentStateMessage.initIntelliCenter(msg);
110
- }
111
- else {
112
- EquipmentStateMessage.initTouch(msg);
113
- sys.board.needsConfigChanges = true;
114
- setTimeout(function () { sys.checkConfiguration(); }, 300);
115
- }
116
- }
117
- public static process(msg: Inbound) {
118
- Message.headerSubByte = msg.header[1];
119
- //console.log(process.memoryUsage());
120
- if (msg.action === 2 && state.isInitialized && sys.controllerType === ControllerType.Nixie) {
121
- // Start over because we didn't have communication before but we now do. This will fall into the if
122
- // below so that it goes through the intialization process. In this case we didn't see an OCP when we started
123
- // but there clearly is one now.
124
- (async () => {
125
- await sys.board.closeAsync();
126
- logger.info(`Closed ${sys.controllerType} board`);
127
- sys.controllerType = ControllerType.Unknown;
128
- state.status = 0;
129
- })();
130
- }
131
- if (!state.isInitialized) {
132
- msg.isProcessed = true;
133
- if (msg.action === 2) EquipmentStateMessage.initController(msg);
134
- else return;
135
- }
136
- else if (!sys.board.modulesAcquired) {
137
- msg.isProcessed = true;
138
- if (msg.action === 204) {
139
- let board = sys.board as IntelliCenterBoard;
140
- // We have determined that the 204 message now contains the information
141
- // related to the installed expansion boards.
142
- console.log(`INTELLICENTER MODULES DETECTED, REQUESTING STATUS!`);
143
- // Master = 13-14
144
- // EXP1 = 15-16
145
- // EXP2 = 17-18
146
- let pc = msg.extractPayloadByte(40);
147
- board.initExpansionModules(msg.extractPayloadByte(13), msg.extractPayloadByte(14),
148
- pc & 0x01 ? msg.extractPayloadByte(15) : 0x00, pc & 0x01 ? msg.extractPayloadByte(16) : 0x00,
149
- pc & 0x02 ? msg.extractPayloadByte(17) : 0x00, pc & 0x02 ? msg.extractPayloadByte(18) : 0x00,
150
- pc & 0x04 ? msg.extractPayloadByte(19) : 0x00, pc & 0x04 ? msg.extractPayloadByte(20) : 0x00);
151
- sys.equipment.setEquipmentIds();
152
- }
153
- else return;
154
- }
155
- switch (msg.action) {
156
- case 2:
157
- {
158
- let fnTempFromByte = function (byte) {
159
- return byte;
160
- //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.
161
- }
162
-
163
- // Shared
164
- let dt = new Date();
165
- // RKS: This was moved to the ChemControllerState message. This is flawed in that it incorrectly sets IntelliChem to no comms.
166
- //if (state.chemControllers.length > 0) {
167
- // // TODO: move this to chemController when we understand the packets better
168
- // for (let i = 0; i < state.chemControllers.length; i++) {
169
- // let ccontroller = state.chemControllers.getItemByIndex(i);
170
- // if (sys.board.valueMaps.chemControllerTypes.getName(ccontroller.type) === 'intellichem') {
171
- // if (dt.getTime() - ccontroller.lastComm > 60000) ccontroller.status = 1;
172
- // }
173
- // }
174
- //}
175
- state.time.hours = msg.extractPayloadByte(0);
176
- state.time.minutes = msg.extractPayloadByte(1);
177
- state.time.seconds = dt.getSeconds();
178
- state.mode = sys.controllerType !== ControllerType.IntelliCenter ? (msg.extractPayloadByte(9) & 0x81) : (msg.extractPayloadByte(9) & 0x01);
179
-
180
- // 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
181
- // 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
182
- // since pressure, volume, and length have been introduced.
183
- sys.general.options.units = state.temps.units = msg.extractPayloadByte(9) & 0x04;
184
- state.valve = msg.extractPayloadByte(10);
185
-
186
-
187
- // RSG - added 7/8/2020
188
- // Every 30 mins, check the timezone and adjust DST settings
189
- if (dt.getMinutes() % 30 === 0) {
190
- sys.board.system.setTZ();
191
- sys.board.schedules.updateSunriseSunsetAsync().then((updated: boolean)=>{
192
- if (updated) {logger.debug(`Sunrise/sunset times updated on schedules.`);}
193
- });
194
- }
195
- // Check and update clock when it is off by >5 mins (just for a small buffer) and:
196
- // 1. IntelliCenter has "manual" time set (Internet will automatically adjust) and autoAdjustDST is enabled
197
- // 2. *Touch is "manual" (only option) and autoAdjustDST is enabled - (same as #1)
198
- // 3. clock source is "server" isn't an OCP option but can be enabled on the clients
199
- if (dt.getMinutes() % 5 === 0 && dt.getSeconds() <= 10 && sys.general.options.clockSource === 'server') {
200
- if ((Math.abs(dt.getTime() - state.time.getTime()) > 60 * 2 * 1000) && !state.time.isUpdating) {
201
- state.time.isUpdating = true;
202
- sys.board.system.setDateTimeAsync({ dt, dst: sys.general.options.adjustDST || 0, })
203
- .then(() => {
204
- logger.info(`njsPC automatically updated OCP time. You're welcome.`);
205
- })
206
- .catch((err) => {
207
- logger.error(`Error automatically setting system time. ${JSON.stringify(err)}`)
208
- })
209
- .finally(() => {
210
- state.time.isUpdating = false;
211
- })
212
- }
213
- }
214
- state.delay = msg.extractPayloadByte(12) & 63; // not sure what 64 val represents
215
- state.freeze = (msg.extractPayloadByte(9) & 0x08) === 0x08;
216
- if (sys.controllerType === ControllerType.IntelliCenter) {
217
- state.temps.waterSensor1 = fnTempFromByte(msg.extractPayloadByte(14));
218
- if (sys.bodies.length > 2 || sys.equipment.dual) state.temps.waterSensor2 = fnTempFromByte(msg.extractPayloadByte(15));
219
- // We are making an assumption here in that the circuits are always labeled the same.
220
- // 1=Spa/Body2
221
- // 6=Pool/Body1
222
- // 12=Body3
223
- // 22=Body4 -- Really not sure about this one.
224
- if (sys.bodies.length > 0) {
225
- // We will not go in here if this is not a shared body.
226
- const tbody: BodyTempState = state.temps.bodies.getItemById(1, true);
227
- const cbody: Body = sys.bodies.getItemById(1);
228
- tbody.heatMode = cbody.heatMode;
229
- tbody.setPoint = cbody.setPoint;
230
- tbody.name = cbody.name;
231
- tbody.circuit = cbody.circuit = 6;
232
- tbody.heatStatus = msg.extractPayloadByte(11) & 0x0F;
233
- // With the IntelliCenter i10D, bit 6 is not reliable. It is not set properly and requires the 204 message
234
- // to process the data.
235
- if (!sys.equipment.dual) {
236
- if ((msg.extractPayloadByte(2) & 0x20) === 32) {
237
- tbody.temp = state.temps.waterSensor1;
238
- tbody.isOn = true;
239
- } else tbody.isOn = false;
240
- }
241
- else if (state.circuits.getItemById(6).isOn === true) {
242
- tbody.temp = state.temps.waterSensor1;
243
- tbody.isOn = true;
244
- }
245
- else tbody.isOn = false;
246
- }
247
- if (sys.bodies.length > 1) {
248
- const tbody: BodyTempState = state.temps.bodies.getItemById(2, true);
249
- const cbody: Body = sys.bodies.getItemById(2);
250
- tbody.heatMode = cbody.heatMode;
251
- tbody.setPoint = cbody.setPoint;
252
- tbody.name = cbody.name;
253
- tbody.circuit = cbody.circuit = 1;
254
- tbody.heatStatus = (msg.extractPayloadByte(11) & 0xF0) >> 4;
255
- if (!sys.equipment.dual) {
256
- if ((msg.extractPayloadByte(2) & 0x01) === 1) {
257
- tbody.temp = sys.equipment.shared ? state.temps.waterSensor1 : state.temps.waterSensor2;
258
- tbody.isOn = true;
259
- } else tbody.isOn = false;
260
- } else if (state.circuits.getItemById(1).isOn === true) {
261
- tbody.temp = sys.equipment.shared ? state.temps.waterSensor1 : state.temps.waterSensor2;
262
- tbody.isOn = true;
263
- }
264
- else tbody.isOn = false;
265
- }
266
- if (sys.bodies.length > 2) {
267
- state.temps.waterSensor3 = fnTempFromByte(msg.extractPayloadByte(20));
268
- // const tbody: BodyTempState = state.temps.bodies.getItemById(10, true);
269
- const tbody: BodyTempState = state.temps.bodies.getItemById(3, true);
270
- const cbody: Body = sys.bodies.getItemById(3);
271
- tbody.name = cbody.name;
272
- tbody.heatMode = cbody.heatMode;
273
- tbody.setPoint = cbody.setPoint;
274
- tbody.heatStatus = msg.extractPayloadByte(11) & 0x0F;
275
- tbody.circuit = cbody.circuit = 12;
276
- if ((msg.extractPayloadByte(3) & 0x08) === 8) {
277
- // This is the first circuit on the second body.
278
- tbody.temp = state.temps.waterSensor3;
279
- tbody.isOn = true;
280
- } else tbody.isOn = false;
281
- }
282
- if (sys.bodies.length > 3) {
283
- state.temps.waterSensor4 = fnTempFromByte(msg.extractPayloadByte(21));
284
- // const tbody: BodyTempState = state.temps.bodies.getItemById(19, true);
285
- const tbody: BodyTempState = state.temps.bodies.getItemById(4, true);
286
- const cbody: Body = sys.bodies.getItemById(4);
287
- tbody.name = cbody.name;
288
- tbody.heatMode = cbody.heatMode;
289
- tbody.setPoint = cbody.setPoint;
290
- tbody.heatStatus = (msg.extractPayloadByte(11) & 0xF0) >> 4;
291
- tbody.circuit = cbody.circuit = 22;
292
- if ((msg.extractPayloadByte(5) & 0x20) === 32) {
293
- // This is the first circuit on the third body or the first circuit on the second expansion.
294
- tbody.temp = state.temps.waterSensor2;
295
- tbody.isOn = true;
296
- } else tbody.isOn = false;
297
- }
298
- state.temps.air = fnTempFromByte(msg.extractPayloadByte(18)); // 18
299
- state.temps.solarSensor1 = fnTempFromByte(msg.extractPayloadByte(19)); // 19
300
- if (sys.bodies.length > 2 || sys.equipment.dual)
301
- state.temps.solarSensor2 = fnTempFromByte(msg.extractPayloadByte(17));
302
- if ((sys.bodies.length > 2))
303
- state.temps.solarSensor3 = fnTempFromByte(msg.extractPayloadByte(22));
304
- if ((sys.bodies.length > 3))
305
- state.temps.solarSensor4 = fnTempFromByte(msg.extractPayloadByte(23));
306
-
307
- if (sys.general.options.clockSource !== 'server' || typeof sys.general.options.adjustDST === 'undefined') sys.general.options.adjustDST = (msg.extractPayloadByte(23) & 0x01) === 0x0; //23
308
- }
309
- else {
310
- state.temps.waterSensor1 = fnTempFromByte(msg.extractPayloadByte(14));
311
- state.temps.air = fnTempFromByte(msg.extractPayloadByte(18));
312
- let solar: Heater = sys.heaters.getItemById(2);
313
- if (solar.isActive) state.temps.solar = fnTempFromByte(msg.extractPayloadByte(19));
314
- //[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]
315
- // byte | val |
316
- // 0 | 15 | Hours
317
- // 1 | 34 | Minutes
318
- // 2 | 32 | Circuits 1-8 bit 6 = Pool on.
319
- // 3 | 0 | Circuits 9-16
320
- // 4 | 0 | Circuits 17-24
321
- // 5 | 0 | Circuits 24-32
322
- // 6 | 0 | Circuits 33-40
323
- // 7 | 0 | Unknown
324
- // 8 | 0 | Unknown
325
- // 9 | 0 | Panel Mode bit flags
326
- // 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)
327
- // 11 | 0 | Unknown (This could be the heat status for body 3 & 4)
328
- // 12 | 0 | Unknown
329
- // 13 | 0 | Unknown
330
- // 14 | 81 | Water sensor 1 temperature
331
- // 15 | 81 | Water sensor 2 temperature (This mirrors water sensor 1 in shared system)
332
- // 16 | 32 | Unknown
333
- // 17 | 91 | Solar sensor 1 temperature
334
- // 18 | 82 | Air temp
335
- // 19 | 91 | Solar sensor 2 temperature (this mirrors solar sensor 1 in shared system)
336
- // 20 | 0 | Unknown (this could be water sensor 3)
337
- // 21 | 0 | Unknown (this could be water sensor 4)
338
- // 22 | 7 | Body 1 & 2 heat mode (body 1 = Solar Only body 2 = Heater)
339
- // 23 | 4 | Body 3 & 4 heat mode
340
- // 24 | 0 | Unknown
341
- // 25 | 77 | Unknown
342
- // 26 | 163 | Unknown
343
- // 27 | 1 | Byte 2 of OCP identifier
344
- // 28 | 0 | Byte 1 of OCP identifier
345
-
346
-
347
- // Heat Modes
348
- // 1 = Heater
349
- // 2 = Solar Preferred
350
- // 3 = Solar Only
351
-
352
- // Heat Status
353
- // 0 = Off
354
- // 1 = Heater
355
- // 2 = Cooling
356
- // 3 = Solar/Heat Pump
357
-
358
- // Pool Heat Mode/Status.
359
- // 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
360
- // lower nibble changed on bit 3. So 0100 0111 from 0100 0011
361
-
362
- // Spa Heat Mode/Status
363
- // 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
364
- // 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
365
- // automatically like the spa has manual heat turned off.
366
- // 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
367
- // solar temp is 79 so the solar should not be coming on.
368
- // 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.
369
- // 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
370
- // 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
371
- // an OCP where the manual heat was turned off.
372
-
373
- // RKS: Added check for i10d for water sensor 2.
374
- if (sys.bodies.length > 2 || sys.equipment.dual) state.temps.waterSensor2 = fnTempFromByte(msg.extractPayloadByte(15));
375
- if (sys.bodies.length > 0) {
376
- // const tbody: BodyTempState = state.temps.bodies.getItemById(6, true);
377
- const tbody: BodyTempState = state.temps.bodies.getItemById(1, true);
378
- const cbody: Body = sys.bodies.getItemById(1);
379
- if ((msg.extractPayloadByte(2) & 0x20) === 32) {
380
- tbody.temp = state.temps.waterSensor1;
381
- tbody.isOn = true;
382
- } else tbody.isOn = false;
383
- tbody.setPoint = cbody.setPoint;
384
- tbody.name = cbody.name;
385
- tbody.circuit = cbody.circuit = 6;
386
-
387
- //RKS: This heat mode did not include all the bits necessary for hybrid heaters
388
- //tbody.heatMode = cbody.heatMode = msg.extractPayloadByte(22) & 0x03;
389
- tbody.heatMode = cbody.heatMode = msg.extractPayloadByte(22) & 0x33;
390
- let heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
391
- if (tbody.isOn) {
392
- if (tbody.heaterOptions.hybrid > 0) {
393
- // ETi When heating with
394
- // Heatpump (1) = 12 H:true S:false C:false
395
- // Gas (2) = 48 H:false S:true C:false
396
- // Hybrid (3) = 48 H:true S:false C:false
397
- // Dual (16) = 60 H:true S:true C:false
398
- // What this means is that Touch actually treats the heat status as either heating with
399
- // the primary heater for the body or the secondary. In the case of a hybrid heater
400
- // the primary is a heatpump and the secondary is gas. In the case of gas + solar or gas + heatpump
401
- // the gas heater is the primary and solar or heatpump is the secondary. So we need to dance a little bit
402
- // here. We do this by checking the heater options.
403
- if (tbody.heatMode > 0) { // Turns out that ET sometimes reports the last heat status when off.
404
- // This can be the only heater solar cannot be installed with this.
405
- let byte = msg.extractPayloadByte(10);
406
- // Either the primary, secondary, or both is engaged.
407
- if ((byte & 0x14) === 0x14) heatStatus = sys.board.valueMaps.heatStatus.getValue('dual');
408
- // else if ((byte & 0x0c) === 0x0c) heatStatus = sys.board.valueMaps.heatStatus.getValue('off'); // don't need since we test for heatMode>0
409
- else if (byte & 0x10) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
410
- else if (byte & 0x04) heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
411
- }
412
- }
413
- else {
414
- //const heaterActive = (msg.extractPayloadByte(10) & 0x0C) === 12;
415
- //const solarActive = (msg.extractPayloadByte(10) & 0x30) === 48;
416
- const heaterActive = (msg.extractPayloadByte(10) & 0x04) === 0x04;
417
- const solarActive = (msg.extractPayloadByte(10) & 0x10) === 0x10;
418
- const cooling = solarActive && tbody.temp > tbody.setPoint;
419
- if (heaterActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
420
- if (cooling) heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
421
- else if (solarActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
422
- }
423
- }
424
- tbody.heatStatus = heatStatus;
425
- sys.board.schedules.syncScheduleHeatSourceAndSetpoint(cbody, tbody);
426
- }
427
- if (sys.bodies.length > 1) {
428
- // const tbody: BodyTempState = state.temps.bodies.getItemById(1, true);
429
- const tbody: BodyTempState = state.temps.bodies.getItemById(2, true);
430
- const cbody: Body = sys.bodies.getItemById(2);
431
- if ((msg.extractPayloadByte(2) & 0x01) === 1) {
432
- tbody.temp = sys.equipment.shared ? state.temps.waterSensor1 : state.temps.waterSensor2;
433
- tbody.isOn = true;
434
- } else tbody.isOn = false;
435
- //RKS: This heat mode did not include all the bits necessary for hybrid heaters
436
- //tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(22) & 0x0C) >> 2;
437
- tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(22) & 0xCC) >> 2;
438
- tbody.setPoint = cbody.setPoint;
439
- tbody.name = cbody.name;
440
- tbody.circuit = cbody.circuit = 1;
441
- let heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
442
- if (tbody.isOn) {
443
- if (tbody.heaterOptions.hybrid > 0) {
444
- // This can be the only heater solar cannot be installed with this.
445
- if (tbody.heatMode > 0) {
446
- let byte = msg.extractPayloadByte(10);
447
- // Either the primary, secondary, or both is engaged.
448
- if ((byte & 0x28) === 0x28) heatStatus = sys.board.valueMaps.heatStatus.getValue('dual');
449
- else if (byte & 0x20) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
450
- else if (byte & 0x08) heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
451
- }
452
- }
453
- else {
454
- //const heaterActive = (msg.extractPayloadByte(10) & 0x0C) === 12;
455
- //const solarActive = (msg.extractPayloadByte(10) & 0x30) === 48;
456
- const heaterActive = (msg.extractPayloadByte(10) & 0x08) === 0x08;
457
- const solarActive = (msg.extractPayloadByte(10) & 0x20) === 0x20;
458
- const cooling = solarActive && tbody.temp > tbody.setPoint;
459
- if (heaterActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
460
- if (cooling) heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
461
- else if (solarActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
462
- }
463
- }
464
- tbody.heatStatus = heatStatus;
465
- sys.board.schedules.syncScheduleHeatSourceAndSetpoint(cbody, tbody);
466
- }
467
- }
468
- switch (sys.controllerType) {
469
- case ControllerType.IntelliCenter:
470
- {
471
- EquipmentStateMessage.processCircuitState(msg);
472
- // RKS: As of 1.04 the entire feature state is emitted on 204. This message
473
- // used to contain the first 4 feature states starting in byte 8 upper 4 bits
474
- // and as of 1.047 release this was no longer reliable. Macro circuits only appear
475
- // to be available on message 30-15 and 168-15.
476
- //EquipmentStateMessage.processFeatureState(msg);
477
- sys.board.circuits.syncCircuitRelayStates();
478
- sys.board.circuits.syncVirtualCircuitStates();
479
- sys.board.valves.syncValveStates();
480
- sys.board.filters.syncFilterStates();
481
- state.emitControllerChange();
482
- state.emitEquipmentChanges();
483
- sys.board.heaters.syncHeaterStates();
484
- break;
485
- }
486
- case ControllerType.SunTouch:
487
- EquipmentStateMessage.processSunTouchCircuits(msg);
488
- sys.board.circuits.syncCircuitRelayStates();
489
- sys.board.features.syncGroupStates();
490
- sys.board.circuits.syncVirtualCircuitStates();
491
- sys.board.valves.syncValveStates();
492
- sys.board.filters.syncFilterStates();
493
- state.emitControllerChange();
494
- state.emitEquipmentChanges();
495
- sys.board.heaters.syncHeaterStates();
496
- sys.board.schedules.syncScheduleStates();
497
- break;
498
- case ControllerType.EasyTouch:
499
- case ControllerType.IntelliCom:
500
- case ControllerType.IntelliTouch:
501
- {
502
- EquipmentStateMessage.processTouchCircuits(msg);
503
- // This will toggle the group states depending on the state of the individual circuits.
504
- sys.board.circuits.syncCircuitRelayStates();
505
- sys.board.features.syncGroupStates();
506
- sys.board.circuits.syncVirtualCircuitStates();
507
- sys.board.valves.syncValveStates();
508
- sys.board.filters.syncFilterStates();
509
- state.emitControllerChange();
510
- state.emitEquipmentChanges();
511
- sys.board.heaters.syncHeaterStates();
512
- sys.board.schedules.syncScheduleStates();
513
- break;
514
- }
515
- }
516
- }
517
- break;
518
- case 5: // Intellitouch only. Date/Time packet
519
- // [255,0,255][165,1,15,16,5,8][15,10,8,1,8,18,0,1][1,15]
520
- state.time.hours = msg.extractPayloadByte(0);
521
- state.time.minutes = msg.extractPayloadByte(1);
522
- // state.time.dayOfWeek = msg.extractPayloadByte(2);
523
- state.time.date = msg.extractPayloadByte(3);
524
- state.time.month = msg.extractPayloadByte(4);
525
- state.time.year = msg.extractPayloadByte(5);
526
- if (sys.general.options.clockSource !== 'server' || typeof sys.general.options.adjustDST === 'undefined') sys.general.options.adjustDST = msg.extractPayloadByte(7) === 0x01;
527
- setTimeout(function () { sys.board.checkConfiguration(); }, 100);
528
- msg.isProcessed = true;
529
- break;
530
- case 8: {
531
- // IntelliTouch only. Heat status
532
- // [165,x,15,16,8,13],[75,75,64,87,101,11,0, 0 ,62 ,0 ,0 ,0 ,0] ,[2,190]
533
- // Heat Modes
534
- // 1 = Heater
535
- // 2 = Solar Preferred
536
- // 3 = Solar Only
537
- //[81, 81, 82, 85, 97, 7, 0, 0, 0, 100, 100, 4, 0][3, 87]
538
- // byte | val |
539
- // 0 | 81 | Water sensor 1
540
- // 1 | 81 | Unknown (Probably water sensor 2 on a D)
541
- // 2 | 82 | Air sensor
542
- // 3 | 85 | Body 1 setpoint
543
- // 4 | 97 | Body 2 setpoint
544
- // 5 | 7 | Body 1 & 2 heat mode. (0111) (Pool = 11 Solar only/Spa = 01 Heater)
545
- // 6 | 0 | Unknown (Water Sensor 3)
546
- // 7 | 0 | Unknown (Water Sensor 4)
547
- // 8 | 0 | Unknown -- Reserved air sensor
548
- // 9 | 100 | Unknown (Body 3 setpoint)
549
- // 10 | 100 | Unknown (Body 4 setpoint)
550
- // 11 | 4 | Unknown (Body 3 & 4 head mode. (0010) (Pool = 00 = Off/ 10 = Solar Preferred)
551
- // 12 | 0 | Unknown
552
- // 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
553
- // is for the remaining 2 bodies. The second half of this message mirrors the values for the second 136 message.
554
- // [255, 0, 255][165, 1, 16, 32, 136, 4][100, 100, 4, 1][2, 47]
555
- state.temps.waterSensor1 = msg.extractPayloadByte(0);
556
- state.temps.air = msg.extractPayloadByte(2);
557
- let solar: Heater = sys.heaters.getItemById(2);
558
- // RKS: 05-18-22 - This is not correct the solar temp is not stored on this message. It is always 0
559
- // on an intelliTouch system with solar.
560
- //if (solar.isActive) state.temps.solar = msg.extractPayloadByte(8);
561
- // pool
562
- let tbody: BodyTempState = state.temps.bodies.getItemById(1, true);
563
- let cbody: Body = sys.bodies.getItemById(1);
564
- // RKS: 02-26-22 - See communications doc for explanation of bits. This needs to support UltraTemp ETi heatpumps.
565
- tbody.heatMode = cbody.heatMode = msg.extractPayloadByte(5) & 0x33;
566
- tbody.setPoint = cbody.setPoint = msg.extractPayloadByte(3);
567
- tbody.coolSetpoint = cbody.coolSetpoint = msg.extractPayloadByte(9);
568
- if (tbody.isOn) tbody.temp = state.temps.waterSensor1;
569
- cbody = sys.bodies.getItemById(2);
570
- if (cbody.isActive) {
571
- // spa
572
- tbody = state.temps.bodies.getItemById(2, true);
573
- tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(5) & 0xCC) >> 2;
574
- //tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(5) & 12) >> 2;
575
- tbody.setPoint = cbody.setPoint = msg.extractPayloadByte(4);
576
- if (tbody.isOn) tbody.temp = state.temps.waterSensor2 = msg.extractPayloadByte(1);
577
- }
578
- state.emitEquipmentChanges();
579
- msg.isProcessed = true;
580
- break;
581
- }
582
- case 96:
583
- EquipmentStateMessage.processIntelliBriteMode(msg);
584
- break;
585
- case 197: {
586
- // request for date/time on *Touch. Use this as an indicator
587
- // that SL has requested config and update lastUpdated date/time
588
- /* let ver: ConfigVersion =
589
- typeof (sys.configVersion) === 'undefined' ? new ConfigVersion({}) : sys.configVersion;
590
- ver.lastUpdated = new Date();
591
- sys.processVersionChanges(ver); */
592
- sys.configVersion.lastUpdated = new Date();
593
- msg.isProcessed = true;
594
- break;
595
- }
596
- case 204: // IntelliCenter only.
597
- state.batteryVoltage = msg.extractPayloadByte(2) / 50;
598
- state.comms.keepAlives = msg.extractPayloadInt(4);
599
- state.time.year = msg.extractPayloadByte(8);
600
- state.time.month = msg.extractPayloadByte(7);
601
- state.time.date = msg.extractPayloadByte(6);
602
- sys.equipment.controllerFirmware = (msg.extractPayloadByte(42) + (msg.extractPayloadByte(43) / 1000)).toString();
603
- if (sys.chlorinators.length > 0) {
604
- if (msg.extractPayloadByte(37, 255) !== 255) {
605
- const chlor = state.chlorinators.getItemById(1);
606
- chlor.superChlorRemaining = msg.extractPayloadByte(37) * 3600 + msg.extractPayloadByte(38) * 60;
607
- } else {
608
- const chlor = state.chlorinators.getItemById(1);
609
- chlor.superChlorRemaining = 0;
610
- chlor.superChlor = false;
611
- }
612
- }
613
- ExternalMessage.processFeatureState(9, msg);
614
- //if (sys.equipment.dual === true) {
615
- // // For IntelliCenter i10D the body state is on byte 26 of the 204. This impacts circuit 6.
616
- // let byte = msg.extractPayloadByte(26);
617
- // let pstate = state.circuits.getItemById(6, true);
618
- // let oldstate = pstate.isOn;
619
- // pstate.isOn = ((byte & 0x0010) === 0x0010);
620
- // logger.info(`Checking i10D pool state ${byte} old:${oldstate} new: ${pstate.isOn}`);
621
- // //if (oldstate !== pstate.isOn) {
622
- // state.temps.bodies.getItemById(1, true).isOn = pstate.isOn;
623
- // sys.board.circuits.syncCircuitRelayStates();
624
- // sys.board.circuits.syncVirtualCircuitStates();
625
- // sys.board.valves.syncValveStates();
626
- // sys.board.filters.syncFilterStates();
627
- // sys.board.heaters.syncHeaterStates();
628
- // //}
629
- // if (oldstate !== pstate.isOn) pstate.emitEquipmentChange();
630
- //}
631
- // At this point normally on is ignored. Not sure what this does.
632
- let cover1 = sys.covers.getItemById(1);
633
- let cover2 = sys.covers.getItemById(2);
634
- if (cover1.isActive) {
635
- let scover1 = state.covers.getItemById(1, true);
636
- scover1.name = cover1.name;
637
- state.temps.bodies.getItemById(cover1.body + 1).isCovered = scover1.isClosed = (msg.extractPayloadByte(30) & 0x0001) > 0;
638
- }
639
- if (cover2.isActive) {
640
- let scover2 = state.covers.getItemById(2, true);
641
- scover2.name = cover2.name;
642
- state.temps.bodies.getItemById(cover2.body + 1).isCovered = scover2.isClosed = (msg.extractPayloadByte(30) & 0x0002) > 0;
643
- }
644
- sys.board.schedules.syncScheduleStates();
645
- msg.isProcessed = true;
646
- state.emitEquipmentChanges();
647
- break;
648
- }
649
- }
650
- private static processCircuitState(msg: Inbound) {
651
- // The way this works is that there is one byte per 8 circuits for a total of 5 bytes or 40 circuits. The
652
- // configuration already determined how many available circuits we have by querying the model of the panel
653
- // and any installed expansion panel models. Only the number of available circuits will appear in this
654
- // array.
655
- let circuitId = 1;
656
- let maxCircuitId = sys.board.equipmentIds.circuits.end;
657
- for (let i = 2; i < msg.payload.length && circuitId <= maxCircuitId; i++) {
658
- const byte = msg.extractPayloadByte(i);
659
- // Shift each bit getting the circuit identified by each value.
660
- for (let j = 0; j < 8; j++) {
661
- let circuit = sys.circuits.getItemById(circuitId, false, { isActive: false });
662
- if (circuit.isActive !== false) {
663
- let cstate = state.circuits.getItemById(circuitId, circuit.isActive);
664
- // For IntelliCenter i10D body circuits are not reported here.
665
- let isOn = ((circuitId === 6 || circuitId === 1) && sys.equipment.dual === true) ? cstate.isOn : (byte & (1 << j)) > 0;
666
- //let isOn = (byte & (1 << j)) > 0;
667
- cstate.isOn = isOn;
668
- cstate.name = circuit.name;
669
- cstate.nameId = circuit.nameId;
670
- cstate.showInFeatures = circuit.showInFeatures;
671
- cstate.type = circuit.type;
672
- sys.board.circuits.setEndTime(circuit, cstate, isOn);
673
- if (sys.controllerType === ControllerType.IntelliCenter) {
674
- // intellitouch sends a separate msg with themes
675
- switch (circuit.type) {
676
- case 6: // Globrite
677
- case 5: // Magicstream
678
- case 8: // Intellibrite
679
- case 10: // Colorcascade
680
- cstate.lightingTheme = circuit.lightingTheme;
681
- break;
682
- case 9:
683
- cstate.level = circuit.level || 0;
684
- break;
685
- }
686
- }
687
- }
688
- circuitId++;
689
- }
690
- }
691
- msg.isProcessed = true;
692
- }
693
- private static processSunTouchCircuits(msg: Inbound) {
694
- // SunTouch has really twisted bit mapping for its
695
- // circuit states. Features are intertwined within the
696
- // features.
697
- let byte = msg.extractPayloadByte(2);
698
- for (let i = 0; i < 8; i++) {
699
- let id = i === 4 ? 7 : i > 5 ? i + 2 : i + 1;
700
- let circ = sys.circuits.getInterfaceById(id, false, { isActive: false });
701
- if (circ.isActive) {
702
- let isOn = ((1 << i) & byte) > 0;
703
- let cstate = state.circuits.getInterfaceById(id, circ.isActive);
704
- if (isOn !== cstate.isOn) {
705
- sys.board.circuits.setEndTime(circ, cstate, isOn);
706
- cstate.isOn = isOn;
707
- }
708
- }
709
- }
710
- byte = msg.extractPayloadByte(3);
711
- {
712
- let circ = sys.circuits.getInterfaceById(10, false, { isActive: false });
713
- if (circ.isActive) {
714
- let isOn = (byte & 1) > 0;
715
- let cstate = state.circuits.getInterfaceById(circ.id, circ.isActive);
716
- if (isOn !== cstate.isOn) {
717
- sys.board.circuits.setEndTime(circ, cstate, isOn);
718
- cstate.isOn = isOn;
719
- }
720
- }
721
- }
722
- state.emitEquipmentChanges();
723
- msg.isProcessed = true;
724
- }
725
-
726
- private static processTouchCircuits(msg: Inbound) {
727
- let circuitId = 1;
728
- let maxCircuitId = sys.board.equipmentIds.features.end;
729
- for (let i = 2; i < msg.payload.length && circuitId <= maxCircuitId; i++) {
730
- const byte = msg.extractPayloadByte(i);
731
- // Shift each bit getting the circuit identified by each value.
732
- for (let j = 0; j < 8; j++) {
733
- const circ = sys.circuits.getInterfaceById(circuitId, false, { isActive: false });
734
- if (!sys.board.equipmentIds.invalidIds.isValidId(circuitId)) {
735
- circ.isActive = false;
736
- }
737
- if (circ.isActive) {
738
- const cstate = state.circuits.getInterfaceById(circuitId, circ.isActive);
739
- cstate.showInFeatures = circ.showInFeatures;
740
- let isOn = (byte & 1 << j) >> j > 0;
741
- if (isOn !== cstate.isOn) {
742
- sys.board.circuits.setEndTime(circ, cstate, isOn);
743
- cstate.isOn = isOn;
744
- }
745
- cstate.name = circ.name;
746
- cstate.type = circ.type;
747
- cstate.nameId = circ.nameId;
748
- }
749
- else {
750
- if (circ instanceof Circuit) {
751
- sys.circuits.removeItemById(circuitId);
752
- // don't forget to remove from state #257
753
- state.circuits.removeItemById(circuitId);
754
- }
755
- else if (circ instanceof Feature) {
756
- sys.features.removeItemById(circuitId);
757
- // don't forget to remove from state #257
758
- state.features.removeItemById(circuitId);
759
- }
760
- }
761
- circuitId++;
762
- }
763
- }
764
- // state.body = body;
765
- //state.emitControllerChange();
766
- state.emitEquipmentChanges();
767
- msg.isProcessed = true;
768
- }
769
-
770
- private static processIntelliBriteMode(msg: Inbound) {
771
- // eg RED: [165,16,16,34,96,2],[195,0],[2,12]
772
- // data[0] = color
773
- const theme = msg.extractPayloadByte(0);
774
- switch (theme) {
775
- case 0: // off
776
- case 1: // on
777
- case 190: // save
778
- // case 191: // recall
779
- // 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.
780
- // do nothing as these don't actually change the state.
781
- break;
782
-
783
- default:
784
- {
785
- // intellibrite themes
786
- // 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
787
- // stored. Once the message is sent then it throws away the data. When you turn the light
788
- // on again it will be on at whatever theme happened to be set at the time it went off. We keep this
789
- // as a best guess so when the user turns on the light it will likely be the last theme observed.
790
- const grp = sys.lightGroups.getItemById(sys.board.equipmentIds.circuitGroups.start);
791
- const sgrp = state.lightGroups.getItemById(sys.board.equipmentIds.circuitGroups.start);
792
- grp.lightingTheme = sgrp.lightingTheme = theme;
793
- for (let i = 0; i < grp.circuits.length; i++) {
794
- let c = grp.circuits.getItemByIndex(i);
795
- let cstate = state.circuits.getItemById(c.circuit);
796
- let circuit = sys.circuits.getInterfaceById(c.circuit);
797
- if (cstate.isOn) cstate.lightingTheme = circuit.lightingTheme = theme;
798
- }
799
- switch (theme) {
800
- case 128: // sync
801
- sys.board.circuits.sequenceLightGroupAsync(grp.id, 'sync');
802
- break;
803
- case 144: // swim
804
- sys.board.circuits.sequenceLightGroupAsync(grp.id, 'swim');
805
- break;
806
- case 160: // set
807
- sys.board.circuits.sequenceLightGroupAsync(grp.id, 'set');
808
- break;
809
- case 190: // save
810
- case 191: // recall
811
- sys.board.circuits.sequenceLightGroupAsync(grp.id, 'other');
812
- break;
813
- default:
814
- sys.board.circuits.sequenceLightGroupAsync(grp.id, 'color');
815
- // other themes for magicstream?
816
- }
817
- break;
818
- }
819
- }
820
- msg.isProcessed = true;
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
+ export class EquipmentStateMessage {
31
+ private static initIntelliCenter(msg: Inbound) {
32
+ sys.controllerType = ControllerType.IntelliCenter;
33
+ sys.equipment.maxSchedules = 100;
34
+ sys.equipment.maxFeatures = 32;
35
+ // Always get equipment since this is volatile between loads. Everything else takes care of itself.
36
+ sys.configVersion.equipment = 0;
37
+ }
38
+ public static initDefaults() {
39
+ // defaults; set to lowest possible values. Each *Touch will extend this once we know the model.
40
+ sys.equipment.maxBodies = 1;
41
+ sys.equipment.maxCircuits = 6;
42
+ sys.equipment.maxSchedules = 12;
43
+ sys.equipment.maxPumps = 2;
44
+ sys.equipment.maxSchedules = 12;
45
+ sys.equipment.maxValves = 2;
46
+ sys.equipment.maxCircuitGroups = 0;
47
+ sys.equipment.maxLightGroups = 1;
48
+ sys.equipment.maxIntelliBrites = 8;
49
+ sys.equipment.maxChemControllers = sys.equipment.maxChlorinators = 1;
50
+ sys.equipment.maxCustomNames = 10;
51
+ sys.equipment.maxChemControllers = 4;
52
+ sys.equipment.maxFeatures = 8;
53
+ sys.equipment.model = 'Unknown';
54
+ }
55
+ private static initTouch(msg: Inbound) {
56
+ let model1 = msg.extractPayloadByte(27);
57
+ let model2 = msg.extractPayloadByte(28);
58
+ switch (model2) {
59
+ case 0:
60
+ case 1:
61
+ case 2:
62
+ case 3:
63
+ case 4:
64
+ case 5:
65
+ logger.info(`Found IntelliTouch Controller`);
66
+ sys.controllerType = ControllerType.IntelliTouch;
67
+ model1 = msg.extractPayloadByte(28);
68
+ model2 = msg.extractPayloadByte(9);
69
+ (sys.board as IntelliTouchBoard).initExpansionModules(model1, model2);
70
+ break;
71
+ case 11:
72
+ logger.info(`Found SunTouch Controller`);
73
+ sys.controllerType = ControllerType.SunTouch;
74
+ (sys.board as SunTouchBoard).initExpansionModules(model1, model2);
75
+ break;
76
+ case 13:
77
+ case 14:
78
+ logger.info(`Found EasyTouch Controller`);
79
+ sys.controllerType = ControllerType.EasyTouch;
80
+ (sys.board as EasyTouchBoard).initExpansionModules(model1, model2);
81
+ break;
82
+ default:
83
+ logger.error(`Unknown Touch Controller ${msg.extractPayloadByte(28)}:${msg.extractPayloadByte(27)}`);
84
+ break;
85
+ }
86
+ }
87
+ private static initController(msg: Inbound) {
88
+ const model1 = msg.extractPayloadByte(27);
89
+ const model2 = msg.extractPayloadByte(28);
90
+ // RKS: 06-15-20 -- While this works for now the way we are detecting seems a bit dubious. First, the 2 status message
91
+ // contains two model bytes. Right now the ones witness in the wild include 23 = fw1.023, 40 = fw1.040, 47 = fw1.047.
92
+ // RKS: 07-21-22 -- Pentair is about to release fw1.232. Unfortunately, the byte mapping for this has changed such that
93
+ // the bytes [27,28] are [0,2] respectively. This looks like it might be in conflict with IntelliTouch but it is not. Below
94
+ // are the combinations of 27,28 we have seen for IntelliTouch
95
+ // [1,0] = i5+3
96
+ // [0,1] = i7+3
97
+ // [1,3] = i5+3s
98
+ // [1,4] = i9+3s
99
+ // [1,5] = i10+3d
100
+ // IntelliCenter v3.004 reports [3,2] for bytes [27,28]
101
+ if ((model2 === 0 && (model1 === 23 || model1 >= 40)) ||
102
+ (model2 === 2 && model1 == 0) ||
103
+ (model2 === 2 && model1 == 3)) {
104
+ state.equipment.controllerType = 'intellicenter';
105
+ sys.board.modulesAcquired = false;
106
+ sys.controllerType = ControllerType.IntelliCenter;
107
+ logger.info(`Found Controller Board ${state.equipment.model || 'IntelliCenter'}, awaiting installed modules.`);
108
+ EquipmentStateMessage.initIntelliCenter(msg);
109
+ }
110
+ else {
111
+ EquipmentStateMessage.initTouch(msg);
112
+ sys.board.needsConfigChanges = true;
113
+ setTimeout(function () { sys.checkConfiguration(); }, 300);
114
+ }
115
+ // Set status = 1 AFTER controllerType change, because the controllerType setter
116
+ // resets state.status = 0 during RESETTING DATA
117
+ state.status = 1;
118
+ }
119
+ public static process(msg: Inbound) {
120
+ Message.headerSubByte = msg.header[1];
121
+ //console.log(process.memoryUsage());
122
+ if (msg.action === 2 && state.isInitialized && sys.controllerType === ControllerType.Nixie) {
123
+ // Start over because we didn't have communication before but we now do.
124
+ // Close the nixie board first, then initialize with the new controller type.
125
+ // Fix: Call initController AFTER the async close completes to avoid race condition.
126
+ (async () => {
127
+ await sys.board.closeAsync();
128
+ logger.info(`Closed ${sys.controllerType} board`);
129
+ sys.controllerType = ControllerType.Unknown;
130
+ state.status = 0;
131
+ // Now initialize the correct controller type after nixie is closed
132
+ EquipmentStateMessage.initController(msg);
133
+ })();
134
+ return; // Don't continue processing until async close/init completes
135
+ }
136
+ // If controller type is unknown (e.g., after a replay/system reset), we must re-detect the controller on Action 2
137
+ // even if state has been initialized from disk.
138
+ if (!state.isInitialized || sys.controllerType === ControllerType.Unknown) {
139
+ msg.isProcessed = true;
140
+ if (msg.action === 2) EquipmentStateMessage.initController(msg);
141
+ else return;
142
+ }
143
+ else if (!sys.board.modulesAcquired) {
144
+ msg.isProcessed = true;
145
+ if (msg.action === 204) {
146
+ // We have determined that the 204 message now contains the information
147
+ // related to the installed expansion boards.
148
+ logger.info(`INTELLICENTER MODULES DETECTED, REQUESTING STATUS!`);
149
+
150
+ // IMPORTANT: v3 module decoding depends on `sys.equipment.isIntellicenterV3`, which is gated by controller firmware.
151
+ // During the "modules not acquired yet" bootstrap we must set firmware BEFORE calling `initExpansionModules()`
152
+ // so v3 systems (e.g. i10PS shared) are decoded with the correct nibble order.
153
+ if (msg.payload.length >= 44) {
154
+ sys.equipment.controllerFirmware = (msg.extractPayloadByte(42) + (msg.extractPayloadByte(43) / 1000)).toString();
155
+ }
156
+ // Master = 13-14
157
+ // EXP1 = 15-16
158
+ // EXP2 = 17-18
159
+ let pc = msg.extractPayloadByte(40);
160
+ (sys.board as IntelliCenterBoard).initExpansionModules(msg.extractPayloadByte(13), msg.extractPayloadByte(14),
161
+ pc & 0x01 ? msg.extractPayloadByte(15) : 0x00, pc & 0x01 ? msg.extractPayloadByte(16) : 0x00,
162
+ pc & 0x02 ? msg.extractPayloadByte(17) : 0x00, pc & 0x02 ? msg.extractPayloadByte(18) : 0x00,
163
+ pc & 0x04 ? msg.extractPayloadByte(19) : 0x00, pc & 0x04 ? msg.extractPayloadByte(20) : 0x00);
164
+ sys.equipment.setEquipmentIds();
165
+ }
166
+ else return;
167
+ }
168
+ switch (msg.action) {
169
+ case 2:
170
+ {
171
+ let fnTempFromByte = function (byte) {
172
+ return byte;
173
+ //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.
174
+ }
175
+
176
+ // Shared
177
+ let dt = new Date();
178
+ // RKS: This was moved to the ChemControllerState message. This is flawed in that it incorrectly sets IntelliChem to no comms.
179
+ //if (state.chemControllers.length > 0) {
180
+ // // TODO: move this to chemController when we understand the packets better
181
+ // for (let i = 0; i < state.chemControllers.length; i++) {
182
+ // let ccontroller = state.chemControllers.getItemByIndex(i);
183
+ // if (sys.board.valueMaps.chemControllerTypes.getName(ccontroller.type) === 'intellichem') {
184
+ // if (dt.getTime() - ccontroller.lastComm > 60000) ccontroller.status = 1;
185
+ // }
186
+ // }
187
+ //}
188
+ state.time.hours = msg.extractPayloadByte(0);
189
+ state.time.minutes = msg.extractPayloadByte(1);
190
+ state.time.seconds = dt.getSeconds();
191
+ state.mode = sys.controllerType !== ControllerType.IntelliCenter ? (msg.extractPayloadByte(9) & 0x81) : (msg.extractPayloadByte(9) & 0x01);
192
+
193
+ // 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
194
+ // 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
195
+ // since pressure, volume, and length have been introduced.
196
+ sys.general.options.units = state.temps.units = msg.extractPayloadByte(9) & 0x04;
197
+ state.valve = msg.extractPayloadByte(10);
198
+
199
+
200
+ // RSG - added 7/8/2020
201
+ // Every 30 mins, check the timezone and adjust DST settings
202
+ if (dt.getMinutes() % 30 === 0) {
203
+ sys.board.system.setTZ();
204
+ sys.board.schedules.updateSunriseSunsetAsync().then((updated: boolean)=>{
205
+ if (updated) {logger.debug(`Sunrise/sunset times updated on schedules.`);}
206
+ });
207
+ }
208
+ // Check and update clock when it is off by >5 mins (just for a small buffer) and:
209
+ // 1. IntelliCenter has "manual" time set (Internet will automatically adjust) and autoAdjustDST is enabled
210
+ // 2. *Touch is "manual" (only option) and autoAdjustDST is enabled - (same as #1)
211
+ // 3. clock source is "server" isn't an OCP option but can be enabled on the clients
212
+ if (dt.getMinutes() % 5 === 0 && dt.getSeconds() <= 10 && sys.general.options.clockSource === 'server') {
213
+ if ((Math.abs(dt.getTime() - state.time.getTime()) > 60 * 2 * 1000) && !state.time.isUpdating) {
214
+ state.time.isUpdating = true;
215
+ sys.board.system.setDateTimeAsync({ dt, dst: sys.general.options.adjustDST || 0, })
216
+ .then(() => {
217
+ logger.info(`njsPC automatically updated OCP time. You're welcome.`);
218
+ })
219
+ .catch((err) => {
220
+ logger.error(`Error automatically setting system time. ${JSON.stringify(err)}`)
221
+ })
222
+ .finally(() => {
223
+ state.time.isUpdating = false;
224
+ })
225
+ }
226
+ }
227
+ state.delay = msg.extractPayloadByte(12) & 63; // not sure what 64 val represents
228
+ state.freeze = (msg.extractPayloadByte(9) & 0x08) === 0x08;
229
+ if (sys.controllerType === ControllerType.IntelliCenter) {
230
+ state.temps.waterSensor1 = fnTempFromByte(msg.extractPayloadByte(14));
231
+ // IntelliCenter: for 2-body non-shared systems, byte(15) is Body2 (Spa) water sensor.
232
+ // Previously gated behind (>2 bodies || dual), which left Spa temp undefined and rendered as "--" in dashPanel.
233
+ if (sys.bodies.length > 1 || sys.equipment.dual) state.temps.waterSensor2 = fnTempFromByte(msg.extractPayloadByte(15));
234
+ // We are making an assumption here in that the circuits are always labeled the same.
235
+ // 1=Spa/Body2
236
+ // 6=Pool/Body1
237
+ // 12=Body3
238
+ // 22=Body4 -- Really not sure about this one.
239
+ if (sys.bodies.length > 0) {
240
+ // We will not go in here if this is not a shared body.
241
+ const tbody: BodyTempState = state.temps.bodies.getItemById(1, true);
242
+ const cbody: Body = sys.bodies.getItemById(1);
243
+ tbody.heatMode = cbody.heatMode;
244
+ tbody.setPoint = cbody.setPoint;
245
+ tbody.name = cbody.name;
246
+ tbody.circuit = cbody.circuit = 6;
247
+ tbody.heatStatus = msg.extractPayloadByte(11) & 0x0F;
248
+ // With the IntelliCenter i10D, bit 6 is not reliable. It is not set properly and requires the 204 message
249
+ // to process the data.
250
+ if (!sys.equipment.dual) {
251
+ if ((msg.extractPayloadByte(2) & 0x20) === 32) {
252
+ tbody.temp = state.temps.waterSensor1;
253
+ tbody.isOn = true;
254
+ } else tbody.isOn = false;
255
+ }
256
+ else if (state.circuits.getItemById(6).isOn === true) {
257
+ tbody.temp = state.temps.waterSensor1;
258
+ tbody.isOn = true;
259
+ }
260
+ else tbody.isOn = false;
261
+ }
262
+ if (sys.bodies.length > 1) {
263
+ const tbody: BodyTempState = state.temps.bodies.getItemById(2, true);
264
+ const cbody: Body = sys.bodies.getItemById(2);
265
+ tbody.heatMode = cbody.heatMode;
266
+ tbody.setPoint = cbody.setPoint;
267
+ tbody.name = cbody.name;
268
+ tbody.circuit = cbody.circuit = 1;
269
+ tbody.heatStatus = (msg.extractPayloadByte(11) & 0xF0) >> 4;
270
+ if (!sys.equipment.dual) {
271
+ if ((msg.extractPayloadByte(2) & 0x01) === 1) {
272
+ tbody.temp = sys.equipment.shared ? state.temps.waterSensor1 : state.temps.waterSensor2;
273
+ tbody.isOn = true;
274
+ } else tbody.isOn = false;
275
+ } else if (state.circuits.getItemById(1).isOn === true) {
276
+ tbody.temp = sys.equipment.shared ? state.temps.waterSensor1 : state.temps.waterSensor2;
277
+ tbody.isOn = true;
278
+ }
279
+ else tbody.isOn = false;
280
+ }
281
+ if (sys.bodies.length > 2) {
282
+ state.temps.waterSensor3 = fnTempFromByte(msg.extractPayloadByte(20));
283
+ // const tbody: BodyTempState = state.temps.bodies.getItemById(10, true);
284
+ const tbody: BodyTempState = state.temps.bodies.getItemById(3, true);
285
+ const cbody: Body = sys.bodies.getItemById(3);
286
+ tbody.name = cbody.name;
287
+ tbody.heatMode = cbody.heatMode;
288
+ tbody.setPoint = cbody.setPoint;
289
+ tbody.heatStatus = msg.extractPayloadByte(11) & 0x0F;
290
+ tbody.circuit = cbody.circuit = 12;
291
+ if ((msg.extractPayloadByte(3) & 0x08) === 8) {
292
+ // This is the first circuit on the second body.
293
+ tbody.temp = state.temps.waterSensor3;
294
+ tbody.isOn = true;
295
+ } else tbody.isOn = false;
296
+ }
297
+ if (sys.bodies.length > 3) {
298
+ state.temps.waterSensor4 = fnTempFromByte(msg.extractPayloadByte(21));
299
+ // const tbody: BodyTempState = state.temps.bodies.getItemById(19, true);
300
+ const tbody: BodyTempState = state.temps.bodies.getItemById(4, true);
301
+ const cbody: Body = sys.bodies.getItemById(4);
302
+ tbody.name = cbody.name;
303
+ tbody.heatMode = cbody.heatMode;
304
+ tbody.setPoint = cbody.setPoint;
305
+ tbody.heatStatus = (msg.extractPayloadByte(11) & 0xF0) >> 4;
306
+ tbody.circuit = cbody.circuit = 22;
307
+ if ((msg.extractPayloadByte(5) & 0x20) === 32) {
308
+ // This is the first circuit on the third body or the first circuit on the second expansion.
309
+ tbody.temp = state.temps.waterSensor2;
310
+ tbody.isOn = true;
311
+ } else tbody.isOn = false;
312
+ }
313
+ state.temps.air = fnTempFromByte(msg.extractPayloadByte(18)); // 18
314
+ state.temps.solarSensor1 = fnTempFromByte(msg.extractPayloadByte(19)); // 19
315
+ if (sys.bodies.length > 2 || sys.equipment.dual)
316
+ state.temps.solarSensor2 = fnTempFromByte(msg.extractPayloadByte(17));
317
+ if ((sys.bodies.length > 2))
318
+ state.temps.solarSensor3 = fnTempFromByte(msg.extractPayloadByte(22));
319
+ if ((sys.bodies.length > 3))
320
+ state.temps.solarSensor4 = fnTempFromByte(msg.extractPayloadByte(23));
321
+
322
+ if (sys.general.options.clockSource !== 'server' || typeof sys.general.options.adjustDST === 'undefined') sys.general.options.adjustDST = (msg.extractPayloadByte(23) & 0x01) === 0x0; //23
323
+ }
324
+ else {
325
+ state.temps.waterSensor1 = fnTempFromByte(msg.extractPayloadByte(14));
326
+ state.temps.air = fnTempFromByte(msg.extractPayloadByte(18));
327
+ let solar: Heater = sys.heaters.getItemById(2);
328
+ if (solar.isActive) state.temps.solar = fnTempFromByte(msg.extractPayloadByte(19));
329
+ //[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]
330
+ // byte | val |
331
+ // 0 | 15 | Hours
332
+ // 1 | 34 | Minutes
333
+ // 2 | 32 | Circuits 1-8 bit 6 = Pool on.
334
+ // 3 | 0 | Circuits 9-16
335
+ // 4 | 0 | Circuits 17-24
336
+ // 5 | 0 | Circuits 24-32
337
+ // 6 | 0 | Circuits 33-40
338
+ // 7 | 0 | Unknown
339
+ // 8 | 0 | Unknown
340
+ // 9 | 0 | Panel Mode bit flags
341
+ // 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)
342
+ // 11 | 0 | Unknown (This could be the heat status for body 3 & 4)
343
+ // 12 | 0 | Unknown
344
+ // 13 | 0 | Unknown
345
+ // 14 | 81 | Water sensor 1 temperature
346
+ // 15 | 81 | Water sensor 2 temperature (This mirrors water sensor 1 in shared system)
347
+ // 16 | 32 | Unknown
348
+ // 17 | 91 | Solar sensor 1 temperature
349
+ // 18 | 82 | Air temp
350
+ // 19 | 91 | Solar sensor 2 temperature (this mirrors solar sensor 1 in shared system)
351
+ // 20 | 0 | Unknown (this could be water sensor 3)
352
+ // 21 | 0 | Unknown (this could be water sensor 4)
353
+ // 22 | 7 | Body 1 & 2 heat mode (body 1 = Solar Only body 2 = Heater)
354
+ // 23 | 4 | Body 3 & 4 heat mode
355
+ // 24 | 0 | Unknown
356
+ // 25 | 77 | Unknown
357
+ // 26 | 163 | Unknown
358
+ // 27 | 1 | Byte 2 of OCP identifier
359
+ // 28 | 0 | Byte 1 of OCP identifier
360
+
361
+
362
+ // Heat Modes
363
+ // 1 = Heater
364
+ // 2 = Solar Preferred
365
+ // 3 = Solar Only
366
+
367
+ // Heat Status
368
+ // 0 = Off
369
+ // 1 = Heater
370
+ // 2 = Cooling
371
+ // 3 = Solar/Heat Pump
372
+
373
+ // Pool Heat Mode/Status.
374
+ // 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
375
+ // lower nibble changed on bit 3. So 0100 0111 from 0100 0011
376
+
377
+ // Spa Heat Mode/Status
378
+ // 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
379
+ // 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
380
+ // automatically like the spa has manual heat turned off.
381
+ // 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
382
+ // solar temp is 79 so the solar should not be coming on.
383
+ // 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.
384
+ // 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
385
+ // 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
386
+ // an OCP where the manual heat was turned off.
387
+
388
+ // RKS: Added check for i10d for water sensor 2.
389
+ if (sys.bodies.length > 2 || sys.equipment.dual) state.temps.waterSensor2 = fnTempFromByte(msg.extractPayloadByte(15));
390
+ if (sys.bodies.length > 0) {
391
+ // const tbody: BodyTempState = state.temps.bodies.getItemById(6, true);
392
+ const tbody: BodyTempState = state.temps.bodies.getItemById(1, true);
393
+ const cbody: Body = sys.bodies.getItemById(1);
394
+ if ((msg.extractPayloadByte(2) & 0x20) === 32) {
395
+ tbody.temp = state.temps.waterSensor1;
396
+ tbody.isOn = true;
397
+ } else tbody.isOn = false;
398
+ tbody.setPoint = cbody.setPoint;
399
+ tbody.name = cbody.name;
400
+ tbody.circuit = cbody.circuit = 6;
401
+
402
+ //RKS: This heat mode did not include all the bits necessary for hybrid heaters
403
+ //tbody.heatMode = cbody.heatMode = msg.extractPayloadByte(22) & 0x03;
404
+ tbody.heatMode = cbody.heatMode = msg.extractPayloadByte(22) & 0x33;
405
+ let heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
406
+ if (tbody.isOn) {
407
+ if (tbody.heaterOptions.hybrid > 0) {
408
+ // ETi When heating with
409
+ // Heatpump (1) = 12 H:true S:false C:false
410
+ // Gas (2) = 48 H:false S:true C:false
411
+ // Hybrid (3) = 48 H:true S:false C:false
412
+ // Dual (16) = 60 H:true S:true C:false
413
+ // What this means is that Touch actually treats the heat status as either heating with
414
+ // the primary heater for the body or the secondary. In the case of a hybrid heater
415
+ // the primary is a heatpump and the secondary is gas. In the case of gas + solar or gas + heatpump
416
+ // the gas heater is the primary and solar or heatpump is the secondary. So we need to dance a little bit
417
+ // here. We do this by checking the heater options.
418
+ if (tbody.heatMode > 0) { // Turns out that ET sometimes reports the last heat status when off.
419
+ // This can be the only heater solar cannot be installed with this.
420
+ let byte = msg.extractPayloadByte(10);
421
+ // Either the primary, secondary, or both is engaged.
422
+ if ((byte & 0x14) === 0x14) heatStatus = sys.board.valueMaps.heatStatus.getValue('dual');
423
+ // else if ((byte & 0x0c) === 0x0c) heatStatus = sys.board.valueMaps.heatStatus.getValue('off'); // don't need since we test for heatMode>0
424
+ else if (byte & 0x10) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
425
+ else if (byte & 0x04) heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
426
+ }
427
+ }
428
+ else {
429
+ //const heaterActive = (msg.extractPayloadByte(10) & 0x0C) === 12;
430
+ //const solarActive = (msg.extractPayloadByte(10) & 0x30) === 48;
431
+ const heaterActive = (msg.extractPayloadByte(10) & 0x04) === 0x04;
432
+ const solarActive = (msg.extractPayloadByte(10) & 0x10) === 0x10;
433
+ const cooling = solarActive && tbody.temp > tbody.setPoint;
434
+ if (heaterActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
435
+ if (cooling) heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
436
+ else if (solarActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
437
+ }
438
+ }
439
+ tbody.heatStatus = heatStatus;
440
+ sys.board.schedules.syncScheduleHeatSourceAndSetpoint(cbody, tbody);
441
+ }
442
+ if (sys.bodies.length > 1) {
443
+ // const tbody: BodyTempState = state.temps.bodies.getItemById(1, true);
444
+ const tbody: BodyTempState = state.temps.bodies.getItemById(2, true);
445
+ const cbody: Body = sys.bodies.getItemById(2);
446
+ if ((msg.extractPayloadByte(2) & 0x01) === 1) {
447
+ tbody.temp = sys.equipment.shared ? state.temps.waterSensor1 : state.temps.waterSensor2;
448
+ tbody.isOn = true;
449
+ } else tbody.isOn = false;
450
+ //RKS: This heat mode did not include all the bits necessary for hybrid heaters
451
+ //tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(22) & 0x0C) >> 2;
452
+ tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(22) & 0xCC) >> 2;
453
+ tbody.setPoint = cbody.setPoint;
454
+ tbody.name = cbody.name;
455
+ tbody.circuit = cbody.circuit = 1;
456
+ let heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
457
+ if (tbody.isOn) {
458
+ if (tbody.heaterOptions.hybrid > 0) {
459
+ // This can be the only heater solar cannot be installed with this.
460
+ if (tbody.heatMode > 0) {
461
+ let byte = msg.extractPayloadByte(10);
462
+ // Either the primary, secondary, or both is engaged.
463
+ if ((byte & 0x28) === 0x28) heatStatus = sys.board.valueMaps.heatStatus.getValue('dual');
464
+ else if (byte & 0x20) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
465
+ else if (byte & 0x08) heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
466
+ }
467
+ }
468
+ else {
469
+ //const heaterActive = (msg.extractPayloadByte(10) & 0x0C) === 12;
470
+ //const solarActive = (msg.extractPayloadByte(10) & 0x30) === 48;
471
+ const heaterActive = (msg.extractPayloadByte(10) & 0x08) === 0x08;
472
+ const solarActive = (msg.extractPayloadByte(10) & 0x20) === 0x20;
473
+ const cooling = solarActive && tbody.temp > tbody.setPoint;
474
+ if (heaterActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
475
+ if (cooling) heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
476
+ else if (solarActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
477
+ }
478
+ }
479
+ tbody.heatStatus = heatStatus;
480
+ sys.board.schedules.syncScheduleHeatSourceAndSetpoint(cbody, tbody);
481
+ }
482
+ }
483
+ switch (sys.controllerType) {
484
+ case ControllerType.IntelliCenter:
485
+ {
486
+ EquipmentStateMessage.processCircuitState(msg);
487
+ // v3.004+: DISABLED - Action 2 bytes 7-8 use non-bitmask encoding
488
+ // Feature state for v3.004+ comes from Action 30 case 15 responses only
489
+ // See: https://github.com/tagyoureit/nodejs-poolController/issues/XXX
490
+ // if (sys.equipment.isIntellicenterV3) {
491
+ // EquipmentStateMessage.processFeatureStateV3(msg);
492
+ // }
493
+ sys.board.circuits.syncCircuitRelayStates();
494
+ sys.board.circuits.syncVirtualCircuitStates();
495
+ sys.board.valves.syncValveStates();
496
+ sys.board.filters.syncFilterStates();
497
+ state.emitControllerChange();
498
+ state.emitEquipmentChanges();
499
+ sys.board.heaters.syncHeaterStates();
500
+ break;
501
+ }
502
+ case ControllerType.SunTouch:
503
+ EquipmentStateMessage.processSunTouchCircuits(msg);
504
+ sys.board.circuits.syncCircuitRelayStates();
505
+ sys.board.features.syncGroupStates();
506
+ sys.board.circuits.syncVirtualCircuitStates();
507
+ sys.board.valves.syncValveStates();
508
+ sys.board.filters.syncFilterStates();
509
+ state.emitControllerChange();
510
+ state.emitEquipmentChanges();
511
+ sys.board.heaters.syncHeaterStates();
512
+ sys.board.schedules.syncScheduleStates();
513
+ break;
514
+ case ControllerType.EasyTouch:
515
+ case ControllerType.IntelliCom:
516
+ case ControllerType.IntelliTouch:
517
+ {
518
+ EquipmentStateMessage.processTouchCircuits(msg);
519
+ // This will toggle the group states depending on the state of the individual circuits.
520
+ sys.board.circuits.syncCircuitRelayStates();
521
+ sys.board.features.syncGroupStates();
522
+ sys.board.circuits.syncVirtualCircuitStates();
523
+ sys.board.valves.syncValveStates();
524
+ sys.board.filters.syncFilterStates();
525
+ state.emitControllerChange();
526
+ state.emitEquipmentChanges();
527
+ sys.board.heaters.syncHeaterStates();
528
+ sys.board.schedules.syncScheduleStates();
529
+ break;
530
+ }
531
+ }
532
+ }
533
+ break;
534
+ case 5: // Intellitouch only. Date/Time packet
535
+ // [255,0,255][165,1,15,16,5,8][15,10,8,1,8,18,0,1][1,15]
536
+ state.time.hours = msg.extractPayloadByte(0);
537
+ state.time.minutes = msg.extractPayloadByte(1);
538
+ // state.time.dayOfWeek = msg.extractPayloadByte(2);
539
+ state.time.date = msg.extractPayloadByte(3);
540
+ state.time.month = msg.extractPayloadByte(4);
541
+ state.time.year = msg.extractPayloadByte(5);
542
+ if (sys.general.options.clockSource !== 'server' || typeof sys.general.options.adjustDST === 'undefined') sys.general.options.adjustDST = msg.extractPayloadByte(7) === 0x01;
543
+ setTimeout(function () { sys.board.checkConfiguration(); }, 100);
544
+ msg.isProcessed = true;
545
+ break;
546
+ case 8: {
547
+ // IntelliTouch only. Heat status
548
+ // [165,x,15,16,8,13],[75,75,64,87,101,11,0, 0 ,62 ,0 ,0 ,0 ,0] ,[2,190]
549
+ // Heat Modes
550
+ // 1 = Heater
551
+ // 2 = Solar Preferred
552
+ // 3 = Solar Only
553
+ //[81, 81, 82, 85, 97, 7, 0, 0, 0, 100, 100, 4, 0][3, 87]
554
+ // byte | val |
555
+ // 0 | 81 | Water sensor 1
556
+ // 1 | 81 | Unknown (Probably water sensor 2 on a D)
557
+ // 2 | 82 | Air sensor
558
+ // 3 | 85 | Body 1 setpoint
559
+ // 4 | 97 | Body 2 setpoint
560
+ // 5 | 7 | Body 1 & 2 heat mode. (0111) (Pool = 11 Solar only/Spa = 01 Heater)
561
+ // 6 | 0 | Unknown (Water Sensor 3)
562
+ // 7 | 0 | Unknown (Water Sensor 4)
563
+ // 8 | 0 | Unknown -- Reserved air sensor
564
+ // 9 | 100 | Unknown (Body 3 setpoint)
565
+ // 10 | 100 | Unknown (Body 4 setpoint)
566
+ // 11 | 4 | Unknown (Body 3 & 4 head mode. (0010) (Pool = 00 = Off/ 10 = Solar Preferred)
567
+ // 12 | 0 | Unknown
568
+ // 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
569
+ // is for the remaining 2 bodies. The second half of this message mirrors the values for the second 136 message.
570
+ // [255, 0, 255][165, 1, 16, 32, 136, 4][100, 100, 4, 1][2, 47]
571
+ state.temps.waterSensor1 = msg.extractPayloadByte(0);
572
+ state.temps.air = msg.extractPayloadByte(2);
573
+ let solar: Heater = sys.heaters.getItemById(2);
574
+ // RKS: 05-18-22 - This is not correct the solar temp is not stored on this message. It is always 0
575
+ // on an intelliTouch system with solar.
576
+ //if (solar.isActive) state.temps.solar = msg.extractPayloadByte(8);
577
+ // pool
578
+ let tbody: BodyTempState = state.temps.bodies.getItemById(1, true);
579
+ let cbody: Body = sys.bodies.getItemById(1);
580
+ // RKS: 02-26-22 - See communications doc for explanation of bits. This needs to support UltraTemp ETi heatpumps.
581
+ tbody.heatMode = cbody.heatMode = msg.extractPayloadByte(5) & 0x33;
582
+ tbody.setPoint = cbody.setPoint = msg.extractPayloadByte(3);
583
+ tbody.coolSetpoint = cbody.coolSetpoint = msg.extractPayloadByte(9);
584
+ if (tbody.isOn) tbody.temp = state.temps.waterSensor1;
585
+ cbody = sys.bodies.getItemById(2);
586
+ if (cbody.isActive) {
587
+ // spa
588
+ tbody = state.temps.bodies.getItemById(2, true);
589
+ tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(5) & 0xCC) >> 2;
590
+ //tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(5) & 12) >> 2;
591
+ tbody.setPoint = cbody.setPoint = msg.extractPayloadByte(4);
592
+ if (tbody.isOn) tbody.temp = state.temps.waterSensor2 = msg.extractPayloadByte(1);
593
+ }
594
+ state.emitEquipmentChanges();
595
+ msg.isProcessed = true;
596
+ break;
597
+ }
598
+ case 96:
599
+ EquipmentStateMessage.processIntelliBriteMode(msg);
600
+ break;
601
+ case 179: {
602
+ // v3.004+ Action 179 - Heartbeat REQUEST from OCP
603
+ // OCP sends Action 179 TO specific device (dest=33 for njsPC, dest=36 for wireless)
604
+ // Device must respond with Action 180 TO OCP (dest=16)
605
+ if (msg.dest === Message.pluginAddress) {
606
+ // OCP is pinging us specifically - respond with Action 180
607
+ logger.silly(`Received heartbeat request (Action 179) from OCP, responding with Action 180`);
608
+ const response: Outbound = Outbound.create({
609
+ dest: 16, // Respond to OCP (16)
610
+ action: 180, // Action 180 = heartbeat response
611
+ payload: Array(16).fill(0), // 16 zeros (observed from wireless remote)
612
+ retries: 0 // Don't retry heartbeat responses
613
+ });
614
+ response.sendAsync().catch(err => {
615
+ // Log but don't fail on heartbeat errors
616
+ logger.silly(`Heartbeat response error: ${err.message}`);
617
+ });
618
+ }
619
+ msg.isProcessed = true;
620
+ break;
621
+ }
622
+ case 184: {
623
+ msg.isProcessed = true;
624
+ break;
625
+ }
626
+ case 217: {
627
+ // v3.004+ Action 217 - Device list broadcast
628
+ // OCP broadcasts registered devices after Action 251→253 handshake
629
+ // Each packet contains info for ONE device
630
+ // Check if this packet is for njsPC (device 33) and update registration status
631
+ if (msg.payload.length > 2 && msg.extractPayloadByte(0) === Message.pluginAddress) {
632
+ const registrationStatus = msg.extractPayloadByte(2);
633
+ // status: 0=unknown, 1=registered, 4=stale/needs-reauth (NOT rejection)
634
+ if (sys.controllerType === ControllerType.IntelliCenter) {
635
+ (sys.board as IntelliCenterBoard).setRegistrationStatus(registrationStatus);
636
+ }
637
+ }
638
+ msg.isProcessed = true;
639
+ break;
640
+ }
641
+ case 197: {
642
+ // request for date/time on *Touch. Use this as an indicator
643
+ // that SL has requested config and update lastUpdated date/time
644
+ /* let ver: ConfigVersion =
645
+ typeof (sys.configVersion) === 'undefined' ? new ConfigVersion({}) : sys.configVersion;
646
+ ver.lastUpdated = new Date();
647
+ sys.processVersionChanges(ver); */
648
+ sys.configVersion.lastUpdated = new Date();
649
+ msg.isProcessed = true;
650
+ break;
651
+ }
652
+ case 204: // IntelliCenter only.
653
+ state.batteryVoltage = msg.extractPayloadByte(2) / 50;
654
+ state.comms.keepAlives = msg.extractPayloadInt(4);
655
+ state.time.year = msg.extractPayloadByte(8);
656
+ state.time.month = msg.extractPayloadByte(7);
657
+ state.time.date = msg.extractPayloadByte(6);
658
+ sys.equipment.controllerFirmware = (msg.extractPayloadByte(42) + (msg.extractPayloadByte(43) / 1000)).toString();
659
+ // v3.004 adds 4 additional bytes (44-46) that are the time of day
660
+ // Byte 44: Hour (0-23)
661
+ // Byte 45: Minute (0-59)
662
+ // Byte 46: Second (0-59)
663
+ // Byte 47: Unknown - possibly DST indicator or status flag
664
+ if (sys.chlorinators.length > 0) {
665
+ if (msg.extractPayloadByte(37, 255) !== 255) {
666
+ const chlor = state.chlorinators.getItemById(1);
667
+ chlor.superChlorRemaining = msg.extractPayloadByte(37) * 3600 + msg.extractPayloadByte(38) * 60;
668
+ } else {
669
+ const chlor = state.chlorinators.getItemById(1);
670
+ chlor.superChlorRemaining = 0;
671
+ chlor.superChlor = false;
672
+ }
673
+ }
674
+ // v3.004+: Do NOT process feature states from Action 204!
675
+ // Evidence from packet captures shows Action 204 byte 19 contains STALE feature state
676
+ // that doesn't update when features change. The authoritative source for v3 feature
677
+ // state is Action 30 case 15 (config response to Action 222 [15,0] request).
678
+ // Action 204 continuously broadcasts stale data and overwrites the correct state.
679
+ //
680
+ // v1.x: Feature states at offset 9 - this was deemed reliable in 2020.
681
+ if (!sys.equipment.isIntellicenterV3) {
682
+ ExternalMessage.processFeatureState(9, msg);
683
+ }
684
+ //if (sys.equipment.dual === true) {
685
+ // // For IntelliCenter i10D the body state is on byte 26 of the 204. This impacts circuit 6.
686
+ // let byte = msg.extractPayloadByte(26);
687
+ // let pstate = state.circuits.getItemById(6, true);
688
+ // let oldstate = pstate.isOn;
689
+ // pstate.isOn = ((byte & 0x0010) === 0x0010);
690
+ // logger.info(`Checking i10D pool state ${byte} old:${oldstate} new: ${pstate.isOn}`);
691
+ // //if (oldstate !== pstate.isOn) {
692
+ // state.temps.bodies.getItemById(1, true).isOn = pstate.isOn;
693
+ // sys.board.circuits.syncCircuitRelayStates();
694
+ // sys.board.circuits.syncVirtualCircuitStates();
695
+ // sys.board.valves.syncValveStates();
696
+ // sys.board.filters.syncFilterStates();
697
+ // sys.board.heaters.syncHeaterStates();
698
+ // //}
699
+ // if (oldstate !== pstate.isOn) pstate.emitEquipmentChange();
700
+ //}
701
+ // At this point normally on is ignored. Not sure what this does.
702
+ let cover1 = sys.covers.getItemById(1);
703
+ let cover2 = sys.covers.getItemById(2);
704
+ if (cover1.isActive) {
705
+ let scover1 = state.covers.getItemById(1, true);
706
+ scover1.name = cover1.name;
707
+ state.temps.bodies.getItemById(cover1.body + 1).isCovered = scover1.isClosed = (msg.extractPayloadByte(30) & 0x0001) > 0;
708
+ }
709
+ if (cover2.isActive) {
710
+ let scover2 = state.covers.getItemById(2, true);
711
+ scover2.name = cover2.name;
712
+ state.temps.bodies.getItemById(cover2.body + 1).isCovered = scover2.isClosed = (msg.extractPayloadByte(30) & 0x0002) > 0;
713
+ }
714
+ sys.board.schedules.syncScheduleStates();
715
+ msg.isProcessed = true;
716
+ state.emitEquipmentChanges();
717
+ break;
718
+ }
719
+ }
720
+ private static processCircuitState(msg: Inbound) {
721
+ // The way this works is that there is one byte per 8 circuits for a total of 5 bytes or 40 circuits. The
722
+ // configuration already determined how many available circuits we have by querying the model of the panel
723
+ // and any installed expansion panel models. Only the number of available circuits will appear in this
724
+ // array.
725
+ let circuitId = 1;
726
+ let maxCircuitId = sys.board.equipmentIds.circuits.end;
727
+ for (let i = 2; i < msg.payload.length && circuitId <= maxCircuitId; i++) {
728
+ const byte = msg.extractPayloadByte(i);
729
+ // Shift each bit getting the circuit identified by each value.
730
+ for (let j = 0; j < 8; j++) {
731
+ let circuit = sys.circuits.getItemById(circuitId, false, { isActive: false });
732
+ if (circuit.isActive !== false) {
733
+ let cstate = state.circuits.getItemById(circuitId, circuit.isActive);
734
+ const wasOn = cstate.isOn;
735
+ // For IntelliCenter i10D body circuits are not reported here.
736
+ let isOn = ((circuitId === 6 || circuitId === 1) && sys.equipment.dual === true) ? cstate.isOn : (byte & (1 << j)) > 0;
737
+ //let isOn = (byte & (1 << j)) > 0;
738
+ cstate.isOn = isOn;
739
+ cstate.name = circuit.name;
740
+ cstate.nameId = circuit.nameId;
741
+ cstate.showInFeatures = circuit.showInFeatures;
742
+ cstate.type = circuit.type;
743
+ sys.board.circuits.setEndTime(circuit, cstate, isOn);
744
+ if (sys.controllerType === ControllerType.IntelliCenter) {
745
+ // intellitouch sends a separate msg with themes
746
+ switch (circuit.type) {
747
+ case 6: // Globrite
748
+ case 5: // Magicstream
749
+ case 8: // Intellibrite
750
+ case 10: // Colorcascade
751
+ cstate.lightingTheme = circuit.lightingTheme;
752
+ break;
753
+ case 9:
754
+ cstate.level = circuit.level || 0;
755
+ break;
756
+ }
757
+ }
758
+ }
759
+ circuitId++;
760
+ }
761
+ }
762
+ msg.isProcessed = true;
763
+ }
764
+ private static processSunTouchCircuits(msg: Inbound) {
765
+ // SunTouch has really twisted bit mapping for its
766
+ // circuit states. Features are intertwined within the
767
+ // features.
768
+ let byte = msg.extractPayloadByte(2);
769
+ for (let i = 0; i < 8; i++) {
770
+ let id = i === 4 ? 7 : i > 5 ? i + 2 : i + 1;
771
+ let circ = sys.circuits.getInterfaceById(id, false, { isActive: false });
772
+ if (circ.isActive) {
773
+ let isOn = ((1 << i) & byte) > 0;
774
+ let cstate = state.circuits.getInterfaceById(id, circ.isActive);
775
+ if (isOn !== cstate.isOn) {
776
+ sys.board.circuits.setEndTime(circ, cstate, isOn);
777
+ cstate.isOn = isOn;
778
+ }
779
+ }
780
+ }
781
+ byte = msg.extractPayloadByte(3);
782
+ {
783
+ let circ = sys.circuits.getInterfaceById(10, false, { isActive: false });
784
+ if (circ.isActive) {
785
+ let isOn = (byte & 1) > 0;
786
+ let cstate = state.circuits.getInterfaceById(circ.id, circ.isActive);
787
+ if (isOn !== cstate.isOn) {
788
+ sys.board.circuits.setEndTime(circ, cstate, isOn);
789
+ cstate.isOn = isOn;
790
+ }
791
+ }
792
+ }
793
+ state.emitEquipmentChanges();
794
+ msg.isProcessed = true;
795
+ }
796
+
797
+ private static processFeatureStateV3(msg: Inbound) {
798
+ // DISABLED: v3.004+ Action 2 bytes 7-8 do NOT use simple bitmask encoding!
799
+ //
800
+ // Analysis from replay 76 (Dec 2024):
801
+ // - F1 on → byte7=16 (bit 4), byte8=0
802
+ // - F1+F2 → byte7=32 (bit 5), byte8=1
803
+ // - F1+F2+F3 → byte7=64 (bit 6), byte8=2
804
+ //
805
+ // This is NOT a bitmask - it appears to be some kind of encoded state.
806
+ // Using this data corrupts feature state and causes wrong features to display.
807
+ //
808
+ // For v3.004+, feature state must come from:
809
+ // 1. Action 30 case 15 responses (when njsPC requests via Action 222)
810
+ // 2. TODO: Snoop on Action 30/15 going to Wireless (dest=36) for real-time updates
811
+ //
812
+ // DO NOT ENABLE THIS FUNCTION until the encoding is fully understood.
813
+ msg.isProcessed = true;
814
+ return;
815
+
816
+ // Original broken code kept for reference:
817
+ /*
818
+ const byte7 = msg.extractPayloadByte(7);
819
+ const byte8 = msg.extractPayloadByte(8);
820
+ const featureStateBits = byte7 | (byte8 << 8);
821
+
822
+ let featureId = sys.board.equipmentIds.features.start;
823
+ let maxFeatureId = sys.features.getMaxId(true, 0);
824
+
825
+ for (let j = 0; featureId <= maxFeatureId && j < 16; j++) {
826
+ let feature = sys.features.getItemById(featureId, false, { isActive: false });
827
+ if (feature.isActive !== false) {
828
+ let fstate = state.features.getItemById(featureId, true);
829
+ let isOn = (featureStateBits & (1 << j)) > 0;
830
+ sys.board.circuits.setEndTime(feature, fstate, isOn);
831
+ fstate.isOn = isOn;
832
+ fstate.name = feature.name;
833
+ }
834
+ else {
835
+ state.features.removeItemById(featureId);
836
+ }
837
+ featureId++;
838
+ }
839
+ state.emitEquipmentChanges();
840
+ */
841
+ msg.isProcessed = true;
842
+ }
843
+
844
+ private static processTouchCircuits(msg: Inbound) {
845
+ let circuitId = 1;
846
+ let maxCircuitId = sys.board.equipmentIds.features.end;
847
+ for (let i = 2; i < msg.payload.length && circuitId <= maxCircuitId; i++) {
848
+ const byte = msg.extractPayloadByte(i);
849
+ // Shift each bit getting the circuit identified by each value.
850
+ for (let j = 0; j < 8; j++) {
851
+ const circ = sys.circuits.getInterfaceById(circuitId, false, { isActive: false });
852
+ if (!sys.board.equipmentIds.invalidIds.isValidId(circuitId)) {
853
+ circ.isActive = false;
854
+ }
855
+ if (circ.isActive) {
856
+ const cstate = state.circuits.getInterfaceById(circuitId, circ.isActive);
857
+ cstate.showInFeatures = circ.showInFeatures;
858
+ let isOn = (byte & 1 << j) >> j > 0;
859
+ if (isOn !== cstate.isOn) {
860
+ sys.board.circuits.setEndTime(circ, cstate, isOn);
861
+ cstate.isOn = isOn;
862
+ }
863
+ cstate.name = circ.name;
864
+ cstate.type = circ.type;
865
+ cstate.nameId = circ.nameId;
866
+ }
867
+ else {
868
+ if (circ instanceof Circuit) {
869
+ sys.circuits.removeItemById(circuitId);
870
+ // don't forget to remove from state #257
871
+ state.circuits.removeItemById(circuitId);
872
+ }
873
+ else if (circ instanceof Feature) {
874
+ sys.features.removeItemById(circuitId);
875
+ // don't forget to remove from state #257
876
+ state.features.removeItemById(circuitId);
877
+ }
878
+ }
879
+ circuitId++;
880
+ }
881
+ }
882
+ // state.body = body;
883
+ //state.emitControllerChange();
884
+ state.emitEquipmentChanges();
885
+ msg.isProcessed = true;
886
+ }
887
+
888
+ private static processIntelliBriteMode(msg: Inbound) {
889
+ // eg RED: [165,16,16,34,96,2],[195,0],[2,12]
890
+ // data[0] = color
891
+ const theme = msg.extractPayloadByte(0);
892
+ switch (theme) {
893
+ case 0: // off
894
+ case 1: // on
895
+ case 190: // save
896
+ // case 191: // recall
897
+ // 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.
898
+ // do nothing as these don't actually change the state.
899
+ break;
900
+
901
+ default:
902
+ {
903
+ // intellibrite themes
904
+ // 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
905
+ // stored. Once the message is sent then it throws away the data. When you turn the light
906
+ // on again it will be on at whatever theme happened to be set at the time it went off. We keep this
907
+ // as a best guess so when the user turns on the light it will likely be the last theme observed.
908
+ const grp = sys.lightGroups.getItemById(sys.board.equipmentIds.circuitGroups.start);
909
+ const sgrp = state.lightGroups.getItemById(sys.board.equipmentIds.circuitGroups.start);
910
+ grp.lightingTheme = sgrp.lightingTheme = theme;
911
+ for (let i = 0; i < grp.circuits.length; i++) {
912
+ let c = grp.circuits.getItemByIndex(i);
913
+ let cstate = state.circuits.getItemById(c.circuit);
914
+ let circuit = sys.circuits.getInterfaceById(c.circuit);
915
+ if (cstate.isOn) cstate.lightingTheme = circuit.lightingTheme = theme;
916
+ }
917
+ switch (theme) {
918
+ case 128: // sync
919
+ sys.board.circuits.sequenceLightGroupAsync(grp.id, 'sync');
920
+ break;
921
+ case 144: // swim
922
+ sys.board.circuits.sequenceLightGroupAsync(grp.id, 'swim');
923
+ break;
924
+ case 160: // set
925
+ sys.board.circuits.sequenceLightGroupAsync(grp.id, 'set');
926
+ break;
927
+ case 190: // save
928
+ case 191: // recall
929
+ sys.board.circuits.sequenceLightGroupAsync(grp.id, 'other');
930
+ break;
931
+ default:
932
+ sys.board.circuits.sequenceLightGroupAsync(grp.id, 'color');
933
+ // other themes for magicstream?
934
+ }
935
+ break;
936
+ }
937
+ }
938
+ msg.isProcessed = true;
939
+ }
940
+ }