nodejs-poolcontroller 8.1.2 → 8.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) 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 -0
  7. package/.github/workflows/ghcr-publish.yml +67 -0
  8. package/AGENTS.md +597 -0
  9. package/CONTRIBUTING.md +74 -74
  10. package/Changelog +292 -257
  11. package/Dockerfile +62 -19
  12. package/Gruntfile.js +40 -40
  13. package/LICENSE +661 -661
  14. package/README.md +318 -191
  15. package/anslq25/MessagesMock.ts +221 -221
  16. package/anslq25/boards/MockBoardFactory.ts +49 -49
  17. package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
  18. package/anslq25/boards/MockSystemBoard.ts +216 -216
  19. package/anslq25/chemistry/MockChlorinator.ts +98 -98
  20. package/anslq25/pumps/MockPump.ts +83 -83
  21. package/app.ts +115 -115
  22. package/config/Config.ts +57 -7
  23. package/config/VersionCheck.ts +63 -35
  24. package/controller/Constants.ts +809 -805
  25. package/controller/Equipment.ts +2688 -2664
  26. package/controller/Errors.ts +181 -181
  27. package/controller/Lockouts.ts +549 -549
  28. package/controller/State.ts +3738 -3690
  29. package/controller/boards/AquaLinkBoard.ts +1003 -1003
  30. package/controller/boards/BoardFactory.ts +53 -53
  31. package/controller/boards/EasyTouchBoard.ts +3202 -3202
  32. package/controller/boards/IntelliCenterBoard.ts +4393 -3899
  33. package/controller/boards/IntelliComBoard.ts +69 -69
  34. package/controller/boards/IntelliTouchBoard.ts +382 -382
  35. package/controller/boards/NixieBoard.ts +1944 -1929
  36. package/controller/boards/SunTouchBoard.ts +400 -400
  37. package/controller/boards/SystemBoard.ts +5268 -5268
  38. package/controller/comms/Comms.ts +1272 -1214
  39. package/controller/comms/ScreenLogic.ts +1665 -1665
  40. package/controller/comms/messages/Messages.ts +1433 -1243
  41. package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
  42. package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
  43. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  44. package/controller/comms/messages/config/ConfigMessage.ts +6 -0
  45. package/controller/comms/messages/config/CoverMessage.ts +0 -0
  46. package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
  47. package/controller/comms/messages/config/EquipmentMessage.ts +216 -210
  48. package/controller/comms/messages/config/ExternalMessage.ts +96 -10
  49. package/controller/comms/messages/config/FeatureMessage.ts +0 -0
  50. package/controller/comms/messages/config/GeneralMessage.ts +0 -0
  51. package/controller/comms/messages/config/HeaterMessage.ts +0 -0
  52. package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
  53. package/controller/comms/messages/config/OptionsMessage.ts +194 -174
  54. package/controller/comms/messages/config/PumpMessage.ts +0 -0
  55. package/controller/comms/messages/config/RemoteMessage.ts +0 -0
  56. package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
  57. package/controller/comms/messages/config/SecurityMessage.ts +0 -0
  58. package/controller/comms/messages/config/ValveMessage.ts +0 -0
  59. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
  60. package/controller/comms/messages/status/EquipmentStateMessage.ts +1158 -822
  61. package/controller/comms/messages/status/HeaterStateMessage.ts +135 -135
  62. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
  63. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
  64. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  65. package/controller/comms/messages/status/RegalModbusStateMessage.ts +411 -0
  66. package/controller/comms/messages/status/VersionMessage.ts +103 -41
  67. package/controller/nixie/Nixie.ts +173 -173
  68. package/controller/nixie/NixieEquipment.ts +104 -104
  69. package/controller/nixie/bodies/Body.ts +120 -120
  70. package/controller/nixie/bodies/Filter.ts +135 -135
  71. package/controller/nixie/chemistry/ChemController.ts +2724 -2724
  72. package/controller/nixie/chemistry/ChemDoser.ts +806 -806
  73. package/controller/nixie/chemistry/Chlorinator.ts +367 -367
  74. package/controller/nixie/circuits/Circuit.ts +478 -478
  75. package/controller/nixie/heaters/Heater.ts +834 -834
  76. package/controller/nixie/pumps/Pump.ts +1194 -996
  77. package/controller/nixie/schedules/Schedule.ts +401 -401
  78. package/controller/nixie/valves/Valve.ts +170 -170
  79. package/defaultConfig.json +352 -347
  80. package/docker-compose.yml +32 -0
  81. package/logger/DataLogger.ts +448 -448
  82. package/logger/Logger.ts +448 -436
  83. package/package.json +58 -60
  84. package/sendSocket.js +32 -32
  85. package/tsconfig.json +25 -25
  86. package/types/express-multer.d.ts +32 -0
  87. package/web/Server.ts +1937 -1927
  88. package/web/bindings/aqualinkD.json +559 -559
  89. package/web/bindings/influxDB.json +1066 -1066
  90. package/web/bindings/mqtt.json +721 -721
  91. package/web/bindings/mqttAlt.json +746 -746
  92. package/web/bindings/rulesManager.json +54 -54
  93. package/web/bindings/smartThings-Hubitat.json +31 -31
  94. package/web/bindings/valveRelays.json +20 -20
  95. package/web/bindings/vera.json +25 -25
  96. package/web/interfaces/baseInterface.ts +188 -188
  97. package/web/interfaces/httpInterface.ts +148 -148
  98. package/web/interfaces/influxInterface.ts +283 -283
  99. package/web/interfaces/mqttInterface.ts +695 -695
  100. package/web/interfaces/ruleInterface.ts +101 -87
  101. package/web/services/config/Config.ts +1063 -1053
  102. package/web/services/config/ConfigSocket.ts +0 -0
  103. package/web/services/state/State.ts +0 -0
  104. package/web/services/state/StateSocket.ts +0 -0
  105. package/web/services/utilities/Utilities.ts +233 -233
  106. package/.github/workflows/docker-publish-njsPC-linux.yml +0 -50
@@ -1,37 +1,37 @@
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
-
23
- export class IntelliValveStateMessage {
24
- public static process(msg: Inbound) {
25
- if (sys.controllerType === ControllerType.Unknown) return;
26
- // We only want to process the messages that are coming from IntelliValve.
27
- if (msg.source !== 12) return;
28
- switch (msg.action) {
29
- case 82: // This is hail from the valve that says it is not bound yet.
30
- break;
31
- default:
32
- logger.info(`IntelliValve sent an unknown action ${msg.action}`);
33
- break;
34
- }
35
- state.emitEquipmentChanges();
36
- }
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
+
23
+ export class IntelliValveStateMessage {
24
+ public static process(msg: Inbound) {
25
+ if (sys.controllerType === ControllerType.Unknown) return;
26
+ // We only want to process the messages that are coming from IntelliValve.
27
+ if (msg.source !== 12) return;
28
+ switch (msg.action) {
29
+ case 82: // This is hail from the valve that says it is not bound yet.
30
+ break;
31
+ default:
32
+ logger.info(`IntelliValve sent an unknown action ${msg.action}`);
33
+ break;
34
+ }
35
+ state.emitEquipmentChanges();
36
+ }
37
37
  }
@@ -0,0 +1,411 @@
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, Outbound, Protocol } from "../Messages";
19
+ import { state } from "../../../State";
20
+ import { sys, ControllerType } from "../../../Equipment";
21
+ import { conn } from "../../Comms";
22
+ import { logger } from "../../../../logger/Logger";
23
+
24
+ // Create a fault object to hold the fault codes and descriptions
25
+ let faultCodes = {
26
+ 0x21: "Software overcurrent",
27
+ 0x22: "DC overvoltage",
28
+ 0x23: "DC undervoltage",
29
+ 0x26: "Hardware overcurrent",
30
+ 0x2A: "Startup failure",
31
+ 0x2D: "Processor - Fatal",
32
+ 0x2E: "IGBT over temperature",
33
+ 0x2F: "Loss of phase",
34
+ 0x30: "Low power",
35
+ 0x31: "Processor - Registers",
36
+ 0x32: "Processor - Program counter",
37
+ 0x33: "Processor - Interrupt/Execution",
38
+ 0x34: "Processor - Clock",
39
+ 0x35: "Processor - Flash Memory",
40
+ 0x36: "Ras fault",
41
+ 0x37: "Processor - ADC",
42
+ 0x3C: "Keypad fault",
43
+ 0x3D: "LVB data flash fault",
44
+ 0x3E: "Comm loss fault - LVB & Drive",
45
+ 0x3F: "Generic fault",
46
+ 0x40: "Coherence fault",
47
+ 0x41: "UL fault",
48
+ 0x42: "SVRS fault type 1",
49
+ 0x43: "SVRS fault type 2",
50
+ 0x44: "SVRS fault type 13",
51
+ }
52
+
53
+ let nackErrors = {
54
+ 0x01: "Command not recognized / illegal",
55
+ 0x02: "Operand out of allowed range",
56
+ 0x03: "Data out of range",
57
+ 0x04: "General failure: fault mode",
58
+ 0x05: "Incorrect command length",
59
+ 0x06: "Command cannot be executed now",
60
+ 0x09: "Buffer error (not used)",
61
+ 0x0A: "Running parameters incomplete (not used)",
62
+ }
63
+
64
+ export class RegalModbusStateMessage {
65
+ public static process(msg: Inbound) {
66
+
67
+ // debug log the message object
68
+ logger.debug(`RegalModbusStateMessage.process ${JSON.stringify(msg)}`);
69
+
70
+ let addr = msg.header[0];
71
+ let functionCode = msg.header[1];
72
+ let ack = msg.header[2];
73
+
74
+ if (ack == 0x20) return;
75
+ if (ack in nackErrors) {
76
+ logger.debug(`RegalModbusStateMessage.process NACK: ${nackErrors[ack]} (Address: ${addr})`);
77
+ return;
78
+ }
79
+ if (ack != 0x10) {
80
+ logger.debug(`RegalModbusStateMessage.process Unknown ACK: ${ack} (Address: ${addr})`);
81
+ return;
82
+ }
83
+
84
+ // If we're here, we have an ack=0x10 message
85
+
86
+ let pumpCfg = sys.pumps.getPumpByAddress(addr, false, { isActive: false });
87
+ let pumpId = pumpCfg.id;
88
+ let pumpType = sys.board.valueMaps.pumpTypes.transform(pumpCfg.type);
89
+ let pumpState = state.pumps.getItemById(pumpId, pumpCfg.isActive === true);
90
+
91
+ logger.debug(`RegalModbusStateMessage.process.pstate ${JSON.stringify(pumpState)}`);
92
+
93
+
94
+ switch (functionCode) {
95
+ case 0x41: { // Go
96
+ logger.debug(`RegalModbusStateMessage.process Go (Address: ${addr})`);
97
+ break;
98
+ }
99
+ case 0x42: { // Stop
100
+ logger.debug(`RegalModbusStateMessage.process Stop (Address: ${addr})`);
101
+ break;
102
+ }
103
+ case 0x43: { // Status
104
+ let status = msg.extractPayloadByte(0);
105
+ switch (status) {
106
+ case 0x00: { // stop mode - motor stopped
107
+ logger.debug(`RegalModbusStateMessage.process Status: Stop (Address: ${addr})`);
108
+ pumpState.driveState = 0;
109
+ pumpState.command = 4; // dashPanel assumes command = 10 in running state
110
+ break;
111
+ }
112
+ case 0x09: { // run mode - boot (motor is getting ready to spin)
113
+ logger.debug(`RegalModbusStateMessage.process Status: Boot (Address: ${addr})`);
114
+ pumpState.driveState = 1;
115
+ pumpState.command = 10; // dashPanel assumes command = 10 in running state
116
+ break;
117
+ }
118
+ case 0x0B: { // run mode - vector
119
+ logger.debug(`RegalModbusStateMessage.process Status: Vector (Address: ${addr})`);
120
+ pumpState.driveState = 2;
121
+ pumpState.command = 10; // dashPanel assumes command = 10 in running state
122
+ break;
123
+ }
124
+ case 0x20: { // fault mode - motor stopped
125
+ logger.debug(`RegalModbusStateMessage.process Status: Fault (Address: ${addr})`);
126
+ pumpState.driveState = 4;
127
+ pumpState.command = 4; // dashPanel assumes command = 10 in running state
128
+ break;
129
+ }
130
+ }
131
+ break;
132
+ }
133
+ case 0x44: { // Set demand
134
+ let mode = msg.extractPayloadByte(0);
135
+ let demandLo = msg.extractPayloadByte(1);
136
+ let demandHi = msg.extractPayloadByte(2);
137
+
138
+ switch (mode) {
139
+ case 0: { // Speed control, demand = RPM * 4
140
+ let rpm = RegalModbusStateMessage.demandToRPM(demandLo, demandHi);
141
+ logger.debug(`RegalModbusStateMessage.process Speed: ${rpm} (Address: ${addr})`);
142
+ pumpState.rpm = rpm;
143
+ break;
144
+ }
145
+ case 1: { // Torque control, demand = lbf-ft * 1200
146
+ logger.debug(`RegalModbusStateMessage.process Ignoring torque: ${demandLo}, ${demandHi} (Address: ${addr})`);
147
+ break;
148
+ }
149
+ case 2: { // Reserved (used to be flow)
150
+ logger.debug(`RegalModbusStateMessage.process Ignoring reserved demand mode ${mode}: ${demandLo}, ${demandHi} (Address: ${addr})`);
151
+ break;
152
+ }
153
+ case 3: { // Reserved
154
+ logger.debug(`RegalModbusStateMessage.process Ignoring reserved demand mode ${mode}: ${demandLo}, ${demandHi} (Address: ${addr})`);
155
+ break;
156
+ }
157
+ }
158
+ break;
159
+ }
160
+ case 0x45: { // Read sensor
161
+ let page = msg.extractPayloadByte(0);
162
+ let sensorAddr = msg.extractPayloadByte(1);
163
+ let valueLo = msg.extractPayloadByte(2);
164
+ let valueHi = msg.extractPayloadByte(3);
165
+ let raw_value = (valueHi << 8) + valueLo;
166
+
167
+ let scaleValue = (value: number, scale: number) => {
168
+ return value / scale;
169
+ };
170
+ let scaled_value;
171
+
172
+ switch (page) {
173
+ case 0: {
174
+ switch (sensorAddr) {
175
+ case 0x00: { // Motor speed
176
+ scaled_value = scaleValue(raw_value, 4);
177
+ logger.debug(`RegalModbusStateMessage.process Motor speed: ${scaled_value} (Address: ${addr})`);
178
+ pumpState.rpm = scaled_value;
179
+ break;
180
+ }
181
+ case 0x01: { // Motor current
182
+ scaled_value = scaleValue(raw_value, 1000);
183
+ logger.debug(`RegalModbusStateMessage.process Motor current: ${scaled_value} (Address: ${addr})`);
184
+ break;
185
+ }
186
+ case 0x02: { // Operating mode
187
+ switch (raw_value) {
188
+ case 0: { // Speed control
189
+ logger.debug(`RegalModbusStateMessage.process Operating mode: Speed control (Address: ${addr})`);
190
+ break;
191
+ }
192
+ case 1: { // Torque control
193
+ logger.debug(`RegalModbusStateMessage.process Operating mode: Torque control (Address: ${addr})`);
194
+ break;
195
+ }
196
+ }
197
+ break;
198
+ }
199
+ case 0x03: { // Demand sent to motor
200
+ logger.debug(`RegalModbusStateMessage.process Raw (unscaled) demand sent to motor: ${raw_value} (Address: ${addr})`);
201
+ break;
202
+ }
203
+ case 0x04: { // Torque
204
+ scaled_value = scaleValue(raw_value, 1200);
205
+ logger.debug(`RegalModbusStateMessage.process Torque: ${scaled_value} (Address: ${addr})`);
206
+ break;
207
+ }
208
+ case 0x05: { // Inverter input power
209
+ logger.debug(`RegalModbusStateMessage.process Raw (unscaled) inverter input power: ${raw_value} (Address: ${addr})`);
210
+ break;
211
+ }
212
+ case 0x06: { // DC bus voltage
213
+ scaled_value = scaleValue(raw_value, 64);
214
+ logger.debug(`RegalModbusStateMessage.process DC bus voltage: ${scaled_value} (Address: ${addr})`);
215
+ break;
216
+ }
217
+ case 0x07: { // Ambient temperature
218
+ scaled_value = scaleValue(raw_value, 128);
219
+ logger.debug(`RegalModbusStateMessage.process Ambient temperature: ${scaled_value} (Address: ${addr})`);
220
+ break;
221
+ }
222
+ case 0x08: { // Status
223
+ switch (raw_value) {
224
+ case 0x00: { // stop mode - motor stopped
225
+ logger.debug(`RegalModbusStateMessage.process Status: Stop (Address: ${addr})`);
226
+ pumpState.driveState = 0;
227
+ break;
228
+ }
229
+ case 0x09: { // run mode - boot (motor is getting ready to spin)
230
+ logger.debug(`RegalModbusStateMessage.process Status: Boot (Address: ${addr})`);
231
+ pumpState.driveState = 1;
232
+ break;
233
+ }
234
+ case 0x0B: { // run mode - vector
235
+ logger.debug(`RegalModbusStateMessage.process Status: Vector (Address: ${addr})`);
236
+ pumpState.driveState = 2;
237
+ break;
238
+ }
239
+ case 0x20: { // fault mode - motor stopped
240
+ logger.debug(`RegalModbusStateMessage.process Status: Fault (Address: ${addr})`);
241
+ pumpState.driveState = 4;
242
+ break;
243
+ }
244
+ }
245
+ break;
246
+ }
247
+ case 0x09: { // Previous fault
248
+ if (raw_value in faultCodes) {
249
+ logger.debug(`RegalModbusStateMessage.process Previous fault: ${faultCodes[raw_value]} (Address: ${addr})`);
250
+ } else {
251
+ logger.debug(`RegalModbusStateMessage.process Previous fault: Unknown fault code ${raw_value} (Address: ${addr})`);
252
+ }
253
+ break;
254
+ }
255
+ case 0X0A: { // Output power
256
+ scaled_value = scaleValue(raw_value, 1);
257
+ logger.debug(`RegalModbusStateMessage.process Shaft power (W): ${scaled_value} (Address: ${addr})`);
258
+ pumpState.watts = scaled_value;
259
+ break;
260
+ }
261
+ case 0x0B: { // SVRS Bypass Status
262
+ break;
263
+ }
264
+ case 0x0C: { // Number of current faults
265
+ logger.debug(`RegalModbusStateMessage.process Number of current faults: ${raw_value} (Address: ${addr})`);
266
+ break;
267
+ }
268
+ case 0x0D: { // Motor line voltage
269
+ logger.debug(`RegalModbusStateMessage.process Raw (unscaled) motor line voltage: ${raw_value} (Address: ${addr})`);
270
+ break;
271
+ }
272
+ case 0x0E: { // Ramp status
273
+ logger.debug(`RegalModbusStateMessage.process Ramp status: ${raw_value} (Address: ${addr})`);
274
+ break;
275
+ }
276
+ case 0x0F: { // Number of total fault
277
+ logger.debug(`RegalModbusStateMessage.process Number of total faults: ${raw_value} (Address: ${addr})`);
278
+ break;
279
+ }
280
+ case 0x10: { // Prime status
281
+ switch (raw_value) {
282
+ case 0: { // Not priming
283
+ logger.debug(`RegalModbusStateMessage.process Prime status: Not priming (Address: ${addr})`);
284
+ break;
285
+ }
286
+ case 1: { // Priming running
287
+ logger.debug(`RegalModbusStateMessage.process Prime status: Priming running (Address: ${addr})`);
288
+ break;
289
+ }
290
+ case 2: { // Priming completed
291
+ logger.debug(`RegalModbusStateMessage.process Prime status: Priming completed (Address: ${addr})`);
292
+ break;
293
+ }
294
+ }
295
+ break;
296
+ }
297
+ case 0x11: { // Motor input power
298
+ logger.debug(`RegalModbusStateMessage.process Raw (unscaled) motor input power: ${raw_value} (Address: ${addr})`);
299
+ break;
300
+ }
301
+ case 0x12: { // IGBT temperature
302
+ scaled_value = scaleValue(raw_value, 128);
303
+ logger.debug(`RegalModbusStateMessage.process IGBT temperature: ${scaled_value} (Address: ${addr})`);
304
+ break;
305
+ }
306
+ case 0x13: { // PCB temperature
307
+ logger.debug(`RegalModbusStateMessage.process Raw (unscaled) PCB temperature: ${raw_value} (Address: ${addr})`);
308
+ break;
309
+ }
310
+ case 0x14: { // Status of external input
311
+ switch (raw_value) {
312
+ case 0: { // No external input
313
+ logger.debug(`RegalModbusStateMessage.process External input: No external input (Address: ${addr})`);
314
+ break;
315
+ }
316
+ case 3: { // PWM
317
+ logger.debug(`RegalModbusStateMessage.process External input: PWM (Address: ${addr})`);
318
+ break;
319
+ }
320
+ case 4: { // DI_1 present
321
+ logger.debug(`RegalModbusStateMessage.process External input: DI_1 present (Address: ${addr})`);
322
+ break;
323
+ }
324
+ case 5: { // DI_2 present
325
+ logger.debug(`RegalModbusStateMessage.process External input: DI_2 present (Address: ${addr})`);
326
+ break;
327
+ }
328
+ case 6: { // DI_3 present
329
+ logger.debug(`RegalModbusStateMessage.process External input: DI_3 present (Address: ${addr})`);
330
+ break;
331
+ }
332
+ case 7: { // DI_4 present
333
+ logger.debug(`RegalModbusStateMessage.process External input: DI_4 present (Address: ${addr})`);
334
+ break;
335
+ }
336
+ case 8: { // Serial input
337
+ logger.debug(`RegalModbusStateMessage.process External input: Serial input (Address: ${addr})`);
338
+ break;
339
+ }
340
+ }
341
+ break;
342
+ }
343
+ case 0x15: { // Reference speed
344
+ scaled_value = scaleValue(raw_value, 4);
345
+ logger.debug(`RegalModbusStateMessage.process Reference speed: ${scaled_value} (Address: ${addr})`);
346
+ break;
347
+ }
348
+ }
349
+ break;
350
+ }
351
+ default: {
352
+ logger.debug(`RegalModbusStateMessage.process Page 1: ${page} (Address: ${addr}) - Not yet implemented`);
353
+ break;
354
+ }
355
+ }
356
+ break;
357
+ }
358
+ case 0x46: { // Read identification
359
+ logger.debug(`RegalModbusStateMessage.process Read identification (Address: ${addr}) - Not yet implemented`);
360
+ break;
361
+ }
362
+ case 0x64: { // Read/write configuration
363
+ logger.debug(`RegalModbusStateMessage.process Read/write configuration (Address: ${addr}) - Not yet implemented`);
364
+ break;
365
+ }
366
+ default: {
367
+ logger.debug(`RegalModbusStateMessage.process Unknown function code: ${functionCode} (Address: ${addr})`);
368
+ break;
369
+ }
370
+ }
371
+ state.emitEquipmentChanges();
372
+ }
373
+
374
+ public static rpmToDemand(rpm: number): [number, number] {
375
+ /**
376
+ * Converts an RPM value to a RegalModbus demand payload in speed control mode.
377
+ *
378
+ * @param {number} rpm - Desired motor speed in RPM.
379
+ * @returns {[number, number]} - [demand_lo, demand_hi]
380
+ * - demand_lo: lower byte of demand
381
+ * - demand_hi: upper byte of demand
382
+ * @throws {Error} - If RPM is out of valid range for RegalModbus demand (0–16383).
383
+ */
384
+ if (rpm < 0 || rpm * 4 > 0xFFFF) {
385
+ throw new Error("RPM is out of valid range for RegalModbus demand (0–16383)");
386
+ }
387
+
388
+ const rawDemand = Math.round(rpm * 4); // Scale RPM by 4
389
+ const demandLo = rawDemand & 0xFF;
390
+ const demandHi = (rawDemand >> 8) & 0xFF;
391
+
392
+ return [demandLo, demandHi];
393
+ }
394
+
395
+ public static demandToRPM(demandLo: number, demandHi: number): number {
396
+ /**
397
+ * Converts a RegalModbus demand payload to an RPM value.
398
+ *
399
+ * @param {number} demandLo - Lower byte of demand.
400
+ * @param {number} demandHi - Upper byte of demand.
401
+ * @returns {number} - Motor speed in RPM.
402
+ * @throws {Error} - If demand is out of valid range for RPM (0–16383).
403
+ **/
404
+ const rawDemand = (demandHi << 8) | demandLo; // Combine high and low bytes
405
+ if (rawDemand < 0 || rawDemand > 0xFFFF) {
406
+ throw new Error("Demand is out of valid range for RPM (0–16383)");
407
+ }
408
+ const rpm = Math.round(rawDemand / 4); // Scale back to RPM
409
+ return rpm;
410
+ }
411
+ }
@@ -1,42 +1,104 @@
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 { sys, ConfigVersion } from "../../../Equipment";
20
- export class VersionMessage {
21
- public static process(msg: Inbound): void {
22
- var ver: ConfigVersion = new ConfigVersion({});
23
- ver.options = msg.extractPayloadInt(6);
24
- ver.circuits = msg.extractPayloadInt(8);
25
- ver.features = msg.extractPayloadInt(10);
26
- ver.schedules = msg.extractPayloadInt(12);
27
- ver.pumps = msg.extractPayloadInt(14);
28
- ver.remotes = msg.extractPayloadInt(16);
29
- ver.circuitGroups = msg.extractPayloadInt(18);
30
- ver.chlorinators = msg.extractPayloadInt(20);
31
- ver.intellichem = msg.extractPayloadInt(22);
32
- ver.valves = msg.extractPayloadInt(24);
33
- ver.heaters = msg.extractPayloadInt(26);
34
- ver.security = msg.extractPayloadInt(28);
35
- ver.general = msg.extractPayloadInt(30);
36
- ver.equipment = msg.extractPayloadInt(32);
37
- ver.covers = msg.extractPayloadInt(34);
38
- ver.systemState = msg.extractPayloadInt(36);
39
- sys.processVersionChanges(ver);
40
- msg.isProcessed = true;
41
- }
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, Message, Outbound, Response } from "../Messages";
19
+ import { sys, ConfigVersion } from "../../../Equipment";
20
+ import { logger } from "../../../../logger/Logger";
21
+
22
+ export class VersionMessage {
23
+ // Debounce config refresh requests to avoid duplicate requests from overlapping triggers
24
+ private static lastConfigRefreshTime: number = 0;
25
+ private static readonly CONFIG_REFRESH_DEBOUNCE_MS = 2000; // 2 seconds
26
+
27
+ /**
28
+ * Shared method to trigger a config refresh with debouncing.
29
+ * Prevents duplicate requests when multiple triggers fire in quick succession.
30
+ */
31
+ private static triggerConfigRefresh(source: string): void {
32
+ const now = Date.now();
33
+ if (now - this.lastConfigRefreshTime < this.CONFIG_REFRESH_DEBOUNCE_MS) {
34
+ logger.silly(`v3.004+ ${source}: Skipping config refresh (debounced, last was ${now - this.lastConfigRefreshTime}ms ago)`);
35
+ return;
36
+ }
37
+ this.lastConfigRefreshTime = now;
38
+
39
+ (sys.board as any).needsConfigChanges = true;
40
+ // Invalidate cached options version so queueChanges() will request category 0.
41
+ // OCP doesn't increment options version when heat mode/setpoints change,
42
+ // so we force a refresh by clearing our cached version.
43
+ sys.configVersion.options = 0;
44
+ logger.silly(`v3.004+ ${source}: Sending Action 228`);
45
+ Outbound.create({
46
+ dest: 16, action: 228, payload: [0], retries: 2,
47
+ response: Response.create({ action: 164 })
48
+ }).sendAsync();
49
+ }
50
+
51
+ /**
52
+ * v3.004+ Piggyback: When another device sends Action 228 to OCP,
53
+ * send our own to catch config changes. See .plan/202-intellicenter-bodies-temps.md
54
+ */
55
+ public static processVersionRequest(msg: Inbound): void {
56
+ if (sys.equipment.isIntellicenterV3 &&
57
+ msg.source !== Message.pluginAddress && // Not from us
58
+ msg.dest === 16) { // Directed to OCP
59
+ this.triggerConfigRefresh('Piggyback');
60
+ }
61
+ msg.isProcessed = true;
62
+ }
63
+
64
+ /**
65
+ * v3.004+ ACK Trigger: When OCP ACKs a Wireless device's Action 168,
66
+ * trigger a config refresh. OCP doesn't send Action 228 after Wireless changes,
67
+ * so we must detect the ACK and request config ourselves.
68
+ * See AGENTS.md for protocol details.
69
+ */
70
+ public static processAction168Ack(msg: Inbound): void {
71
+ // Only for v3.004+ when OCP (src=16) ACKs a non-njsPC device's 168
72
+ if (sys.equipment.isIntellicenterV3 &&
73
+ msg.source === 16 && // From OCP
74
+ msg.dest !== Message.pluginAddress && // Not to us
75
+ msg.dest !== 16 && // Not to OCP itself
76
+ msg.payload.length > 0 &&
77
+ msg.payload[0] === 168) { // ACKing Action 168
78
+ this.triggerConfigRefresh(`ACK Trigger (device ${msg.dest})`);
79
+ }
80
+ msg.isProcessed = true;
81
+ }
82
+
83
+ public static process(msg: Inbound): void {
84
+ var ver: ConfigVersion = new ConfigVersion({});
85
+ ver.options = msg.extractPayloadInt(6);
86
+ ver.circuits = msg.extractPayloadInt(8);
87
+ ver.features = msg.extractPayloadInt(10);
88
+ ver.schedules = msg.extractPayloadInt(12);
89
+ ver.pumps = msg.extractPayloadInt(14);
90
+ ver.remotes = msg.extractPayloadInt(16);
91
+ ver.circuitGroups = msg.extractPayloadInt(18);
92
+ ver.chlorinators = msg.extractPayloadInt(20);
93
+ ver.intellichem = msg.extractPayloadInt(22);
94
+ ver.valves = msg.extractPayloadInt(24);
95
+ ver.heaters = msg.extractPayloadInt(26);
96
+ ver.security = msg.extractPayloadInt(28);
97
+ ver.general = msg.extractPayloadInt(30);
98
+ ver.equipment = msg.extractPayloadInt(32);
99
+ ver.covers = msg.extractPayloadInt(34);
100
+ ver.systemState = msg.extractPayloadInt(36);
101
+ sys.processVersionChanges(ver);
102
+ msg.isProcessed = true;
103
+ }
42
104
  }