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,449 +1,449 @@
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 { Inbound } from "../Messages";
19
- import { state } from "../../../State";
20
- import { sys, ControllerType } from "../../../Equipment";
21
- import { logger } from "../../../../logger/Logger";
22
- import { webApp } from "../../../../web/Server";
23
- import { Timestamp, utils } from "../../../Constants"
24
- export class IntelliChemStateMessage {
25
- public static process(msg: Inbound) {
26
- if (sys.controllerType === ControllerType.Unknown) return;
27
- let address = (msg.dest >= 144 && msg.dest <= 158) ? msg.dest : msg.source;
28
- if (address < 144 || address > 158) return;
29
- let controller = sys.chemControllers.getItemByAddress(address);
30
- // RKS: 07-13-22 Lets just assume that SunTouch doesn't report its IntelliChem at this point. The action 40 return
31
- // does not contain the IntelliChem bit when it is returned for this controller.
32
- if (!controller.isActive && sys.controllerType !== ControllerType.SunTouch) {
33
- msg.isProcessed = true;
34
- return;
35
- }
36
- switch (msg.action) {
37
- // ---------- IntelliChem Control panel is spitting out its status ----------- //
38
- case 18: // IntelliChem is sending us it's status.
39
- IntelliChemStateMessage.processState(msg);
40
- break;
41
- case 210: // OCP is asking IntelliChem controller for it's current status info.
42
- // [165,0,144,16,210,1],[210],[2,234]
43
- let schem = state.chemControllers.getItemById(controller.id);
44
- if (schem.lastComm + (30 * 1000) < new Date().getTime()) {
45
- // We have not talked to the chem controller in 30 seconds so we have lost communication.
46
- schem.status = schem.alarms.comms = 1;
47
- }
48
- msg.isProcessed = true;
49
- break;
50
- // ---------- End IntelliChem set get ----------- //
51
-
52
- // ---------- OCP set get ----------- //
53
- case 19: // Request to OCP to return the status that ic currently has.
54
- // [165,14,16,34,19,1],[0],[0,249]
55
- break;
56
-
57
- /* RKS: This is processed in the IntellichemMessage.processTouch() and is the results of asking for the IntelliChem configuration.
58
- case 147: // OCP is broadcasting it's known ic values... Need to change our settings if virtual.
59
- // 147 is a proto:broadcast message;
60
- // it has exactly the same format as 18 but there is payload[0] which is inserted at the beginning. Likely the chem controller id.
61
- if (msg.dest < 144 || msg.dest > 158) return;
62
- IntelliChemStateMessage.processControllerChange(msg);
63
- break;
64
- // ---------- End OCP set get ----------- //
65
- */
66
- // ---------- ICP or SL set get ----------- //
67
- case 211: // SL or other controller is telling OCP to set IntelliChem value
68
- // It will take these values and pass them in 146 to IntelliChem
69
- // values that are NOT SET should be ignored
70
- msg.isProcessed = true;
71
- break;
72
- case 146: // OCP is telling IntelliChem that it needs to change its settings to...
73
- //let scontroller = state.chemControllers.getItemById(controller.id, true);
74
- //if (scontroller.lastComm + (30 * 1000) < new Date().getTime()) {
75
- // // We have not talked to the chem controller in 30 seconds so we have lost communication.
76
- // scontroller.status = scontroller.alarms.comms = 1;
77
- //}
78
- controller.ph.tank.capacity = controller.orp.tank.capacity = 6;
79
- controller.ph.tank.units = controller.orp.tank.units = '';
80
- msg.isProcessed = true;
81
- break;
82
- // ---------- OCP set get ----------- //
83
- }
84
- state.emitEquipmentChanges();
85
- }
86
- private static processControllerChange(msg: Inbound) {
87
- // We don't do anything with this inbound action 147 message.
88
- logger.info(`Incoming message from IntelliChem ${msg.toShortPacket()}`);
89
- msg.isProcessed = true;
90
- }
91
- private static processState(msg: Inbound) {
92
- if (msg.source < 144 || msg.source > 158) return;
93
-
94
- // Setup with CO2 dosing and IntelliChlor.
95
- //[2, 245, 2, 162, 2, 248, 2, 88, 0, 0, [0-9]
96
- // 0, 26, 0, 0, 0, 0, 0, 0, 0, 0, [10-19]
97
- // 7, 0, 249, 1, 94, 0, 81, 0, 80, 57, [20-29]
98
- // 0, 82, 0, 0, 162, 32, 80, 1, 0, 0, 0] [30-40]
99
- // Setup with 2 Tanks
100
- //[2, 238, 2, 208, 2, 248, 2, 188, 0, 0,
101
- // 0, 2, 0, 0, 0, 29, 0, 4, 0, 63,
102
- // 2, 2, 157, 0, 25, 0, 0, 0, 90, 20,
103
- // 0, 83, 0, 0, 149, 0, 60, 1, 1, 0, 0]
104
-
105
- // This is an action 18 that comes from IntelliChem. There is also a 147 that comes from an OCP but this is the raw data.
106
- //[165, 0, 16, 144, 18, 41][2,228,3,2,2,228,2,188,0,0,0,16,0,0,0,0,0,35,0,0,6,6,3,0,250,0,44,0,160,20,0,81,8,0,149,0,80,1,0,0,0]
107
- // Bytes - Descrption
108
- // 0-1 : pH byte(0) x 256 + byte(1) / 100
109
- // 2-3 : ORP byte(2) x 256 + byte(3)
110
- // 4-5 : pH Setpoint : byte(4) x 256 + byte(5) / 100
111
- // 6-7 : ORP Setpoint : byte(6) x 256 + byte(7)
112
- // 8 : Unknown = 0
113
- // 9 : Unknown = 0
114
- // 10-11 : pH Dose time seconds. The number of seconds since the dose started. byte(10) x 256 + byte(11)
115
- // 12: Unknown
116
- // 13 : Unknown
117
- // 14-15 : ORP Dose time seconds. The number of seconds since the dose started. byte(14) x 256 + byte(15)
118
- // 16-17 : pH Dose volume (unknown units) - These appear to be mL.
119
- // 18-19 : ORP Dose volume (unknown units) - These appear to be mL
120
- // 20 : pH tank level 1-7
121
- // 21 : ORP tank level 1-7
122
- // 22 : LSI. (byte(22) & 0x80) === 0x80 ? (256 - byte(22) / -100 : byte(22)/100
123
- // 23-24 : Calcium Hardness = byte(23) x 256 + byte(24) = 250
124
- // 25 : Unknown = 0 (probably reserved CYA byte so the chem is always dealing with integers)
125
- // 26 : CYA Max value = 210.
126
- // 27-28 : Alkalinity = byte(27) x 256 + byte(28)
127
- // 29 : Salt level = byte(29) x 50
128
- // 30 : Unknown
129
- // 31 : Temperature
130
- // 32 : Alarms
131
- // 33 : Warnings pH Lockout, Daily Limit Reached, Invalid Setup, Chlorinator Comm error
132
- // 34 : Dosing Status/Doser Type (pH Monitoring, ORP Mixing)
133
- // 35 : Delays
134
- // 36-37 : Firmware = 80,1 = 1.080
135
- // 38 : Water Chemistry Warning (Corrosion...)
136
- // 39 : Unknown
137
- // 40 : Unknown
138
- let address = msg.source;
139
-
140
- // The address is king here. The id is not.
141
- let chem = sys.chemControllers.getItemByAddress(address, true);
142
- let schem = state.chemControllers.getItemById(chem.id, true);
143
-
144
- // Get the doser types and set up our capabilities
145
- chem.ph.doserType = (msg.extractPayloadByte(34) & 0x03);
146
- chem.orp.doserType = (msg.extractPayloadByte(34) & 0x0C) >> 2;
147
- schem.ph.enabled = chem.ph.enabled = chem.ph.doserType !== 0;
148
- schem.ph.enabled = chem.orp.enabled = chem.orp.doserType !== 0;
149
- if (chem.ph.doserType === 2) schem.ph.chemType = 'CO2';
150
- else if (chem.ph.doserType === 0) schem.ph.chemType = 'none';
151
- else if (chem.ph.doserType === 1) schem.ph.chemType = 'acid';
152
- else if (chem.ph.doserType === 3) schem.ph.chemType = 'acid';
153
-
154
- if (chem.orp.doserType === 0) schem.orp.chemType = 'none';
155
- else schem.orp.chemType = 'orp';
156
-
157
- schem.isActive = chem.isActive = true;
158
- schem.status = 0;
159
- schem.type = chem.type = sys.board.valueMaps.chemControllerTypes.getValue('intellichem');
160
- chem.name = chem.name || `IntelliChem ${chem.address - 143}`; // default to true id if no name is set
161
- schem.lastComm = new Date().getTime();
162
- schem.status = schem.alarms.comms = 0;
163
- chem.ph.tank.capacity = chem.orp.tank.capacity = 6;
164
- chem.ph.tank.units = chem.orp.tank.units = '';
165
- chem.ph.tank.alarmEmptyEnabled = false;
166
- chem.ph.tank.alarmEmptyLevel = 1;
167
- chem.orp.tank.alarmEmptyEnabled = false;
168
- chem.orp.tank.alarmEmptyLevel = 1;
169
- schem.address = chem.address;
170
- schem.ph.level = schem.ph.probe.level = msg.extractPayloadIntBE(0) / 100;
171
- schem.orp.level = schem.orp.probe.level = msg.extractPayloadIntBE(2);
172
- chem.ph.setpoint = msg.extractPayloadIntBE(4) / 100;
173
- chem.orp.setpoint = msg.extractPayloadIntBE(6);
174
- // Missing information on the related bytes.
175
- // Bytes 8-14 (Probably Total Dissolved Solids in here if no IntelliChlor)
176
- let phPrev = { status: schem.ph.dosingStatus, time: schem.ph.timeDosed || 0, vol: schem.ph.volumeDosed };
177
- let orpPrev = { status: schem.orp.dosingStatus, time: schem.orp.timeDosed || 0, vol: schem.orp.volumeDosed };
178
- // IntelliChem never tells us what the dose time or volume is so we will let that dog lie.
179
- // 10-11 : pH Dose time
180
- schem.ph.timeDosed = (msg.extractPayloadByte(10) * 256) + msg.extractPayloadByte(11);
181
- // 14-15 : ORP Dose time seconds. The number of seconds since the dose started.
182
- schem.orp.timeDosed = (msg.extractPayloadByte(14) * 256) + msg.extractPayloadByte(15);
183
- // 16-17 : pH Dose volume (unknown units) = 35
184
- schem.ph.volumeDosed = (msg.extractPayloadByte(16) * 256) + msg.extractPayloadByte(17);
185
- // 18-19 : ORP Dose volume (unknown units) = 0
186
- schem.orp.volumeDosed = (msg.extractPayloadByte(18) * 256) + msg.extractPayloadByte(19);
187
- // 20 : pH tank level 1-7 = 6
188
- schem.ph.tank.level = Math.max(msg.extractPayloadByte(20) > 0 ? msg.extractPayloadByte(20) - 1 : msg.extractPayloadByte(20), 0); // values reported as 1-7; possibly 0 if no tank present
189
- // 21 : ORP tank level 1-7 = 6 if the tank levels report 0 then the chemical side is not enabled.
190
- schem.orp.tank.level = Math.max(msg.extractPayloadByte(21) > 0 ? msg.extractPayloadByte(21) - 1 : msg.extractPayloadByte(21), 0); // values reported as 1-7; possibly 0 if no tank present
191
-
192
- // 22 : LSI = 3 & 0x80 === 0x80 ? (256 - 3) / -100 : 3/100 = .03
193
- let lsi = msg.extractPayloadByte(22);
194
- schem.lsi = (lsi & 0x80) === 0x80 ? (256 - lsi) / -100 : lsi / 100;
195
- // 23-24 : Calcium Hardness = 0x256+250 = 250
196
- chem.calciumHardness = (msg.extractPayloadByte(23) * 256) + msg.extractPayloadByte(24);
197
- // 26 : CYA = 44
198
- chem.cyanuricAcid = msg.extractPayloadByte(26);
199
- // 27-28 : Alkalinity
200
- chem.alkalinity = (msg.extractPayloadByte(27) * 256) + msg.extractPayloadByte(28);
201
- // 29 : Salt level = 20
202
- if (sys.chlorinators.length > 0) {
203
- let chlor = state.chlorinators.find(elem => elem.id === 1);
204
- schem.orp.probe.saltLevel = (typeof chlor !== 'undefined') ? chlor.saltLevel : msg.extractPayloadByte(29) * 50;
205
- }
206
- else schem.orp.probe.saltLevel = 0;
207
- // 31 : Temperature = 81
208
- schem.ph.probe.temperature = msg.extractPayloadByte(31);
209
- schem.ph.probe.tempUnits = state.temps.units;
210
- // 32 : Alarms = 8 = (no alarm)
211
- const alarms = schem.alarms;
212
- alarms.flow = msg.extractPayloadByte(32) & 0x01;
213
- if (alarms.flow === 0) schem.flowDetected = true;
214
-
215
- // The pH and ORP alarms are in a word stupid for IntelliChem. So we are
216
- // going to override these.
217
- //alarms.pH = msg.extractPayloadByte(32) & 0x06;
218
- //alarms.orp = msg.extractPayloadByte(32) & 0x18;
219
- if (chem.ph.tolerance.enabled && schem.flowDetected) {
220
- if (schem.ph.level > chem.ph.tolerance.high) alarms.pH = 2;
221
- else if (schem.ph.level < chem.ph.tolerance.low) alarms.pH = 4;
222
- else alarms.pH = 0;
223
- }
224
- else alarms.pH = 0;
225
- if (chem.orp.tolerance.enabled && schem.flowDetected) {
226
- if (schem.orp.level > chem.orp.tolerance.high) alarms.orp = 8;
227
- else if (schem.orp.level < chem.orp.tolerance.low) alarms.orp = 16;
228
- else alarms.orp = 0;
229
- }
230
- else alarms.orp = 0;
231
- // IntelliChem will still send a tank empty alarm even if there is no tank.
232
- alarms.pHTank = (chem.ph.enabled && (chem.ph.doserType === 1 || chem.ph.doserType === 3)) ? msg.extractPayloadByte(32) & 0x20 : 0;
233
- alarms.orpTank = (chem.orp.enabled && (chem.orp.doserType === 1 || chem.orp.doserType === 3)) ? msg.extractPayloadByte(32) & 0x40 : 0;
234
- alarms.probeFault = msg.extractPayloadByte(32) & 0x80;
235
- // 33 : Warnings -- pH Lockout, Daily Limit Reached, Invalid Setup, Chlorinator Comm error
236
- const warnings = schem.warnings;
237
- warnings.pHLockout = msg.extractPayloadByte(33) & 0x01;
238
- warnings.pHDailyLimitReached = msg.extractPayloadByte(33) & 0x02;
239
- warnings.orpDailyLimitReached = msg.extractPayloadByte(33) & 0x04;
240
- warnings.invalidSetup = msg.extractPayloadByte(33) & 0x08;
241
- warnings.chlorinatorCommError = msg.extractPayloadByte(33) & 0x10;
242
- // So we need to do some calculation here.
243
-
244
- // 34 : Dosing Status = 149 = (pH Monitoring, ORP Mixing)
245
- schem.ph.dosingStatus = (msg.extractPayloadByte(34) & 0x30) >> 4; // mask 00xx0000 and shift bit 5 & 6
246
- schem.orp.dosingStatus = (msg.extractPayloadByte(34) & 0xC0) >> 6; // mask xx000000 and shift bit 7 & 8
247
- // 35 : Delays = 0
248
- schem.status = (msg.extractPayloadByte(35) & 0x80) >> 7; // to be verified as comms lost
249
- schem.ph.manualDosing = (msg.extractPayloadByte(35) & 0x08) === 1 ? true : false;
250
- chem.orp.useChlorinator = (msg.extractPayloadByte(35) & 0x10) === 1 ? true : false;
251
- chem.HMIAdvancedDisplay = (msg.extractPayloadByte(35) & 0x20) === 1 ? true : false;
252
- chem.ph.phSupply = (msg.extractPayloadByte(35) & 0x40) === 1 ? 'acid' : 'base'; // acid pH dosing = 1; base pH dosing = 0;
253
- // 36-37 : Firmware = 80,1 = 1.080
254
- chem.firmware = `${msg.extractPayloadByte(37)}.${msg.extractPayloadByte(36).toString().padStart(3, '0')}`
255
- // 38 : Water Chemistry Warning
256
- // The LSI handling is also stupid with IntelliChem so we are going to have our way with it.
257
- // schem.warnings.waterChemistry = msg.extractPayloadByte(38);
258
- schem.calculateSaturationIndex();
259
- if (schem.saturationIndex > chem.lsiRange.high) schem.warnings.waterChemistry = 2;
260
- else if (schem.saturationIndex < chem.lsiRange.low) schem.warnings.waterChemistry = 1;
261
- else schem.warnings.waterChemistry = 0;
262
- if (typeof chem.body === 'undefined') chem.body = schem.body = 0;
263
- if (state.equipment.controllerType === 'nixie') {
264
- if (chem.ph.probe.feedBodyTemp) {
265
- let temps: any = {};
266
- let body = state.temps.bodies.getBodyIsOn();
267
- if (typeof body !== 'undefined') {
268
- if (body.id === 1 && (schem.body === 0 || schem.body === 32)) {
269
- temps.waterSensor1 = schem.ph.probe.temperature;
270
- }
271
- else if (body.id === 2 && (schem.body === 2 || schem.body === 32)) {
272
- temps.waterSensor1 = schem.ph.probe.temperature;
273
- }
274
- else if (body.id === 2 && chem.body === 1) {
275
- temps.waterSensor2 = schem.ph.probe.temperature;
276
- }
277
- }
278
- sys.board.system.setTempsAsync(temps).catch(err => logger.error(`Error setting temp compensation for IntelliChem State: ${err.message}`))
279
- }
280
- }
281
- schem.ph.pump.isDosing = schem.ph.dosingStatus === 0 && chem.ph.enabled;
282
- schem.orp.pump.isDosing = schem.orp.dosingStatus === 0 && chem.orp.enabled;
283
- schem.lastComm = new Date().getTime();
284
- // If we are changing states lets set that up.
285
- if (schem.ph.dosingStatus === 0 && phPrev.status !== 0) {
286
- if (schem.ph.dosingStatus === 0) {
287
- // We are starting a dose so we need to set the current dose.
288
- schem.ph.startDose(Timestamp.now.addSeconds(-schem.ph.doseTime).toDate(), schem.ph.manualDosing ? 'manual' : 'auto', 0, schem.ph.volumeDosed, 0, schem.ph.timeDosed * 1000);
289
- }
290
- }
291
- else if (schem.ph.dosingStatus !== 0 && phPrev.status === 0) {
292
- if (typeof schem.ph.currentDose !== 'undefined') {
293
- // We just ended a dose so write it out to the chem logs.
294
- schem.ph.endDose(Timestamp.now.addSeconds(-(schem.ph.doseTime - phPrev.time)).toDate(), 'completed',
295
- schem.ph.volumeDosed - phPrev.vol, (schem.ph.timeDosed - phPrev.time) * 1000);
296
- }
297
- }
298
- else if (schem.ph.dosingStatus === 0) {
299
- // We are still dosing so add the time and volume to the dose.
300
- schem.ph.appendDose(schem.ph.doseVolume - phPrev.vol, (schem.ph.timeDosed - phPrev.time) * 1000);
301
- }
302
- else {
303
- //console.log(`DOSING STATUS === ${schem.ph.dosingStatus}`);
304
- // Make sure we don't have a current dose going.
305
- schem.ph.currentDose = undefined;
306
- }
307
- // If we are changing states lets set that up for orp.
308
- if (schem.orp.dosingStatus === 0 && orpPrev.status !== 0) {
309
- if (schem.orp.dosingStatus === 0) {
310
- // We are starting a dose so we need to set the current dose.
311
- schem.orp.startDose(Timestamp.now.addSeconds(-schem.orp.doseTime).toDate(), schem.orp.manualDosing ? 'manual' : 'auto', 0, schem.orp.volumeDosed, 0, schem.orp.timeDosed * 1000);
312
- }
313
- }
314
- else if (schem.orp.dosingStatus !== 0 && orpPrev.status === 0) {
315
- if (typeof schem.orp.currentDose !== 'undefined') {
316
- // We just ended a dose so write it out to the chem logs.
317
- schem.orp.endDose(Timestamp.now.addSeconds(-(schem.orp.doseTime - orpPrev.time)).toDate(), 'completed',
318
- schem.orp.volumeDosed - orpPrev.vol, (schem.orp.timeDosed - orpPrev.time) * 1000);
319
- }
320
- }
321
- else if (schem.orp.dosingStatus === 0) {
322
- // We are still dosing so add the time and volume to the dose.
323
- schem.orp.appendDose(schem.orp.doseVolume - orpPrev.vol, (schem.orp.timeDosed - orpPrev.time) * 1000);
324
- }
325
- else {
326
- // Make sure we don't have a current dose going.
327
- schem.orp.currentDose = undefined;
328
- }
329
- // manually emit extended values
330
- webApp.emitToClients('chemController', schem.getExtended()); // emit extended data
331
- schem.hasChanged = false; // try to avoid duplicate emits
332
- msg.isProcessed = true;
333
-
334
- /*
335
- //Status 0x12 (18) - Intellichem Status (length 41)
336
- example:
337
- 02 E3 02 AF 02 EE 02 BC 00 00 00 02 00 00 00 2A 00 04 00 5C 06 05 18 01 90 00 00 00 96 14 00 51 00 00 65 20 3C 01 00 00 00
338
-
339
- 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
340
- [165,16,15,16,18 41], [2 227 2 175 2 238 2 188 0 0 0 2 0 0 0 42 0 4 0 92 6 5 24 1 144 0 0 0 150 20 0 81 0 0 101 32 60 1 0 0 0]
341
- ph--- orp-- ph--- orp-- tanks CH--- CYA TA--- Wtr MODE
342
- 0-1 pH(1-2) / ORP(8-9) reading
343
- 02 E3 - pH 2*256 + e3(227) = 739
344
- 02 AF - ORP 2*256 + af(175) = 687
345
-
346
- 4-5 pH settings
347
- D0 = 7.2 (hi/lo bits - 720 = 7.2pH)
348
- DA = 7.3
349
- E4 = 7.4
350
- EE = 7.5
351
- F8 = 7.6
352
-
353
- 6-7 ORP settings
354
- 02 BC = 700 (hi/lo bits)
355
-
356
- 20-21 Tank levels; 21 is acid? 22 is chlorine?
357
- 06 and 05
358
-
359
- 23-24 Calcimum Hardness
360
- 90 is CH (90 = 400; 8b = 395) hi/lo bits
361
-
362
- 26
363
- 00 is CYA (00 = 0; 9 = 9; c9 = 201) (does not appear to have hi/lo - 201 is max
364
-
365
- 27-28 - Total Alkalinity
366
- 96 is TA (96 = 150)
367
-
368
- 30 - Water Flow Alarm (00 is ok; 01 is no flow)
369
- 00 flow is OK
370
- 01 flow is Alarm on (Water STOPPED)
371
-
372
- // todo: these are probably 2-byte status message but need to confirm
373
- 36 Mode
374
- 0x25 dosing (auto)?
375
- 0x45 dosing acid (manually?)
376
- 0x55 mixing???
377
- 0x65 monitoring
378
- 0x02 (12 when mixing) and 04 (27 when mixing) related???
379
-
380
- 37
381
- 20 Nothing
382
- 22 Dosing Chlorine(?)
383
- */
384
-
385
-
386
- //// Missing information on the related bytes.
387
- //// Bytes 8-14 (Probably Total Dissolved Solids in here if no IntelliChlor)
388
- //// controller.waterVolume = msg.extractPayloadByte(15) * 1000;
389
- //// Bytes 16-19
390
- //scontroller.ph.dosingVolumeRemaining = msg.extractPayloadByte(17); // Previous pH Dose volume
391
- //scontroller.orp.dosingVolumeRemaining = msg.extractPayloadByte(19); // Previous ORP Dose volume
392
- //scontroller.ph.tank.level = Math.max(msg.extractPayloadByte(20) > 0 ? msg.extractPayloadByte(20) - 1 : msg.extractPayloadByte(20), 0); // values reported as 1-7; possibly 0 if no tank present
393
- //scontroller.orp.tank.level = Math.max(msg.extractPayloadByte(21) > 0 ? msg.extractPayloadByte(21) - 1 : msg.extractPayloadByte(21), 0); // values reported as 1-7; possibly 0 if no tank present
394
- //let SIRaw = msg.extractPayloadByte(22);
395
- //if ((SIRaw & 0x80) === 0x80) {
396
- // // negative SI
397
- // scontroller.saturationIndex = (256 - SIRaw) / -100;
398
- //}
399
- //else {
400
- // scontroller.saturationIndex = msg.extractPayloadByte(22) / 100;
401
- //}
402
- //controller.calciumHardness = msg.extractPayloadIntBE(23);
403
-
404
- //// scontroller.status2 = msg.extractPayloadByte(25); // remove/unsure?
405
- //controller.cyanuricAcid = msg.extractPayloadByte(26);
406
- //controller.alkalinity = msg.extractPayloadIntBE(27);
407
-
408
- //// scontroller.waterFlow = msg.extractPayloadByte(30); // This is probably the temp units.
409
- //scontroller.ph.probe.tempUnits = 0;//msg.extractPayloadByte(30); See Above. This is probably the units.
410
- //scontroller.ph.probe.temperature = msg.extractPayloadByte(31);
411
-
412
- //msg.extractPayloadByte(33);
413
-
414
- //// [0, { name: 'dosing', desc: 'Dosing' }],
415
- //// [1, { name: 'monitoring', desc: 'Monitoring' }],
416
- //// [2, { name: 'mixing', desc: 'Mixing' }]
417
-
418
- //scontroller.ph.dosingStatus = (msg.extractPayloadByte(34) & 0x30) >> 4; // mask 00xx0000 and shift bit 5 & 6
419
- //scontroller.orp.dosingStatus = (msg.extractPayloadByte(34) & 0xC0) >> 6; // mask xx000000 and shift bit 7 & 8
420
- //scontroller.ph.flowDelay = scontroller.orp.flowDelay = (msg.extractPayloadByte(35) & 0x02) === 1 ? true : false;
421
- //scontroller.status = msg.extractPayloadByte(35) & 0x80 >> 7; // to be verified as comms lost
422
- //scontroller.ph.manualDosing = (msg.extractPayloadByte(35) & 0x08) === 1 ? true : false;
423
- //controller.orp.useChlorinator = (msg.extractPayloadByte(35) & 0x10) === 1 ? true : false;
424
- //controller.HMIAdvancedDisplay = (msg.extractPayloadByte(35) & 0x20) === 1 ? true : false;
425
- //controller.ph.phSupply = (msg.extractPayloadByte(35) & 0x40) === 1 ? true : false; // acid pH dosing = 1; base pH dosing = 0;
426
- //scontroller.firmware = `${msg.extractPayloadByte(37)}.${msg.extractPayloadByte(36).toString().padStart(3, '0')}`
427
-
428
- //const warnings = scontroller.warnings;
429
- //warnings.waterChemistry = msg.extractPayloadByte(38);
430
- //warnings.pHLockout = msg.extractPayloadByte(33) & 0x01;
431
- //warnings.pHDailyLimitReached = msg.extractPayloadByte(33) & 0x02;
432
- //warnings.orpDailyLimitReached = msg.extractPayloadByte(33) & 0x04;
433
- //warnings.invalidSetup = msg.extractPayloadByte(33) & 0x08;
434
- //warnings.chlorinatorCommError = msg.extractPayloadByte(33) & 0x10;
435
-
436
- //// RKS: This should really match against the body for the chlorinator when *Chem thinks it has been provided TDS.
437
- //// RG: Byte 35, bit 4 indicates IntelliChlor is used. Until we know more, this logic suffices.
438
- //if (sys.chlorinators.length > 0) {
439
- // let chlor = state.chlorinators.find(elem => elem.id === 1);
440
- // scontroller.orp.probe.saltLevel = (typeof chlor !== 'undefined') ? chlor.saltLevel : msg.extractPayloadByte(29) * 50;
441
- //}
442
- //else scontroller.orp.probe.saltLevel = 0;
443
-
444
- //// manually emit extended values
445
- //webApp.emitToClients('chemController', scontroller.getExtended()); // emit extended data
446
- //scontroller.hasChanged = false; // try to avoid duplicate emits
447
- //msg.isProcessed = true;
448
- }
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 { Inbound } from "../Messages";
19
+ import { state } from "../../../State";
20
+ import { sys, ControllerType } from "../../../Equipment";
21
+ import { logger } from "../../../../logger/Logger";
22
+ import { webApp } from "../../../../web/Server";
23
+ import { Timestamp, utils } from "../../../Constants"
24
+ export class IntelliChemStateMessage {
25
+ public static process(msg: Inbound) {
26
+ if (sys.controllerType === ControllerType.Unknown) return;
27
+ let address = (msg.dest >= 144 && msg.dest <= 158) ? msg.dest : msg.source;
28
+ if (address < 144 || address > 158) return;
29
+ let controller = sys.chemControllers.getItemByAddress(address);
30
+ // RKS: 07-13-22 Lets just assume that SunTouch doesn't report its IntelliChem at this point. The action 40 return
31
+ // does not contain the IntelliChem bit when it is returned for this controller.
32
+ if (!controller.isActive && sys.controllerType !== ControllerType.SunTouch) {
33
+ msg.isProcessed = true;
34
+ return;
35
+ }
36
+ switch (msg.action) {
37
+ // ---------- IntelliChem Control panel is spitting out its status ----------- //
38
+ case 18: // IntelliChem is sending us it's status.
39
+ IntelliChemStateMessage.processState(msg);
40
+ break;
41
+ case 210: // OCP is asking IntelliChem controller for it's current status info.
42
+ // [165,0,144,16,210,1],[210],[2,234]
43
+ let schem = state.chemControllers.getItemById(controller.id);
44
+ if (schem.lastComm + (30 * 1000) < new Date().getTime()) {
45
+ // We have not talked to the chem controller in 30 seconds so we have lost communication.
46
+ schem.status = schem.alarms.comms = 1;
47
+ }
48
+ msg.isProcessed = true;
49
+ break;
50
+ // ---------- End IntelliChem set get ----------- //
51
+
52
+ // ---------- OCP set get ----------- //
53
+ case 19: // Request to OCP to return the status that ic currently has.
54
+ // [165,14,16,34,19,1],[0],[0,249]
55
+ break;
56
+
57
+ /* RKS: This is processed in the IntellichemMessage.processTouch() and is the results of asking for the IntelliChem configuration.
58
+ case 147: // OCP is broadcasting it's known ic values... Need to change our settings if virtual.
59
+ // 147 is a proto:broadcast message;
60
+ // it has exactly the same format as 18 but there is payload[0] which is inserted at the beginning. Likely the chem controller id.
61
+ if (msg.dest < 144 || msg.dest > 158) return;
62
+ IntelliChemStateMessage.processControllerChange(msg);
63
+ break;
64
+ // ---------- End OCP set get ----------- //
65
+ */
66
+ // ---------- ICP or SL set get ----------- //
67
+ case 211: // SL or other controller is telling OCP to set IntelliChem value
68
+ // It will take these values and pass them in 146 to IntelliChem
69
+ // values that are NOT SET should be ignored
70
+ msg.isProcessed = true;
71
+ break;
72
+ case 146: // OCP is telling IntelliChem that it needs to change its settings to...
73
+ //let scontroller = state.chemControllers.getItemById(controller.id, true);
74
+ //if (scontroller.lastComm + (30 * 1000) < new Date().getTime()) {
75
+ // // We have not talked to the chem controller in 30 seconds so we have lost communication.
76
+ // scontroller.status = scontroller.alarms.comms = 1;
77
+ //}
78
+ controller.ph.tank.capacity = controller.orp.tank.capacity = 6;
79
+ controller.ph.tank.units = controller.orp.tank.units = '';
80
+ msg.isProcessed = true;
81
+ break;
82
+ // ---------- OCP set get ----------- //
83
+ }
84
+ state.emitEquipmentChanges();
85
+ }
86
+ private static processControllerChange(msg: Inbound) {
87
+ // We don't do anything with this inbound action 147 message.
88
+ logger.info(`Incoming message from IntelliChem ${msg.toShortPacket()}`);
89
+ msg.isProcessed = true;
90
+ }
91
+ private static processState(msg: Inbound) {
92
+ if (msg.source < 144 || msg.source > 158) return;
93
+
94
+ // Setup with CO2 dosing and IntelliChlor.
95
+ //[2, 245, 2, 162, 2, 248, 2, 88, 0, 0, [0-9]
96
+ // 0, 26, 0, 0, 0, 0, 0, 0, 0, 0, [10-19]
97
+ // 7, 0, 249, 1, 94, 0, 81, 0, 80, 57, [20-29]
98
+ // 0, 82, 0, 0, 162, 32, 80, 1, 0, 0, 0] [30-40]
99
+ // Setup with 2 Tanks
100
+ //[2, 238, 2, 208, 2, 248, 2, 188, 0, 0,
101
+ // 0, 2, 0, 0, 0, 29, 0, 4, 0, 63,
102
+ // 2, 2, 157, 0, 25, 0, 0, 0, 90, 20,
103
+ // 0, 83, 0, 0, 149, 0, 60, 1, 1, 0, 0]
104
+
105
+ // This is an action 18 that comes from IntelliChem. There is also a 147 that comes from an OCP but this is the raw data.
106
+ //[165, 0, 16, 144, 18, 41][2,228,3,2,2,228,2,188,0,0,0,16,0,0,0,0,0,35,0,0,6,6,3,0,250,0,44,0,160,20,0,81,8,0,149,0,80,1,0,0,0]
107
+ // Bytes - Descrption
108
+ // 0-1 : pH byte(0) x 256 + byte(1) / 100
109
+ // 2-3 : ORP byte(2) x 256 + byte(3)
110
+ // 4-5 : pH Setpoint : byte(4) x 256 + byte(5) / 100
111
+ // 6-7 : ORP Setpoint : byte(6) x 256 + byte(7)
112
+ // 8 : Unknown = 0
113
+ // 9 : Unknown = 0
114
+ // 10-11 : pH Dose time seconds. The number of seconds since the dose started. byte(10) x 256 + byte(11)
115
+ // 12: Unknown
116
+ // 13 : Unknown
117
+ // 14-15 : ORP Dose time seconds. The number of seconds since the dose started. byte(14) x 256 + byte(15)
118
+ // 16-17 : pH Dose volume (unknown units) - These appear to be mL.
119
+ // 18-19 : ORP Dose volume (unknown units) - These appear to be mL
120
+ // 20 : pH tank level 1-7
121
+ // 21 : ORP tank level 1-7
122
+ // 22 : LSI. (byte(22) & 0x80) === 0x80 ? (256 - byte(22) / -100 : byte(22)/100
123
+ // 23-24 : Calcium Hardness = byte(23) x 256 + byte(24) = 250
124
+ // 25 : Unknown = 0 (probably reserved CYA byte so the chem is always dealing with integers)
125
+ // 26 : CYA Max value = 210.
126
+ // 27-28 : Alkalinity = byte(27) x 256 + byte(28)
127
+ // 29 : Salt level = byte(29) x 50
128
+ // 30 : Unknown
129
+ // 31 : Temperature
130
+ // 32 : Alarms
131
+ // 33 : Warnings pH Lockout, Daily Limit Reached, Invalid Setup, Chlorinator Comm error
132
+ // 34 : Dosing Status/Doser Type (pH Monitoring, ORP Mixing)
133
+ // 35 : Delays
134
+ // 36-37 : Firmware = 80,1 = 1.080
135
+ // 38 : Water Chemistry Warning (Corrosion...)
136
+ // 39 : Unknown
137
+ // 40 : Unknown
138
+ let address = msg.source;
139
+
140
+ // The address is king here. The id is not.
141
+ let chem = sys.chemControllers.getItemByAddress(address, true);
142
+ let schem = state.chemControllers.getItemById(chem.id, true);
143
+
144
+ // Get the doser types and set up our capabilities
145
+ chem.ph.doserType = (msg.extractPayloadByte(34) & 0x03);
146
+ chem.orp.doserType = (msg.extractPayloadByte(34) & 0x0C) >> 2;
147
+ schem.ph.enabled = chem.ph.enabled = chem.ph.doserType !== 0;
148
+ schem.ph.enabled = chem.orp.enabled = chem.orp.doserType !== 0;
149
+ if (chem.ph.doserType === 2) schem.ph.chemType = 'CO2';
150
+ else if (chem.ph.doserType === 0) schem.ph.chemType = 'none';
151
+ else if (chem.ph.doserType === 1) schem.ph.chemType = 'acid';
152
+ else if (chem.ph.doserType === 3) schem.ph.chemType = 'acid';
153
+
154
+ if (chem.orp.doserType === 0) schem.orp.chemType = 'none';
155
+ else schem.orp.chemType = 'orp';
156
+
157
+ schem.isActive = chem.isActive = true;
158
+ schem.status = 0;
159
+ schem.type = chem.type = sys.board.valueMaps.chemControllerTypes.getValue('intellichem');
160
+ chem.name = chem.name || `IntelliChem ${chem.address - 143}`; // default to true id if no name is set
161
+ schem.lastComm = new Date().getTime();
162
+ schem.status = schem.alarms.comms = 0;
163
+ chem.ph.tank.capacity = chem.orp.tank.capacity = 6;
164
+ chem.ph.tank.units = chem.orp.tank.units = '';
165
+ chem.ph.tank.alarmEmptyEnabled = false;
166
+ chem.ph.tank.alarmEmptyLevel = 1;
167
+ chem.orp.tank.alarmEmptyEnabled = false;
168
+ chem.orp.tank.alarmEmptyLevel = 1;
169
+ schem.address = chem.address;
170
+ schem.ph.level = schem.ph.probe.level = msg.extractPayloadIntBE(0) / 100;
171
+ schem.orp.level = schem.orp.probe.level = msg.extractPayloadIntBE(2);
172
+ chem.ph.setpoint = msg.extractPayloadIntBE(4) / 100;
173
+ chem.orp.setpoint = msg.extractPayloadIntBE(6);
174
+ // Missing information on the related bytes.
175
+ // Bytes 8-14 (Probably Total Dissolved Solids in here if no IntelliChlor)
176
+ let phPrev = { status: schem.ph.dosingStatus, time: schem.ph.timeDosed || 0, vol: schem.ph.volumeDosed };
177
+ let orpPrev = { status: schem.orp.dosingStatus, time: schem.orp.timeDosed || 0, vol: schem.orp.volumeDosed };
178
+ // IntelliChem never tells us what the dose time or volume is so we will let that dog lie.
179
+ // 10-11 : pH Dose time
180
+ schem.ph.timeDosed = (msg.extractPayloadByte(10) * 256) + msg.extractPayloadByte(11);
181
+ // 14-15 : ORP Dose time seconds. The number of seconds since the dose started.
182
+ schem.orp.timeDosed = (msg.extractPayloadByte(14) * 256) + msg.extractPayloadByte(15);
183
+ // 16-17 : pH Dose volume (unknown units) = 35
184
+ schem.ph.volumeDosed = (msg.extractPayloadByte(16) * 256) + msg.extractPayloadByte(17);
185
+ // 18-19 : ORP Dose volume (unknown units) = 0
186
+ schem.orp.volumeDosed = (msg.extractPayloadByte(18) * 256) + msg.extractPayloadByte(19);
187
+ // 20 : pH tank level 1-7 = 6
188
+ schem.ph.tank.level = Math.max(msg.extractPayloadByte(20) > 0 ? msg.extractPayloadByte(20) - 1 : msg.extractPayloadByte(20), 0); // values reported as 1-7; possibly 0 if no tank present
189
+ // 21 : ORP tank level 1-7 = 6 if the tank levels report 0 then the chemical side is not enabled.
190
+ schem.orp.tank.level = Math.max(msg.extractPayloadByte(21) > 0 ? msg.extractPayloadByte(21) - 1 : msg.extractPayloadByte(21), 0); // values reported as 1-7; possibly 0 if no tank present
191
+
192
+ // 22 : LSI = 3 & 0x80 === 0x80 ? (256 - 3) / -100 : 3/100 = .03
193
+ let lsi = msg.extractPayloadByte(22);
194
+ schem.lsi = (lsi & 0x80) === 0x80 ? (256 - lsi) / -100 : lsi / 100;
195
+ // 23-24 : Calcium Hardness = 0x256+250 = 250
196
+ chem.calciumHardness = (msg.extractPayloadByte(23) * 256) + msg.extractPayloadByte(24);
197
+ // 26 : CYA = 44
198
+ chem.cyanuricAcid = msg.extractPayloadByte(26);
199
+ // 27-28 : Alkalinity
200
+ chem.alkalinity = (msg.extractPayloadByte(27) * 256) + msg.extractPayloadByte(28);
201
+ // 29 : Salt level = 20
202
+ if (sys.chlorinators.length > 0) {
203
+ let chlor = state.chlorinators.find(elem => elem.id === 1);
204
+ schem.orp.probe.saltLevel = (typeof chlor !== 'undefined') ? chlor.saltLevel : msg.extractPayloadByte(29) * 50;
205
+ }
206
+ else schem.orp.probe.saltLevel = 0;
207
+ // 31 : Temperature = 81
208
+ schem.ph.probe.temperature = msg.extractPayloadByte(31);
209
+ schem.ph.probe.tempUnits = state.temps.units;
210
+ // 32 : Alarms = 8 = (no alarm)
211
+ const alarms = schem.alarms;
212
+ alarms.flow = msg.extractPayloadByte(32) & 0x01;
213
+ if (alarms.flow === 0) schem.flowDetected = true;
214
+
215
+ // The pH and ORP alarms are in a word stupid for IntelliChem. So we are
216
+ // going to override these.
217
+ //alarms.pH = msg.extractPayloadByte(32) & 0x06;
218
+ //alarms.orp = msg.extractPayloadByte(32) & 0x18;
219
+ if (chem.ph.tolerance.enabled && schem.flowDetected) {
220
+ if (schem.ph.level > chem.ph.tolerance.high) alarms.pH = 2;
221
+ else if (schem.ph.level < chem.ph.tolerance.low) alarms.pH = 4;
222
+ else alarms.pH = 0;
223
+ }
224
+ else alarms.pH = 0;
225
+ if (chem.orp.tolerance.enabled && schem.flowDetected) {
226
+ if (schem.orp.level > chem.orp.tolerance.high) alarms.orp = 8;
227
+ else if (schem.orp.level < chem.orp.tolerance.low) alarms.orp = 16;
228
+ else alarms.orp = 0;
229
+ }
230
+ else alarms.orp = 0;
231
+ // IntelliChem will still send a tank empty alarm even if there is no tank.
232
+ alarms.pHTank = (chem.ph.enabled && (chem.ph.doserType === 1 || chem.ph.doserType === 3)) ? msg.extractPayloadByte(32) & 0x20 : 0;
233
+ alarms.orpTank = (chem.orp.enabled && (chem.orp.doserType === 1 || chem.orp.doserType === 3)) ? msg.extractPayloadByte(32) & 0x40 : 0;
234
+ alarms.probeFault = msg.extractPayloadByte(32) & 0x80;
235
+ // 33 : Warnings -- pH Lockout, Daily Limit Reached, Invalid Setup, Chlorinator Comm error
236
+ const warnings = schem.warnings;
237
+ warnings.pHLockout = msg.extractPayloadByte(33) & 0x01;
238
+ warnings.pHDailyLimitReached = msg.extractPayloadByte(33) & 0x02;
239
+ warnings.orpDailyLimitReached = msg.extractPayloadByte(33) & 0x04;
240
+ warnings.invalidSetup = msg.extractPayloadByte(33) & 0x08;
241
+ warnings.chlorinatorCommError = msg.extractPayloadByte(33) & 0x10;
242
+ // So we need to do some calculation here.
243
+
244
+ // 34 : Dosing Status = 149 = (pH Monitoring, ORP Mixing)
245
+ schem.ph.dosingStatus = (msg.extractPayloadByte(34) & 0x30) >> 4; // mask 00xx0000 and shift bit 5 & 6
246
+ schem.orp.dosingStatus = (msg.extractPayloadByte(34) & 0xC0) >> 6; // mask xx000000 and shift bit 7 & 8
247
+ // 35 : Delays = 0
248
+ schem.status = (msg.extractPayloadByte(35) & 0x80) >> 7; // to be verified as comms lost
249
+ schem.ph.manualDosing = (msg.extractPayloadByte(35) & 0x08) === 1 ? true : false;
250
+ chem.orp.useChlorinator = (msg.extractPayloadByte(35) & 0x10) === 1 ? true : false;
251
+ chem.HMIAdvancedDisplay = (msg.extractPayloadByte(35) & 0x20) === 1 ? true : false;
252
+ chem.ph.phSupply = (msg.extractPayloadByte(35) & 0x40) === 1 ? 'acid' : 'base'; // acid pH dosing = 1; base pH dosing = 0;
253
+ // 36-37 : Firmware = 80,1 = 1.080
254
+ chem.firmware = `${msg.extractPayloadByte(37)}.${msg.extractPayloadByte(36).toString().padStart(3, '0')}`
255
+ // 38 : Water Chemistry Warning
256
+ // The LSI handling is also stupid with IntelliChem so we are going to have our way with it.
257
+ // schem.warnings.waterChemistry = msg.extractPayloadByte(38);
258
+ schem.calculateSaturationIndex();
259
+ if (schem.saturationIndex > chem.lsiRange.high) schem.warnings.waterChemistry = 2;
260
+ else if (schem.saturationIndex < chem.lsiRange.low) schem.warnings.waterChemistry = 1;
261
+ else schem.warnings.waterChemistry = 0;
262
+ if (typeof chem.body === 'undefined') chem.body = schem.body = 0;
263
+ if (state.equipment.controllerType === 'nixie') {
264
+ if (chem.ph.probe.feedBodyTemp) {
265
+ let temps: any = {};
266
+ let body = state.temps.bodies.getBodyIsOn();
267
+ if (typeof body !== 'undefined') {
268
+ if (body.id === 1 && (schem.body === 0 || schem.body === 32)) {
269
+ temps.waterSensor1 = schem.ph.probe.temperature;
270
+ }
271
+ else if (body.id === 2 && (schem.body === 2 || schem.body === 32)) {
272
+ temps.waterSensor1 = schem.ph.probe.temperature;
273
+ }
274
+ else if (body.id === 2 && chem.body === 1) {
275
+ temps.waterSensor2 = schem.ph.probe.temperature;
276
+ }
277
+ }
278
+ sys.board.system.setTempsAsync(temps).catch(err => logger.error(`Error setting temp compensation for IntelliChem State: ${err.message}`))
279
+ }
280
+ }
281
+ schem.ph.pump.isDosing = schem.ph.dosingStatus === 0 && chem.ph.enabled;
282
+ schem.orp.pump.isDosing = schem.orp.dosingStatus === 0 && chem.orp.enabled;
283
+ schem.lastComm = new Date().getTime();
284
+ // If we are changing states lets set that up.
285
+ if (schem.ph.dosingStatus === 0 && phPrev.status !== 0) {
286
+ if (schem.ph.dosingStatus === 0) {
287
+ // We are starting a dose so we need to set the current dose.
288
+ schem.ph.startDose(Timestamp.now.addSeconds(-schem.ph.doseTime).toDate(), schem.ph.manualDosing ? 'manual' : 'auto', 0, schem.ph.volumeDosed, 0, schem.ph.timeDosed * 1000);
289
+ }
290
+ }
291
+ else if (schem.ph.dosingStatus !== 0 && phPrev.status === 0) {
292
+ if (typeof schem.ph.currentDose !== 'undefined') {
293
+ // We just ended a dose so write it out to the chem logs.
294
+ schem.ph.endDose(Timestamp.now.addSeconds(-(schem.ph.doseTime - phPrev.time)).toDate(), 'completed',
295
+ schem.ph.volumeDosed - phPrev.vol, (schem.ph.timeDosed - phPrev.time) * 1000);
296
+ }
297
+ }
298
+ else if (schem.ph.dosingStatus === 0) {
299
+ // We are still dosing so add the time and volume to the dose.
300
+ schem.ph.appendDose(schem.ph.doseVolume - phPrev.vol, (schem.ph.timeDosed - phPrev.time) * 1000);
301
+ }
302
+ else {
303
+ //console.log(`DOSING STATUS === ${schem.ph.dosingStatus}`);
304
+ // Make sure we don't have a current dose going.
305
+ schem.ph.currentDose = undefined;
306
+ }
307
+ // If we are changing states lets set that up for orp.
308
+ if (schem.orp.dosingStatus === 0 && orpPrev.status !== 0) {
309
+ if (schem.orp.dosingStatus === 0) {
310
+ // We are starting a dose so we need to set the current dose.
311
+ schem.orp.startDose(Timestamp.now.addSeconds(-schem.orp.doseTime).toDate(), schem.orp.manualDosing ? 'manual' : 'auto', 0, schem.orp.volumeDosed, 0, schem.orp.timeDosed * 1000);
312
+ }
313
+ }
314
+ else if (schem.orp.dosingStatus !== 0 && orpPrev.status === 0) {
315
+ if (typeof schem.orp.currentDose !== 'undefined') {
316
+ // We just ended a dose so write it out to the chem logs.
317
+ schem.orp.endDose(Timestamp.now.addSeconds(-(schem.orp.doseTime - orpPrev.time)).toDate(), 'completed',
318
+ schem.orp.volumeDosed - orpPrev.vol, (schem.orp.timeDosed - orpPrev.time) * 1000);
319
+ }
320
+ }
321
+ else if (schem.orp.dosingStatus === 0) {
322
+ // We are still dosing so add the time and volume to the dose.
323
+ schem.orp.appendDose(schem.orp.doseVolume - orpPrev.vol, (schem.orp.timeDosed - orpPrev.time) * 1000);
324
+ }
325
+ else {
326
+ // Make sure we don't have a current dose going.
327
+ schem.orp.currentDose = undefined;
328
+ }
329
+ // manually emit extended values
330
+ webApp.emitToClients('chemController', schem.getExtended()); // emit extended data
331
+ schem.hasChanged = false; // try to avoid duplicate emits
332
+ msg.isProcessed = true;
333
+
334
+ /*
335
+ //Status 0x12 (18) - Intellichem Status (length 41)
336
+ example:
337
+ 02 E3 02 AF 02 EE 02 BC 00 00 00 02 00 00 00 2A 00 04 00 5C 06 05 18 01 90 00 00 00 96 14 00 51 00 00 65 20 3C 01 00 00 00
338
+
339
+ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
340
+ [165,16,15,16,18 41], [2 227 2 175 2 238 2 188 0 0 0 2 0 0 0 42 0 4 0 92 6 5 24 1 144 0 0 0 150 20 0 81 0 0 101 32 60 1 0 0 0]
341
+ ph--- orp-- ph--- orp-- tanks CH--- CYA TA--- Wtr MODE
342
+ 0-1 pH(1-2) / ORP(8-9) reading
343
+ 02 E3 - pH 2*256 + e3(227) = 739
344
+ 02 AF - ORP 2*256 + af(175) = 687
345
+
346
+ 4-5 pH settings
347
+ D0 = 7.2 (hi/lo bits - 720 = 7.2pH)
348
+ DA = 7.3
349
+ E4 = 7.4
350
+ EE = 7.5
351
+ F8 = 7.6
352
+
353
+ 6-7 ORP settings
354
+ 02 BC = 700 (hi/lo bits)
355
+
356
+ 20-21 Tank levels; 21 is acid? 22 is chlorine?
357
+ 06 and 05
358
+
359
+ 23-24 Calcimum Hardness
360
+ 90 is CH (90 = 400; 8b = 395) hi/lo bits
361
+
362
+ 26
363
+ 00 is CYA (00 = 0; 9 = 9; c9 = 201) (does not appear to have hi/lo - 201 is max
364
+
365
+ 27-28 - Total Alkalinity
366
+ 96 is TA (96 = 150)
367
+
368
+ 30 - Water Flow Alarm (00 is ok; 01 is no flow)
369
+ 00 flow is OK
370
+ 01 flow is Alarm on (Water STOPPED)
371
+
372
+ // todo: these are probably 2-byte status message but need to confirm
373
+ 36 Mode
374
+ 0x25 dosing (auto)?
375
+ 0x45 dosing acid (manually?)
376
+ 0x55 mixing???
377
+ 0x65 monitoring
378
+ 0x02 (12 when mixing) and 04 (27 when mixing) related???
379
+
380
+ 37
381
+ 20 Nothing
382
+ 22 Dosing Chlorine(?)
383
+ */
384
+
385
+
386
+ //// Missing information on the related bytes.
387
+ //// Bytes 8-14 (Probably Total Dissolved Solids in here if no IntelliChlor)
388
+ //// controller.waterVolume = msg.extractPayloadByte(15) * 1000;
389
+ //// Bytes 16-19
390
+ //scontroller.ph.dosingVolumeRemaining = msg.extractPayloadByte(17); // Previous pH Dose volume
391
+ //scontroller.orp.dosingVolumeRemaining = msg.extractPayloadByte(19); // Previous ORP Dose volume
392
+ //scontroller.ph.tank.level = Math.max(msg.extractPayloadByte(20) > 0 ? msg.extractPayloadByte(20) - 1 : msg.extractPayloadByte(20), 0); // values reported as 1-7; possibly 0 if no tank present
393
+ //scontroller.orp.tank.level = Math.max(msg.extractPayloadByte(21) > 0 ? msg.extractPayloadByte(21) - 1 : msg.extractPayloadByte(21), 0); // values reported as 1-7; possibly 0 if no tank present
394
+ //let SIRaw = msg.extractPayloadByte(22);
395
+ //if ((SIRaw & 0x80) === 0x80) {
396
+ // // negative SI
397
+ // scontroller.saturationIndex = (256 - SIRaw) / -100;
398
+ //}
399
+ //else {
400
+ // scontroller.saturationIndex = msg.extractPayloadByte(22) / 100;
401
+ //}
402
+ //controller.calciumHardness = msg.extractPayloadIntBE(23);
403
+
404
+ //// scontroller.status2 = msg.extractPayloadByte(25); // remove/unsure?
405
+ //controller.cyanuricAcid = msg.extractPayloadByte(26);
406
+ //controller.alkalinity = msg.extractPayloadIntBE(27);
407
+
408
+ //// scontroller.waterFlow = msg.extractPayloadByte(30); // This is probably the temp units.
409
+ //scontroller.ph.probe.tempUnits = 0;//msg.extractPayloadByte(30); See Above. This is probably the units.
410
+ //scontroller.ph.probe.temperature = msg.extractPayloadByte(31);
411
+
412
+ //msg.extractPayloadByte(33);
413
+
414
+ //// [0, { name: 'dosing', desc: 'Dosing' }],
415
+ //// [1, { name: 'monitoring', desc: 'Monitoring' }],
416
+ //// [2, { name: 'mixing', desc: 'Mixing' }]
417
+
418
+ //scontroller.ph.dosingStatus = (msg.extractPayloadByte(34) & 0x30) >> 4; // mask 00xx0000 and shift bit 5 & 6
419
+ //scontroller.orp.dosingStatus = (msg.extractPayloadByte(34) & 0xC0) >> 6; // mask xx000000 and shift bit 7 & 8
420
+ //scontroller.ph.flowDelay = scontroller.orp.flowDelay = (msg.extractPayloadByte(35) & 0x02) === 1 ? true : false;
421
+ //scontroller.status = msg.extractPayloadByte(35) & 0x80 >> 7; // to be verified as comms lost
422
+ //scontroller.ph.manualDosing = (msg.extractPayloadByte(35) & 0x08) === 1 ? true : false;
423
+ //controller.orp.useChlorinator = (msg.extractPayloadByte(35) & 0x10) === 1 ? true : false;
424
+ //controller.HMIAdvancedDisplay = (msg.extractPayloadByte(35) & 0x20) === 1 ? true : false;
425
+ //controller.ph.phSupply = (msg.extractPayloadByte(35) & 0x40) === 1 ? true : false; // acid pH dosing = 1; base pH dosing = 0;
426
+ //scontroller.firmware = `${msg.extractPayloadByte(37)}.${msg.extractPayloadByte(36).toString().padStart(3, '0')}`
427
+
428
+ //const warnings = scontroller.warnings;
429
+ //warnings.waterChemistry = msg.extractPayloadByte(38);
430
+ //warnings.pHLockout = msg.extractPayloadByte(33) & 0x01;
431
+ //warnings.pHDailyLimitReached = msg.extractPayloadByte(33) & 0x02;
432
+ //warnings.orpDailyLimitReached = msg.extractPayloadByte(33) & 0x04;
433
+ //warnings.invalidSetup = msg.extractPayloadByte(33) & 0x08;
434
+ //warnings.chlorinatorCommError = msg.extractPayloadByte(33) & 0x10;
435
+
436
+ //// RKS: This should really match against the body for the chlorinator when *Chem thinks it has been provided TDS.
437
+ //// RG: Byte 35, bit 4 indicates IntelliChlor is used. Until we know more, this logic suffices.
438
+ //if (sys.chlorinators.length > 0) {
439
+ // let chlor = state.chlorinators.find(elem => elem.id === 1);
440
+ // scontroller.orp.probe.saltLevel = (typeof chlor !== 'undefined') ? chlor.saltLevel : msg.extractPayloadByte(29) * 50;
441
+ //}
442
+ //else scontroller.orp.probe.saltLevel = 0;
443
+
444
+ //// manually emit extended values
445
+ //webApp.emitToClients('chemController', scontroller.getExtended()); // emit extended data
446
+ //scontroller.hasChanged = false; // try to avoid duplicate emits
447
+ //msg.isProcessed = true;
448
+ }
449
449
  }