nodejs-poolcontroller 7.5.1 → 7.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -0
- package/.github/ISSUE_TEMPLATE/2-docs.md +12 -0
- package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -0
- package/.github/ISSUE_TEMPLATE/config.yml +8 -0
- package/Changelog +19 -0
- package/Dockerfile +3 -3
- package/README.md +13 -8
- package/app.ts +1 -1
- package/config/Config.ts +38 -2
- package/config/VersionCheck.ts +27 -12
- package/controller/Constants.ts +2 -1
- package/controller/Equipment.ts +193 -9
- package/controller/Errors.ts +10 -0
- package/controller/Lockouts.ts +503 -0
- package/controller/State.ts +269 -64
- package/controller/boards/AquaLinkBoard.ts +1000 -0
- package/controller/boards/BoardFactory.ts +4 -0
- package/controller/boards/EasyTouchBoard.ts +468 -144
- package/controller/boards/IntelliCenterBoard.ts +466 -307
- package/controller/boards/IntelliTouchBoard.ts +37 -5
- package/controller/boards/NixieBoard.ts +671 -141
- package/controller/boards/SystemBoard.ts +1397 -641
- package/controller/comms/Comms.ts +462 -362
- package/controller/comms/messages/Messages.ts +174 -30
- package/controller/comms/messages/config/ChlorinatorMessage.ts +6 -3
- package/controller/comms/messages/config/CircuitMessage.ts +1 -0
- package/controller/comms/messages/config/ExternalMessage.ts +10 -8
- package/controller/comms/messages/config/HeaterMessage.ts +141 -29
- package/controller/comms/messages/config/OptionsMessage.ts +9 -2
- package/controller/comms/messages/config/PumpMessage.ts +53 -35
- package/controller/comms/messages/config/ScheduleMessage.ts +33 -25
- package/controller/comms/messages/config/ValveMessage.ts +2 -2
- package/controller/comms/messages/status/ChlorinatorStateMessage.ts +38 -86
- package/controller/comms/messages/status/EquipmentStateMessage.ts +59 -23
- package/controller/comms/messages/status/HeaterStateMessage.ts +57 -3
- package/controller/comms/messages/status/IntelliChemStateMessage.ts +56 -8
- package/controller/comms/messages/status/PumpStateMessage.ts +23 -1
- package/controller/nixie/Nixie.ts +1 -1
- package/controller/nixie/bodies/Body.ts +3 -0
- package/controller/nixie/chemistry/ChemController.ts +164 -51
- package/controller/nixie/chemistry/Chlorinator.ts +137 -88
- package/controller/nixie/circuits/Circuit.ts +51 -19
- package/controller/nixie/heaters/Heater.ts +241 -31
- package/controller/nixie/pumps/Pump.ts +488 -206
- package/controller/nixie/schedules/Schedule.ts +91 -35
- package/controller/nixie/valves/Valve.ts +1 -1
- package/defaultConfig.json +20 -0
- package/package.json +21 -21
- package/web/Server.ts +94 -49
- package/web/bindings/aqualinkD.json +505 -0
- package/web/bindings/influxDB.json +71 -1
- package/web/bindings/mqtt.json +98 -39
- package/web/bindings/mqttAlt.json +59 -1
- package/web/interfaces/baseInterface.ts +1 -0
- package/web/interfaces/httpInterface.ts +23 -2
- package/web/interfaces/influxInterface.ts +45 -10
- package/web/interfaces/mqttInterface.ts +114 -54
- package/web/services/config/Config.ts +55 -132
- package/web/services/state/State.ts +81 -4
- package/web/services/state/StateSocket.ts +4 -4
- package/web/services/utilities/Utilities.ts +8 -6
- package/.github/ISSUE_TEMPLATE/bug_report.md +0 -52
- package/config copy.json +0 -300
- package/issue_template.md +0 -52
|
@@ -21,13 +21,19 @@ import { sys, ControllerType, Chlorinator } from "../../../Equipment";
|
|
|
21
21
|
export class ChlorinatorStateMessage {
|
|
22
22
|
public static process(msg: Inbound) {
|
|
23
23
|
if (msg.protocol === Protocol.Chlorinator) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
// RKS: 03-29-22 A lot of water has gone under the bridge at this point and we know much more. First there are two types of messages. Those inbound
|
|
25
|
+
// messages that are being sent to the chlorinator as commands and responses from the chlorinator. We have also determined that the chlorinator does
|
|
26
|
+
// not actually support a RS485 address. So njsPC can declare as many ports as required to communicate with multiple chlorinators.
|
|
27
|
+
//
|
|
28
|
+
// So instead of matching the chlorinator up with a specific id we need to match on the port as well for the inbound message. OCPs always operate on
|
|
29
|
+
// portId = 0. If the portId is > 0 then it is considered an Aux port. At this point you can only control 1 chlorinator per port so in order to
|
|
30
|
+
// control more than one chlorinator there needs to be only one chlorinator on each port.
|
|
31
|
+
let chlor: Chlorinator = sys.chlorinators.findItemByPortId(msg.portId || 0);
|
|
32
|
+
if (typeof chlor === 'undefined' || chlor.isActive === false) return; // Bail out of here if we don't find an active chlorinator.
|
|
33
|
+
let cstate: ChlorinatorState = state.chlorinators.getItemById(chlor.id, true);
|
|
27
34
|
if (msg.dest >= 1 && msg.dest <= 4) {
|
|
28
|
-
// RKS:
|
|
29
|
-
// is
|
|
30
|
-
cstate = state.chlorinators.getItemById(msg.dest, true);
|
|
35
|
+
// RKS: This message is from the OCP to the chlorinator. NOTE: We will not see these messages when the communication is coming from njsPC on any
|
|
36
|
+
// comms port. The processing for no comms is done in the Nixe control when the message is sent.
|
|
31
37
|
if (typeof cstate.lastComm === 'undefined') cstate.lastComm = new Date(1970, 0, 1, 0, 0, 0, 0).getTime();
|
|
32
38
|
// RG: I was getting some time deltas of 25-30s and bumped this up
|
|
33
39
|
else if (cstate.lastComm + (30 * 1000) < new Date().getTime()) {
|
|
@@ -35,27 +41,24 @@ export class ChlorinatorStateMessage {
|
|
|
35
41
|
cstate.status = 128;
|
|
36
42
|
}
|
|
37
43
|
chlor = sys.chlorinators.getItemById(msg.dest, true);
|
|
38
|
-
chlor.address = msg.dest + 79;
|
|
44
|
+
chlor.address = msg.dest + 79; // Theoretically, this will always be 80.
|
|
39
45
|
if (typeof chlor.isActive === 'undefined') cstate.isActive = chlor.isActive = true;
|
|
40
46
|
}
|
|
41
47
|
else {
|
|
42
|
-
//
|
|
43
|
-
|
|
44
|
-
chlor = sys.chlorinators.getItemById(msg.dest + 1, true);
|
|
48
|
+
// Response message from chlorinator. If the chlorinator is speaking to us then we need to hear it and clear the status when
|
|
49
|
+
// the previous status was no comms.
|
|
45
50
|
cstate.lastComm = new Date().getTime();
|
|
46
51
|
if (cstate.status === 128) cstate.status = 0;
|
|
47
52
|
}
|
|
48
53
|
cstate.body = chlor.body;
|
|
49
54
|
switch (msg.action) {
|
|
50
|
-
case 0: //
|
|
55
|
+
case 0: // Set control OCP->Chlorinator: [16,2,80,0][0][98,16,3]
|
|
51
56
|
break;
|
|
52
|
-
case 1: //
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
}
|
|
58
|
-
case 3: {
|
|
57
|
+
case 1: // Ack control Chlorinator->OCP response: [16,2,0,1][0,0][19,16,3]
|
|
58
|
+
// let chlor = sys.chlorinators.getItemById(1, true);
|
|
59
|
+
// chlor.isActive = true;
|
|
60
|
+
break;
|
|
61
|
+
case 3: // Chlorinator->OCP response for Get Model: [16,2,0,3][0,73,110,116,101,108,108,105,99,104,108,111,114,45,45,52,48][188,16,3]
|
|
59
62
|
// RKS: 07-16-20 -- It appears that this message doesn't always come. It is very likely that the newer versions of IntelliChlor
|
|
60
63
|
// do not respond to the 20 message. There have been multiple instances of this with recent versions of IntelliChlor. As a result,
|
|
61
64
|
// don't overwrite the name should the user set it.
|
|
@@ -65,12 +68,11 @@ export class ChlorinatorStateMessage {
|
|
|
65
68
|
// This is the model number of the chlorinator and the address is actually the second byte.
|
|
66
69
|
let name = msg.extractPayloadString(1, 16);
|
|
67
70
|
if (typeof chlor.name === 'undefined' || chlor.name === '') chlor.name = cstate.name = name;
|
|
68
|
-
if (typeof chlor.model === 'undefined') chlor.model = sys.board.valueMaps.chlorinatorModel.getValue(name.toLowerCase());
|
|
71
|
+
if (typeof chlor.model === 'undefined') chlor.model = sys.board.valueMaps.chlorinatorModel.getValue(name.toLowerCase());
|
|
69
72
|
cstate.isActive = chlor.isActive;
|
|
70
73
|
state.emitEquipmentChanges();
|
|
71
74
|
break;
|
|
72
|
-
|
|
73
|
-
case 17: {
|
|
75
|
+
case 17: // OCP->Chlorinator set output. [16,2,80,17][15][130,16,3]
|
|
74
76
|
// If the chlorinator is no longer talking to us then clear the current output.
|
|
75
77
|
if (cstate.status === 128) cstate.currentOutput = 0;
|
|
76
78
|
cstate.targetOutput = msg.extractPayloadByte(0);
|
|
@@ -78,13 +80,16 @@ export class ChlorinatorStateMessage {
|
|
|
78
80
|
// Some dumbass is trying to change our output. We need to set it back to 0.
|
|
79
81
|
sys.board.chlorinator.setChlorAsync({ id: chlor.id, disabled: chlor.disabled, superChlor: false, superChlorHours: 0 });
|
|
80
82
|
}
|
|
81
|
-
else if (chlor.isDosing && cstate.targetOutput !== 100){
|
|
83
|
+
else if (chlor.isDosing && cstate.targetOutput !== 100) {
|
|
82
84
|
sys.board.chlorinator.setChlorAsync({ id: chlor.id, isDosing: chlor.isDosing });
|
|
83
85
|
}
|
|
84
86
|
state.emitEquipmentChanges();
|
|
85
87
|
break;
|
|
86
|
-
|
|
87
|
-
|
|
88
|
+
case 21: // Set output OCP->Chlorinator [16,2,80,21][0][119,16,3]
|
|
89
|
+
// RKS: 03-29-22 There is something absolutley wrong about the understanding of this message. It has to do with the fact that you could never set
|
|
90
|
+
// the output percentage to anything greater than 25.6% if this were the case. I assume this has something to do with the
|
|
91
|
+
// way IntelliChlor works. Values between 0 and 20% can be fractional. In this case the OCP would send a 21 instead of a 17 with
|
|
92
|
+
// the fractional value. Anything above that value will be sent in a 17 message.
|
|
88
93
|
// Set Cell Output / 10
|
|
89
94
|
// This packet is coming through differently on the IntelliConnect.
|
|
90
95
|
// eg 13:42:31.304 VERBOSE Msg# 1531 Controller --> Salt cell: Set current output to 1.6 %: 16,2,80,21,0,119,16,3
|
|
@@ -95,34 +100,31 @@ export class ChlorinatorStateMessage {
|
|
|
95
100
|
// Some dumbass is trying to change our output. We need to set it back to 0.
|
|
96
101
|
sys.board.chlorinator.setChlorAsync({ id: chlor.id, disabled: chlor.disabled, superChlor: false, superChlorHours: 0 });
|
|
97
102
|
}
|
|
98
|
-
else if (chlor.isDosing && cstate.targetOutput !== 100){
|
|
103
|
+
else if (chlor.isDosing && cstate.targetOutput !== 100) {
|
|
99
104
|
sys.board.chlorinator.setChlorAsync({ id: chlor.id, isDosing: chlor.isDosing });
|
|
100
105
|
}
|
|
101
106
|
state.emitEquipmentChanges();
|
|
102
107
|
break;
|
|
103
|
-
|
|
104
|
-
case 18: {
|
|
108
|
+
case 18:
|
|
105
109
|
// Response to Set Salt Output (17 & 20)
|
|
106
110
|
// The most common failure with IntelliChlor is that the salt level stops reporting. Below should allow it to be fed from an alternate
|
|
107
111
|
// source like REM.
|
|
108
|
-
if(!chlor.ignoreSaltReading) cstate.saltLevel = msg.extractPayloadByte(0) * 50 || cstate.saltLevel || 0;
|
|
112
|
+
if (!chlor.ignoreSaltReading) cstate.saltLevel = msg.extractPayloadByte(0) * 50 || cstate.saltLevel || 0;
|
|
109
113
|
cstate.status = (msg.extractPayloadByte(1) & 0x007F); // Strip off the high bit. The chlorinator does not actually report this.
|
|
110
114
|
cstate.currentOutput = chlor.disabled ? 0 : cstate.setPointForCurrentBody;
|
|
111
115
|
state.emitEquipmentChanges();
|
|
112
116
|
break;
|
|
113
|
-
|
|
114
|
-
case 19: {
|
|
117
|
+
case 19:
|
|
115
118
|
// This is an iChlor message with no payload. Perhaps simply a keep alive for the iChlor.
|
|
116
119
|
// [16, 2, 80, 19][117, 16, 3]
|
|
117
120
|
break;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
121
|
+
case 20:
|
|
122
|
+
// OCP->Chlorinator Get model [16,2,80,20][0][118,16,3]
|
|
123
|
+
if (typeof chlor.type === 'undefined') chlor.type = msg.extractPayloadByte(0);
|
|
124
|
+
cstate.type = chlor.type;
|
|
122
125
|
state.emitEquipmentChanges();
|
|
123
126
|
break;
|
|
124
|
-
|
|
125
|
-
case 22: {
|
|
127
|
+
case 22: // Chlorinator->OCP this is actually an iChlor message and has no bearing on IntelliConnect.
|
|
126
128
|
// temp and output as seen from IntelliConnect.
|
|
127
129
|
// Issue #157 - https://github.com/tagyoureit/nodejs-poolController/issues/157
|
|
128
130
|
// [10 02, 10, 16], [00, 0f, 49, 00,05, 10], [85, 10,03] = hex
|
|
@@ -141,57 +143,7 @@ export class ChlorinatorStateMessage {
|
|
|
141
143
|
}
|
|
142
144
|
state.emitEquipmentChanges();
|
|
143
145
|
break;
|
|
144
|
-
}
|
|
145
146
|
}
|
|
146
147
|
}
|
|
147
|
-
// question: does IntelliCenter ever broadcast Chlorinator packet? Answer: Never. My guess is that this is actually
|
|
148
|
-
// a configuration message rather than a status message. Also, IntelliCenter has a 204 extension status that contains
|
|
149
|
-
// the current countdown timer for the superChlor in the last 2 bytes. Perhaps this is the equivalent.
|
|
150
|
-
//else if (msg.protocol === Protocol.Broadcast) {
|
|
151
|
-
// if (!state.isInitialized) return;
|
|
152
|
-
// // sample packet
|
|
153
|
-
// // [165,33,15,16,25,22],[1,10,128,29,132,0,73,110,116,101,108,108,105,99,104,108,111,114,45,45,52,48],[7,231]
|
|
154
|
-
// let chlorId = 1;
|
|
155
|
-
// let chlor = sys.chlorinators.getItemById(chlorId, true);
|
|
156
|
-
// if (chlor.isVirtual) { return; } // shouldn't get here except for testing Chlor on *Touch system.
|
|
157
|
-
// // installed = (aaaaaaa)1 so 1 = installed
|
|
158
|
-
// chlor.isActive = (msg.extractPayloadByte(0) & 0x01) === 1;
|
|
159
|
-
// if (chlor.isActive) {
|
|
160
|
-
// // RSG : making the assumption here that the chlorinator will be tied to the pool in any system that is not a shared body
|
|
161
|
-
// sys.equipment.maxBodies >= 1 || sys.equipment.shared === true ? chlor.body = 32 : chlor.body = 0;
|
|
162
|
-
// // outputSpaPercent field is aaaaaaab (binary) where aaaaaaa = % and b===installed (0=no,1=yes)
|
|
163
|
-
// // eg. a value of 41 is 00101001
|
|
164
|
-
// // spa percent = 0010100(b) so 10100 = 20
|
|
165
|
-
// if (!chlor.disabled) {
|
|
166
|
-
// // RKS: We don't want these setpoints if our chem controller disabled the
|
|
167
|
-
// // chlorinator. These should be 0 anyway.
|
|
168
|
-
// chlor.spaSetpoint = msg.extractPayloadByte(0) >> 1;
|
|
169
|
-
// chlor.poolSetpoint = msg.extractPayloadByte(1);
|
|
170
|
-
// }
|
|
171
|
-
// chlor.address = chlor.id + 79;
|
|
172
|
-
// chlor.superChlor = msg.extractPayloadByte(5) > 0;
|
|
173
|
-
// chlor.superChlorHours = msg.extractPayloadByte(5);
|
|
174
|
-
// chlor.name = msg.extractPayloadString(6, 16);
|
|
175
|
-
// let schlor = state.chlorinators.getItemById(chlorId, true);
|
|
176
|
-
// // The most common failure with IntelliChlor is that the salt level stops reporting. Below should allow it to be fed from an alternate
|
|
177
|
-
// // source like REM.
|
|
178
|
-
// schlor.saltLevel = msg.extractPayloadByte(3) * 50 || schlor.saltLevel || 0;
|
|
179
|
-
// schlor.status = msg.extractPayloadByte(4) & 0x007F; // Strip off the high bit. The chlorinator does not actually report this.;
|
|
180
|
-
// schlor.lastComm = new Date().getTime(); // rely solely on "true" chlor messages for this?
|
|
181
|
-
// schlor.poolSetpoint = chlor.poolSetpoint;
|
|
182
|
-
// schlor.spaSetpoint = chlor.spaSetpoint;
|
|
183
|
-
// schlor.superChlor = chlor.superChlor;
|
|
184
|
-
// schlor.superChlorHours = chlor.superChlorHours;
|
|
185
|
-
// schlor.name = chlor.name;
|
|
186
|
-
// schlor.body = chlor.body;
|
|
187
|
-
// if (state.temps.bodies.getItemById(1).isOn) schlor.targetOutput = chlor.disabled ? 0 : chlor.poolSetpoint;
|
|
188
|
-
// else if (state.temps.bodies.getItemById(2).isOn) schlor.targetOutput = chlor.disabled ? 0 : chlor.spaSetpoint;
|
|
189
|
-
// state.emitEquipmentChanges();
|
|
190
|
-
// }
|
|
191
|
-
// else {
|
|
192
|
-
// sys.chlorinators.removeItemById(chlorId);
|
|
193
|
-
// state.chlorinators.removeItemById(chlorId);
|
|
194
|
-
// }
|
|
195
|
-
//}
|
|
196
148
|
}
|
|
197
149
|
}
|
|
@@ -357,17 +357,41 @@ export class EquipmentStateMessage {
|
|
|
357
357
|
tbody.setPoint = cbody.setPoint;
|
|
358
358
|
tbody.name = cbody.name;
|
|
359
359
|
tbody.circuit = cbody.circuit = 6;
|
|
360
|
-
|
|
360
|
+
|
|
361
|
+
//RKS: This heat mode did not include all the bits necessary for hybrid heaters
|
|
362
|
+
//tbody.heatMode = cbody.heatMode = msg.extractPayloadByte(22) & 0x03;
|
|
363
|
+
tbody.heatMode = cbody.heatMode = msg.extractPayloadByte(22) & 0x33;
|
|
361
364
|
let heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
|
|
362
365
|
if (tbody.isOn) {
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
366
|
+
if (tbody.heaterOptions.hybrid > 0) {
|
|
367
|
+
// ETi When heating with
|
|
368
|
+
// Heatpump (1) = 12 H:true S:false C:false
|
|
369
|
+
// Gas (2) = 48 H:false S:true C:false
|
|
370
|
+
// Hybrid (3) = 48 H:true S:false C:false
|
|
371
|
+
// Dual (16) = 60 H:true S:true C:false
|
|
372
|
+
// What this means is that Touch actually treats the heat status as either heating with
|
|
373
|
+
// the primary heater for the body or the secondary. In the case of a hybrid heater
|
|
374
|
+
// the primary is a heatpump and the secondary is gas. In the case of gas + solar or gas + heatpump
|
|
375
|
+
// the gas heater is the primary and solar or heatpump is the secondary. So we need to dance a little bit
|
|
376
|
+
// here. We do this by checking the heater options.
|
|
377
|
+
|
|
378
|
+
// This can be the only heater solar cannot be installed with this.
|
|
379
|
+
let byte = msg.extractPayloadByte(10);
|
|
380
|
+
// Either the primary, secondary, or both is engaged.
|
|
381
|
+
if ((byte & 0x14) === 0x14) heatStatus = sys.board.valueMaps.heatStatus.getValue('dual');
|
|
382
|
+
else if (byte & 0x10) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
383
|
+
else if (byte & 0x04) heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
|
|
384
|
+
}
|
|
385
|
+
else {
|
|
386
|
+
//const heaterActive = (msg.extractPayloadByte(10) & 0x0C) === 12;
|
|
387
|
+
//const solarActive = (msg.extractPayloadByte(10) & 0x30) === 48;
|
|
388
|
+
const heaterActive = (msg.extractPayloadByte(10) & 0x04) === 0x04;
|
|
389
|
+
const solarActive = (msg.extractPayloadByte(10) & 0x10) === 0x10;
|
|
390
|
+
const cooling = solarActive && tbody.temp > tbody.setPoint;
|
|
391
|
+
if (heaterActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
392
|
+
if (cooling) heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
|
|
393
|
+
else if (solarActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
|
|
394
|
+
}
|
|
371
395
|
}
|
|
372
396
|
tbody.heatStatus = heatStatus;
|
|
373
397
|
sys.board.schedules.syncScheduleHeatSourceAndSetpoint(cbody, tbody);
|
|
@@ -380,21 +404,32 @@ export class EquipmentStateMessage {
|
|
|
380
404
|
tbody.temp = sys.equipment.shared ? state.temps.waterSensor1 : state.temps.waterSensor2;
|
|
381
405
|
tbody.isOn = true;
|
|
382
406
|
} else tbody.isOn = false;
|
|
383
|
-
|
|
407
|
+
//RKS: This heat mode did not include all the bits necessary for hybrid heaters
|
|
408
|
+
//tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(22) & 0x0C) >> 2;
|
|
409
|
+
tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(22) & 0xCC) >> 2;
|
|
384
410
|
tbody.setPoint = cbody.setPoint;
|
|
385
411
|
tbody.name = cbody.name;
|
|
386
412
|
tbody.circuit = cbody.circuit = 1;
|
|
387
413
|
let heatStatus = sys.board.valueMaps.heatStatus.getValue('off');
|
|
388
414
|
if (tbody.isOn) {
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
else
|
|
415
|
+
if (tbody.heaterOptions.hybrid > 0) {
|
|
416
|
+
// This can be the only heater solar cannot be installed with this.
|
|
417
|
+
let byte = msg.extractPayloadByte(10);
|
|
418
|
+
// Either the primary, secondary, or both is engaged.
|
|
419
|
+
if ((byte & 0x28) === 0x28) heatStatus = sys.board.valueMaps.heatStatus.getValue('dual');
|
|
420
|
+
else if (byte & 0x20) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
421
|
+
else if (byte & 0x08) heatStatus = sys.board.valueMaps.heatStatus.getValue('hpheat');
|
|
422
|
+
}
|
|
423
|
+
else {
|
|
424
|
+
//const heaterActive = (msg.extractPayloadByte(10) & 0x0C) === 12;
|
|
425
|
+
//const solarActive = (msg.extractPayloadByte(10) & 0x30) === 48;
|
|
426
|
+
const heaterActive = (msg.extractPayloadByte(10) & 0x08) === 0x08;
|
|
427
|
+
const solarActive = (msg.extractPayloadByte(10) & 0x20) === 0x20;
|
|
428
|
+
const cooling = solarActive && tbody.temp > tbody.setPoint;
|
|
429
|
+
if (heaterActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('heater');
|
|
430
|
+
if (cooling) heatStatus = sys.board.valueMaps.heatStatus.getValue('cooling');
|
|
431
|
+
else if (solarActive) heatStatus = sys.board.valueMaps.heatStatus.getValue('solar');
|
|
432
|
+
}
|
|
398
433
|
}
|
|
399
434
|
tbody.heatStatus = heatStatus;
|
|
400
435
|
sys.board.schedules.syncScheduleHeatSourceAndSetpoint(cbody, tbody);
|
|
@@ -482,7 +517,8 @@ export class EquipmentStateMessage {
|
|
|
482
517
|
// pool
|
|
483
518
|
let tbody: BodyTempState = state.temps.bodies.getItemById(1, true);
|
|
484
519
|
let cbody: Body = sys.bodies.getItemById(1);
|
|
485
|
-
|
|
520
|
+
// RKS: 02-26-22 - See communications doc for explanation of bits. This needs to support UltraTemp ETi heatpumps.
|
|
521
|
+
tbody.heatMode = cbody.heatMode = msg.extractPayloadByte(5) & 0x33;
|
|
486
522
|
tbody.setPoint = cbody.setPoint = msg.extractPayloadByte(3);
|
|
487
523
|
tbody.coolSetpoint = cbody.coolSetpoint = msg.extractPayloadByte(9);
|
|
488
524
|
if (tbody.isOn) tbody.temp = state.temps.waterSensor1;
|
|
@@ -490,8 +526,8 @@ export class EquipmentStateMessage {
|
|
|
490
526
|
if (cbody.isActive) {
|
|
491
527
|
// spa
|
|
492
528
|
tbody = state.temps.bodies.getItemById(2, true);
|
|
493
|
-
tbody.heatMode = cbody.heatMode =
|
|
494
|
-
|
|
529
|
+
tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(5) & 0xCC) >> 2;
|
|
530
|
+
//tbody.heatMode = cbody.heatMode = (msg.extractPayloadByte(5) & 12) >> 2;
|
|
495
531
|
tbody.setPoint = cbody.setPoint = msg.extractPayloadByte(4);
|
|
496
532
|
if (tbody.isOn) tbody.temp = state.temps.waterSensor2 = msg.extractPayloadByte(1);
|
|
497
533
|
}
|
|
@@ -689,7 +725,7 @@ export class EquipmentStateMessage {
|
|
|
689
725
|
case 144: // swim
|
|
690
726
|
sys.board.circuits.sequenceLightGroupAsync(grp.id, 'swim');
|
|
691
727
|
break;
|
|
692
|
-
case 160: //
|
|
728
|
+
case 160: // set
|
|
693
729
|
sys.board.circuits.sequenceLightGroupAsync(grp.id, 'set');
|
|
694
730
|
break;
|
|
695
731
|
case 190: // save
|
|
@@ -22,15 +22,50 @@ export class HeaterStateMessage {
|
|
|
22
22
|
public static process(msg: Inbound) {
|
|
23
23
|
if (msg.protocol === Protocol.Heater) {
|
|
24
24
|
switch (msg.action) {
|
|
25
|
-
case
|
|
25
|
+
case 112: // This is a message from a master controlling MasterTemp or UltraTemp ETi
|
|
26
|
+
break;
|
|
27
|
+
case 114: // This is a message from a master controlling UltraTemp
|
|
28
|
+
msg.isProcessed = true;
|
|
29
|
+
break;
|
|
30
|
+
case 113:
|
|
31
|
+
HeaterStateMessage.processHybridStatus(msg);
|
|
32
|
+
break;
|
|
33
|
+
case 116:
|
|
34
|
+
HeaterStateMessage.processMasterTempStatus(msg);
|
|
26
35
|
break;
|
|
27
36
|
case 115:
|
|
28
|
-
HeaterStateMessage.
|
|
37
|
+
HeaterStateMessage.processUltraTempStatus(msg);
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
public static processHeaterCommand(msg: Inbound) {
|
|
43
|
+
let heater: Heater = sys.heaters.getItemByAddress(msg.source);
|
|
44
|
+
// At this point there is no other configuration data for ET
|
|
45
|
+
if (sys.controllerType === ControllerType.EasyTouch) {
|
|
46
|
+
let htype = sys.board.valueMaps.heaterTypes.transform(heater.type);
|
|
47
|
+
switch (htype.name) {
|
|
48
|
+
case 'hybrid':
|
|
49
|
+
heater.economyTime = msg.extractPayloadByte(3);
|
|
50
|
+
heater.maxBoostTemp = msg.extractPayloadByte(4);
|
|
29
51
|
break;
|
|
30
52
|
}
|
|
31
53
|
}
|
|
32
54
|
}
|
|
33
|
-
public static
|
|
55
|
+
public static processHybridStatus(msg: Inbound) {
|
|
56
|
+
//[165, 0, 16, 112, 113, 10][1, 1, 0, 0, 0, 0, 0, 0, 0, 0][1, 162]
|
|
57
|
+
let heater: Heater = sys.heaters.getItemByAddress(msg.source);
|
|
58
|
+
let sheater = state.heaters.getItemById(heater.id);
|
|
59
|
+
sheater.isOn = msg.extractPayloadByte(0) > 0;
|
|
60
|
+
|
|
61
|
+
let byte = msg.extractPayloadByte(2);
|
|
62
|
+
//sheater.isOn = byte >= 1;
|
|
63
|
+
//sheater.isCooling = byte === 2;
|
|
64
|
+
sheater.commStatus = 0;
|
|
65
|
+
state.equipment.messages.removeItemByCode(`heater:${heater.id}:comms`);
|
|
66
|
+
msg.isProcessed = true;
|
|
67
|
+
}
|
|
68
|
+
public static processUltraTempStatus(msg: Inbound) {
|
|
34
69
|
// RKS: 07-03-21 - We only know byte 2 at this point for Ultratemp for the 115 message we are processing here. The
|
|
35
70
|
// byte description
|
|
36
71
|
// ------------------------------------------------
|
|
@@ -60,4 +95,23 @@ export class HeaterStateMessage {
|
|
|
60
95
|
state.equipment.messages.removeItemByCode(`heater:${heater.id}:comms`);
|
|
61
96
|
msg.isProcessed = true;
|
|
62
97
|
}
|
|
98
|
+
public static processMasterTempStatus(msg: Inbound) {
|
|
99
|
+
//[255, 0, 255][165, 0, 16, 112, 116, 23][67, 0, 0, 0, 0, 0, 0, 0, 68, 0, 0, 0, 10, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0][2, 66]
|
|
100
|
+
// Byte 1 is the indicator to which setpoint it is heating to.
|
|
101
|
+
// Byte 8 increments over time when the heater is on.
|
|
102
|
+
// Byte 13 looks like the mode the heater is in for instance it is in cooldown mode.
|
|
103
|
+
// 0 = Normal
|
|
104
|
+
// 2 = ??????
|
|
105
|
+
// 6 = Cooldown
|
|
106
|
+
// Byte 14 looks like the cooldown delay in minutes.
|
|
107
|
+
let heater: Heater = sys.heaters.getItemByAddress(msg.source);
|
|
108
|
+
let sheater = state.heaters.getItemById(heater.id);
|
|
109
|
+
let byte = msg.extractPayloadByte(1);
|
|
110
|
+
sheater.isOn = byte >= 1;
|
|
111
|
+
sheater.isCooling = false;
|
|
112
|
+
sheater.commStatus = 0;
|
|
113
|
+
state.equipment.messages.removeItemByCode(`heater:${heater.id}:comms`);
|
|
114
|
+
msg.isProcessed = true;
|
|
115
|
+
}
|
|
116
|
+
|
|
63
117
|
}
|
|
@@ -87,6 +87,18 @@ export class IntelliChemStateMessage {
|
|
|
87
87
|
}
|
|
88
88
|
private static processState(msg: Inbound) {
|
|
89
89
|
if (msg.source < 144 || msg.source > 158) return;
|
|
90
|
+
|
|
91
|
+
// Setup with CO2 dosing and IntelliChlor.
|
|
92
|
+
//[2, 245, 2, 162, 2, 248, 2, 88, 0, 0, [0-9]
|
|
93
|
+
// 0, 26, 0, 0, 0, 0, 0, 0, 0, 0, [10-19]
|
|
94
|
+
// 7, 0, 249, 1, 94, 0, 81, 0, 80, 57, [20-29]
|
|
95
|
+
// 0, 82, 0, 0, 162, 32, 80, 1, 0, 0, 0] [30-40]
|
|
96
|
+
// Setup with 2 Tanks
|
|
97
|
+
//[2, 238, 2, 208, 2, 248, 2, 188, 0, 0,
|
|
98
|
+
// 0, 2, 0, 0, 0, 29, 0, 4, 0, 63,
|
|
99
|
+
// 2, 2, 157, 0, 25, 0, 0, 0, 90, 20,
|
|
100
|
+
// 0, 83, 0, 0, 149, 0, 60, 1, 1, 0, 0]
|
|
101
|
+
|
|
90
102
|
// 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.
|
|
91
103
|
//[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]
|
|
92
104
|
// Bytes - Descrption
|
|
@@ -114,16 +126,31 @@ export class IntelliChemStateMessage {
|
|
|
114
126
|
// 31 : Temperature
|
|
115
127
|
// 32 : Alarms
|
|
116
128
|
// 33 : Warnings pH Lockout, Daily Limit Reached, Invalid Setup, Chlorinator Comm error
|
|
117
|
-
// 34 : Dosing Status (pH Monitoring, ORP Mixing)
|
|
129
|
+
// 34 : Dosing Status/Doser Type (pH Monitoring, ORP Mixing)
|
|
118
130
|
// 35 : Delays
|
|
119
131
|
// 36-37 : Firmware = 80,1 = 1.080
|
|
120
132
|
// 38 : Water Chemistry Warning (Corrosion...)
|
|
121
133
|
// 39 : Unknown
|
|
122
134
|
// 40 : Unknown
|
|
123
135
|
let address = msg.source;
|
|
136
|
+
|
|
124
137
|
// The address is king here. The id is not.
|
|
125
138
|
let chem = sys.chemControllers.getItemByAddress(address, true);
|
|
126
139
|
let schem = state.chemControllers.getItemById(chem.id, true);
|
|
140
|
+
|
|
141
|
+
// Get the doser types and set up our capabilities
|
|
142
|
+
chem.ph.doserType = (msg.extractPayloadByte(34) & 0x03);
|
|
143
|
+
chem.orp.doserType = (msg.extractPayloadByte(34) & 0x0C) >> 2;
|
|
144
|
+
schem.ph.enabled = chem.ph.enabled = chem.ph.doserType !== 0;
|
|
145
|
+
schem.ph.enabled = chem.orp.enabled = chem.orp.doserType !== 0;
|
|
146
|
+
if (chem.ph.doserType === 2) schem.ph.chemType = 'CO2';
|
|
147
|
+
else if (chem.ph.doserType === 0) schem.ph.chemType = 'none';
|
|
148
|
+
else if (chem.ph.doserType === 1) schem.ph.chemType = 'acid';
|
|
149
|
+
else if (chem.ph.doserType === 3) schem.ph.chemType = 'acid';
|
|
150
|
+
|
|
151
|
+
if (chem.orp.doserType === 0) schem.orp.chemType = 'none';
|
|
152
|
+
else schem.orp.chemType = 'orp';
|
|
153
|
+
|
|
127
154
|
schem.isActive = chem.isActive = true;
|
|
128
155
|
schem.status = 0;
|
|
129
156
|
schem.type = chem.type = sys.board.valueMaps.chemControllerTypes.getValue('intellichem');
|
|
@@ -156,8 +183,9 @@ export class IntelliChemStateMessage {
|
|
|
156
183
|
schem.orp.volumeDosed = (msg.extractPayloadByte(18) * 256) + msg.extractPayloadByte(19);
|
|
157
184
|
// 20 : pH tank level 1-7 = 6
|
|
158
185
|
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
|
|
159
|
-
// 21 : ORP tank level 1-7 = 6
|
|
186
|
+
// 21 : ORP tank level 1-7 = 6 if the tank levels report 0 then the chemical side is not enabled.
|
|
160
187
|
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
|
|
188
|
+
|
|
161
189
|
// 22 : LSI = 3 & 0x80 === 0x80 ? (256 - 3) / -100 : 3/100 = .03
|
|
162
190
|
let lsi = msg.extractPayloadByte(22);
|
|
163
191
|
schem.lsi = (lsi & 0x80) === 0x80 ? (256 - lsi) / -100 : lsi / 100;
|
|
@@ -180,10 +208,26 @@ export class IntelliChemStateMessage {
|
|
|
180
208
|
const alarms = schem.alarms;
|
|
181
209
|
alarms.flow = msg.extractPayloadByte(32) & 0x01;
|
|
182
210
|
if (alarms.flow === 0) schem.flowDetected = true;
|
|
183
|
-
|
|
184
|
-
alarms
|
|
185
|
-
|
|
186
|
-
alarms.
|
|
211
|
+
|
|
212
|
+
// The pH and ORP alarms are in a word stupid for IntelliChem. So we are
|
|
213
|
+
// going to override these.
|
|
214
|
+
//alarms.pH = msg.extractPayloadByte(32) & 0x06;
|
|
215
|
+
//alarms.orp = msg.extractPayloadByte(32) & 0x18;
|
|
216
|
+
if (chem.ph.tolerance.enabled && schem.flowDetected) {
|
|
217
|
+
if (schem.ph.level > chem.ph.tolerance.high) alarms.pH = 2;
|
|
218
|
+
else if (schem.ph.level < chem.ph.tolerance.low) alarms.pH = 4;
|
|
219
|
+
else alarms.pH = 0;
|
|
220
|
+
}
|
|
221
|
+
else alarms.pH = 0;
|
|
222
|
+
if (chem.orp.tolerance.enabled && schem.flowDetected) {
|
|
223
|
+
if (schem.orp.level > chem.orp.tolerance.high) alarms.orp = 8;
|
|
224
|
+
else if (schem.orp.level < chem.orp.tolerance.low) alarms.orp = 16;
|
|
225
|
+
else alarms.orp = 0;
|
|
226
|
+
}
|
|
227
|
+
else alarms.orp = 0;
|
|
228
|
+
// IntelliChem will still send a tank empty alarm even if there is no tank.
|
|
229
|
+
alarms.pHTank = (chem.ph.enabled && (chem.ph.doserType === 1 || chem.ph.doserType === 3)) ? msg.extractPayloadByte(32) & 0x20 : 0;
|
|
230
|
+
alarms.orpTank = (chem.orp.enabled && (chem.orp.doserType === 1 || chem.orp.doserType === 3)) ? msg.extractPayloadByte(32) & 0x40 : 0;
|
|
187
231
|
alarms.probeFault = msg.extractPayloadByte(32) & 0x80;
|
|
188
232
|
// 33 : Warnings -- pH Lockout, Daily Limit Reached, Invalid Setup, Chlorinator Comm error
|
|
189
233
|
const warnings = schem.warnings;
|
|
@@ -206,7 +250,12 @@ export class IntelliChemStateMessage {
|
|
|
206
250
|
// 36-37 : Firmware = 80,1 = 1.080
|
|
207
251
|
chem.firmware = `${msg.extractPayloadByte(37)}.${msg.extractPayloadByte(36).toString().padStart(3, '0')}`
|
|
208
252
|
// 38 : Water Chemistry Warning
|
|
209
|
-
|
|
253
|
+
// The LSI handling is also stupid with IntelliChem so we are going to have our way with it.
|
|
254
|
+
// schem.warnings.waterChemistry = msg.extractPayloadByte(38);
|
|
255
|
+
schem.calculateSaturationIndex();
|
|
256
|
+
if (schem.saturationIndex > chem.lsiRange.high) schem.warnings.waterChemistry = 2;
|
|
257
|
+
else if (schem.saturationIndex < chem.lsiRange.low) schem.warnings.waterChemistry = 1;
|
|
258
|
+
else schem.warnings.waterChemistry = 0;
|
|
210
259
|
if (typeof chem.body === 'undefined') chem.body = schem.body = 0;
|
|
211
260
|
if (state.equipment.controllerType === 'nixie') {
|
|
212
261
|
if (chem.ph.probe.feedBodyTemp) {
|
|
@@ -228,7 +277,6 @@ export class IntelliChemStateMessage {
|
|
|
228
277
|
}
|
|
229
278
|
schem.ph.pump.isDosing = schem.ph.dosingStatus === 0 && chem.ph.enabled;
|
|
230
279
|
schem.orp.pump.isDosing = schem.orp.dosingStatus === 0 && chem.orp.enabled;
|
|
231
|
-
schem.calculateSaturationIndex();
|
|
232
280
|
schem.lastComm = new Date().getTime();
|
|
233
281
|
// If we are changing states lets set that up.
|
|
234
282
|
if (schem.ph.dosingStatus === 0 && phPrev.status !== 0) {
|
|
@@ -55,7 +55,29 @@ export class PumpStateMessage {
|
|
|
55
55
|
state.emitEquipmentChanges();
|
|
56
56
|
// if (msg.action !== 7) this.processDirectPumpMessages(msg);
|
|
57
57
|
}
|
|
58
|
-
|
|
58
|
+
public static processHayward(msg: Inbound) {
|
|
59
|
+
switch (msg.action) {
|
|
60
|
+
case 12:
|
|
61
|
+
PumpStateMessage.processHaywardStar(msg);
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
public static processHaywardStar(msg: Inbound) {
|
|
66
|
+
// src act dest
|
|
67
|
+
//[0x10, 0x02, 0x00, 0x0C, 0x00][0x00, 0x62, 0x17, 0x81][0x01, 0x18, 0x10, 0x03]
|
|
68
|
+
//[0x10, 0x02, 0x00, 0x0C, 0x00][0x00, 0x2D, 0x02, 0x36][0x00, 0x83, 0x10, 0x03] -- Response from pump
|
|
69
|
+
let ptype = sys.board.valueMaps.pumpTypes.transformByName('hwvs');
|
|
70
|
+
let pump = sys.pumps.find(elem => elem.address === msg.source + 96 && elem.type === 6);
|
|
71
|
+
if (typeof pump !== 'undefined') {
|
|
72
|
+
let pstate = state.pumps.getItemById(pump.id, true);
|
|
73
|
+
// 3450 * .5
|
|
74
|
+
pstate.rpm = Math.round(ptype.maxSpeed * (msg.extractPayloadByte(1) / 100));
|
|
75
|
+
// This is really goofy as the watts are actually the hex string from the two bytes.
|
|
76
|
+
pstate.watts = parseInt(msg.extractPayloadByte(2).toString(16) + msg.extractPayloadByte(3).toString(16), 10);
|
|
77
|
+
pstate.isActive = true;
|
|
78
|
+
state.emitEquipmentChanges();
|
|
79
|
+
}
|
|
80
|
+
}
|
|
59
81
|
public static processDirectPumpMessages(msg: Inbound) {
|
|
60
82
|
|
|
61
83
|
if (msg.payload.length === 2)
|
|
@@ -118,11 +118,11 @@ export class NixieControlPanel implements INixieControlPanel {
|
|
|
118
118
|
}
|
|
119
119
|
public async closeAsync() {
|
|
120
120
|
// Close all the associated equipment.
|
|
121
|
-
await this.pumps.closeAsync();
|
|
122
121
|
await this.chemControllers.closeAsync();
|
|
123
122
|
await this.chlorinators.closeAsync();
|
|
124
123
|
await this.heaters.closeAsync();
|
|
125
124
|
await this.circuits.closeAsync();
|
|
125
|
+
await this.pumps.closeAsync();
|
|
126
126
|
await this.filters.closeAsync();
|
|
127
127
|
await this.bodies.closeAsync();
|
|
128
128
|
await this.valves.closeAsync();
|
|
@@ -64,6 +64,9 @@ export class NixieBody extends NixieEquipment {
|
|
|
64
64
|
super(ncp);
|
|
65
65
|
this.body = body;
|
|
66
66
|
this.pollEquipmentAsync();
|
|
67
|
+
let bs = state.temps.bodies.getItemById(body.id);
|
|
68
|
+
bs.heaterCooldownDelay = false;
|
|
69
|
+
bs.heatStatus = 0;
|
|
67
70
|
}
|
|
68
71
|
public get id(): number { return typeof this.body !== 'undefined' ? this.body.id : -1; }
|
|
69
72
|
public async setBodyAsync(data: any) {
|