nodejs-poolcontroller 7.6.1 → 8.0.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 (102) hide show
  1. package/.eslintrc.json +36 -45
  2. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -0
  3. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -0
  4. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -0
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  6. package/CONTRIBUTING.md +74 -74
  7. package/Changelog +242 -215
  8. package/Dockerfile +17 -17
  9. package/Gruntfile.js +40 -40
  10. package/LICENSE +661 -661
  11. package/README.md +195 -191
  12. package/anslq25/MessagesMock.ts +218 -0
  13. package/anslq25/boards/MockBoardFactory.ts +50 -0
  14. package/anslq25/boards/MockEasyTouchBoard.ts +696 -0
  15. package/anslq25/boards/MockSystemBoard.ts +217 -0
  16. package/anslq25/chemistry/MockChlorinator.ts +75 -0
  17. package/anslq25/pumps/MockPump.ts +84 -0
  18. package/app.ts +10 -14
  19. package/config/Config.ts +26 -8
  20. package/config/VersionCheck.ts +8 -4
  21. package/controller/Constants.ts +59 -25
  22. package/controller/Equipment.ts +2667 -2459
  23. package/controller/Errors.ts +181 -180
  24. package/controller/Lockouts.ts +534 -436
  25. package/controller/State.ts +596 -77
  26. package/controller/boards/AquaLinkBoard.ts +1003 -0
  27. package/controller/boards/BoardFactory.ts +53 -45
  28. package/controller/boards/EasyTouchBoard.ts +3079 -2653
  29. package/controller/boards/IntelliCenterBoard.ts +3821 -4230
  30. package/controller/boards/IntelliComBoard.ts +69 -63
  31. package/controller/boards/IntelliTouchBoard.ts +384 -241
  32. package/controller/boards/NixieBoard.ts +1871 -1675
  33. package/controller/boards/SunTouchBoard.ts +393 -0
  34. package/controller/boards/SystemBoard.ts +5244 -4697
  35. package/controller/comms/Comms.ts +905 -541
  36. package/controller/comms/ScreenLogic.ts +1663 -0
  37. package/controller/comms/messages/Messages.ts +382 -54
  38. package/controller/comms/messages/config/ChlorinatorMessage.ts +8 -4
  39. package/controller/comms/messages/config/CircuitGroupMessage.ts +5 -2
  40. package/controller/comms/messages/config/CircuitMessage.ts +82 -13
  41. package/controller/comms/messages/config/ConfigMessage.ts +3 -1
  42. package/controller/comms/messages/config/CoverMessage.ts +2 -1
  43. package/controller/comms/messages/config/CustomNameMessage.ts +31 -30
  44. package/controller/comms/messages/config/EquipmentMessage.ts +5 -1
  45. package/controller/comms/messages/config/ExternalMessage.ts +33 -3
  46. package/controller/comms/messages/config/FeatureMessage.ts +2 -1
  47. package/controller/comms/messages/config/GeneralMessage.ts +2 -1
  48. package/controller/comms/messages/config/HeaterMessage.ts +145 -11
  49. package/controller/comms/messages/config/IntellichemMessage.ts +2 -1
  50. package/controller/comms/messages/config/OptionsMessage.ts +16 -27
  51. package/controller/comms/messages/config/PumpMessage.ts +62 -47
  52. package/controller/comms/messages/config/RemoteMessage.ts +80 -13
  53. package/controller/comms/messages/config/ScheduleMessage.ts +390 -347
  54. package/controller/comms/messages/config/SecurityMessage.ts +2 -1
  55. package/controller/comms/messages/config/ValveMessage.ts +44 -27
  56. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +44 -91
  57. package/controller/comms/messages/status/EquipmentStateMessage.ts +139 -30
  58. package/controller/comms/messages/status/HeaterStateMessage.ts +135 -86
  59. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -445
  60. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -35
  61. package/controller/comms/messages/status/PumpStateMessage.ts +92 -2
  62. package/controller/comms/messages/status/VersionMessage.ts +2 -1
  63. package/controller/nixie/Nixie.ts +173 -162
  64. package/controller/nixie/NixieEquipment.ts +104 -103
  65. package/controller/nixie/bodies/Body.ts +120 -120
  66. package/controller/nixie/bodies/Filter.ts +135 -135
  67. package/controller/nixie/chemistry/ChemController.ts +2682 -2498
  68. package/controller/nixie/chemistry/ChemDoser.ts +806 -0
  69. package/controller/nixie/chemistry/Chlorinator.ts +367 -314
  70. package/controller/nixie/circuits/Circuit.ts +402 -248
  71. package/controller/nixie/heaters/Heater.ts +815 -649
  72. package/controller/nixie/pumps/Pump.ts +934 -661
  73. package/controller/nixie/schedules/Schedule.ts +319 -257
  74. package/controller/nixie/valves/Valve.ts +170 -170
  75. package/defaultConfig.json +346 -286
  76. package/logger/DataLogger.ts +448 -448
  77. package/logger/Logger.ts +38 -9
  78. package/package.json +60 -56
  79. package/tsconfig.json +25 -25
  80. package/web/Server.ts +275 -117
  81. package/web/bindings/aqualinkD.json +560 -0
  82. package/web/bindings/homeassistant.json +437 -0
  83. package/web/bindings/influxDB.json +1066 -1021
  84. package/web/bindings/mqtt.json +721 -654
  85. package/web/bindings/mqttAlt.json +746 -684
  86. package/web/bindings/rulesManager.json +54 -54
  87. package/web/bindings/smartThings-Hubitat.json +31 -31
  88. package/web/bindings/valveRelays.json +20 -20
  89. package/web/bindings/vera.json +25 -25
  90. package/web/interfaces/baseInterface.ts +188 -136
  91. package/web/interfaces/httpInterface.ts +148 -124
  92. package/web/interfaces/influxInterface.ts +283 -245
  93. package/web/interfaces/mqttInterface.ts +695 -475
  94. package/web/interfaces/ruleInterface.ts +87 -0
  95. package/web/services/config/Config.ts +177 -49
  96. package/web/services/config/ConfigSocket.ts +2 -1
  97. package/web/services/state/State.ts +154 -3
  98. package/web/services/state/StateSocket.ts +69 -18
  99. package/web/services/utilities/Utilities.ts +232 -42
  100. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -52
  101. package/config copy.json +0 -300
  102. package/issue_template.md +0 -52
@@ -0,0 +1,1003 @@
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 * as extend from 'extend';
19
+ import { logger } from '../../logger/Logger';
20
+ import { Message, Outbound, Protocol, Response } from '../comms/messages/Messages';
21
+ import { BodyCommands, byteValueMap, ChemControllerCommands, ChlorinatorCommands, CircuitCommands, ConfigQueue, ConfigRequest, EquipmentIdRange, FeatureCommands, HeaterCommands, PumpCommands, ScheduleCommands, SystemBoard, SystemCommands } from './SystemBoard';
22
+ import { BodyTempState, ChlorinatorState, ICircuitGroupState, ICircuitState, LightGroupState, state } from '../State';
23
+ import { Body, ChemController, ConfigVersion, EggTimer, Feature, Heater, ICircuit, LightGroup, LightGroupCircuit, PoolSystem, Pump, Schedule, sys } from '../Equipment';
24
+ import { EquipmentTimeoutError, InvalidEquipmentDataError, InvalidEquipmentIdError, InvalidOperationError } from '../Errors';
25
+ import { conn } from '../comms/Comms';
26
+ import { ncp } from "../nixie/Nixie";
27
+ import { utils } from '../Constants';
28
+
29
+ export class AquaLinkBoard extends SystemBoard {
30
+ constructor(system: PoolSystem) {
31
+ super(system);
32
+ this.equipmentIds.features.start = 41;
33
+ this.equipmentIds.features.end = 50;
34
+ this.valueMaps.expansionBoards = new byteValueMap([
35
+ [0, { name: 'IT5', part: 'i5+3', desc: 'IntelliTouch i5+3', circuits: 6, shared: true }],
36
+ [1, { name: 'IT7', part: 'i7+3', desc: 'IntelliTouch i7+3', circuits: 8, shared: true }],
37
+ [2, { name: 'IT9', part: 'i9+3', desc: 'IntelliTouch i9+3', circuits: 10, shared: true }],
38
+ [3, { name: 'IT5S', part: 'i5+3S', desc: 'IntelliTouch i5+3S', circuits: 5, shared: false, bodies: 1, intakeReturnValves: false }],
39
+ [4, { name: 'IT9S', part: 'i9+3S', desc: 'IntelliTouch i9+3S', circuits: 9, shared: false, bodies: 1, intakeReturnValves: false }],
40
+ [5, { name: 'IT10D', part: 'i10D', desc: 'IntelliTouch i10D', circuits: 10, shared: false, dual: true }],
41
+ [32, { name: 'IT5X', part: 'i5X', desc: 'IntelliTouch i5X', circuits: 5 }],
42
+ [33, { name: 'IT10X', part: 'i10X', desc: 'IntelliTouch i10X', circuits: 10 }]
43
+ ]);
44
+ }
45
+ public initExpansionModules(byte1: number, byte2: number) {
46
+ console.log(`Jandy AquaLink System Detected!`);
47
+ state.emitControllerChange();
48
+ }
49
+ public bodies: AquaLinkBodyCommands = new AquaLinkBodyCommands(this);
50
+ public system: AquaLinkSystemCommands = new AquaLinkSystemCommands(this);
51
+ public circuits: AquaLinkCircuitCommands = new AquaLinkCircuitCommands(this);
52
+ public features: AquaLinkFeatureCommands = new AquaLinkFeatureCommands(this);
53
+ public chlorinator: AquaLinkChlorinatorCommands = new AquaLinkChlorinatorCommands(this);
54
+ public pumps: AquaLinkPumpCommands = new AquaLinkPumpCommands(this);
55
+ public schedules: AquaLinkScheduleCommands = new AquaLinkScheduleCommands(this);
56
+ public heaters: AquaLinkHeaterCommands = new AquaLinkHeaterCommands(this);
57
+ protected _configQueue: AquaLinkConfigQueue = new AquaLinkConfigQueue();
58
+
59
+ }
60
+ class AquaLinkConfigQueue extends ConfigQueue {
61
+ //protected _configQueueTimer: NodeJS.Timeout;
62
+ //public clearTimer(): void { clearTimeout(this._configQueueTimer); }
63
+ protected queueRange(cat: number, start: number, end: number) {}
64
+ protected queueItems(cat: number, items: number[] = [0]) { }
65
+ public queueChanges() {
66
+ this.reset();
67
+ logger.info(`Requesting ${sys.controllerType} configuration`);
68
+ if (this.remainingItems > 0) {
69
+ var self = this;
70
+ setTimeout(() => { self.processNext(); }, 50);
71
+ } else {
72
+ state.status = 1;
73
+ }
74
+ state.emitControllerChange();
75
+ }
76
+ // TODO: RKS -- Investigate why this is needed. Me thinks that there really is no difference once the whole thing is optimized. With a little
77
+ // bit of work I'll bet we can eliminate these extension objects altogether.
78
+ public processNext(msg?: Outbound) {
79
+ if (this.closed) return;
80
+ if (typeof msg !== "undefined" && msg !== null)
81
+ if (!msg.failed) {
82
+ // Remove all references to future items. We got it so we don't need it again.
83
+ this.removeItem(msg.action, msg.payload[0]);
84
+ if (this.curr && this.curr.isComplete) {
85
+ if (!this.curr.failed) {
86
+ // Call the identified callback. This may add additional items.
87
+ if (typeof this.curr.oncomplete === 'function') {
88
+ this.curr.oncomplete(this.curr);
89
+ this.curr.oncomplete = undefined;
90
+ }
91
+ }
92
+ }
93
+
94
+ } else this.curr.failed = true;
95
+ if (!this.curr && this.queue.length > 0) this.curr = this.queue.shift();
96
+ if (!this.curr) {
97
+ // There never was anything for us to do. We will likely never get here.
98
+ state.status = 1;
99
+ state.emitControllerChange();
100
+ return;
101
+ } else {
102
+ state.status = sys.board.valueMaps.controllerStatus.transform(2, this.percent);
103
+ }
104
+ // Shift to the next config queue item.
105
+ logger.verbose(`Config Queue Completed... ${this.percent}% (${this.remainingItems} remaining)`);
106
+ while ( this.queue.length > 0 && this.curr.isComplete) { this.curr = this.queue.shift() || null; }
107
+ let itm = 0;
108
+ const self = this;
109
+ if (this.curr && !this.curr.isComplete) {
110
+ itm = this.curr.items.shift();
111
+ } else {
112
+ // Now that we are done check the configuration a final time. If we have anything outstanding
113
+ // it will get picked up.
114
+ state.status = 1;
115
+ this.curr = null;
116
+ sys.configVersion.lastUpdated = new Date();
117
+ // set a timer for 20 mins; if we don't get the config request it again. This most likely happens if there is no other indoor/outdoor remotes or ScreenLogic.
118
+ // this._configQueueTimer = setTimeout(()=>{sys.board.checkConfiguration();}, 20 * 60 * 1000);
119
+ logger.info(`AquaLink system config complete.`);
120
+ state.cleanupState();
121
+ ncp.initAsync(sys);
122
+ }
123
+ // Notify all the clients of our processing status.
124
+ state.emitControllerChange();
125
+ }
126
+ }
127
+ class AquaLinkScheduleCommands extends ScheduleCommands {
128
+ public async setScheduleAsync(data: any): Promise<Schedule> {
129
+ let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
130
+ if (id <= 0) id = sys.schedules.getNextEquipmentId(new EquipmentIdRange(1, sys.equipment.maxSchedules));
131
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid schedule id: ${data.id}`, data.id, 'Schedule'));
132
+ let sched = sys.schedules.getItemById(id, id > 0);
133
+ let ssched = state.schedules.getItemById(id, id > 0);
134
+ let schedType = typeof data.scheduleType !== 'undefined' ? data.scheduleType : sched.scheduleType;
135
+ if (typeof schedType === 'undefined') schedType = sys.board.valueMaps.scheduleTypes.getValue('repeat'); // Repeats
136
+
137
+ let startTimeType = typeof data.startTimeType !== 'undefined' ? data.startTimeType : sched.startTimeType;
138
+ let endTimeType = typeof data.endTimeType !== 'undefined' ? data.endTimeType : sched.endTimeType;
139
+ // let startDate = typeof data.startDate !== 'undefined' ? data.startDate : sched.startDate;
140
+ // if (typeof startDate.getMonth !== 'function') startDate = new Date(startDate);
141
+ let heatSource = typeof data.heatSource !== 'undefined' && data.heatSource !== null ? data.heatSource : sched.heatSource || 32;
142
+ let heatSetpoint = typeof data.heatSetpoint !== 'undefined' ? data.heatSetpoint : sched.heatSetpoint;
143
+ let circuit = typeof data.circuit !== 'undefined' ? data.circuit : sched.circuit;
144
+ let startTime = typeof data.startTime !== 'undefined' ? data.startTime : sched.startTime;
145
+ let endTime = typeof data.endTime !== 'undefined' ? data.endTime : sched.endTime;
146
+ let schedDays = sys.board.schedules.transformDays(typeof data.scheduleDays !== 'undefined' ? data.scheduleDays : sched.scheduleDays || 255); // default to all days
147
+ let changeHeatSetpoint = typeof (data.changeHeatSetpoint !== 'undefined') ? utils.makeBool(data.changeHeatSetpoint) : sched.changeHeatSetpoint;
148
+ let display = typeof data.display !== 'undefined' ? data.display : sched.display || 0;
149
+
150
+ // Ensure all the defaults.
151
+ // if (isNaN(startDate.getTime())) startDate = new Date();
152
+ if (typeof startTime === 'undefined') startTime = 480; // 8am
153
+ if (typeof endTime === 'undefined') endTime = 1020; // 5pm
154
+ if (typeof startTimeType === 'undefined') startTimeType = 0; // Manual
155
+ if (typeof endTimeType === 'undefined') endTimeType = 0; // Manual
156
+ if (typeof circuit === 'undefined') circuit = 6; // pool
157
+ if (typeof heatSource !== 'undefined' && typeof heatSetpoint === 'undefined') heatSetpoint = state.temps.units === sys.board.valueMaps.tempUnits.getValue('C') ? 26 : 80;
158
+ if (typeof changeHeatSetpoint === 'undefined') changeHeatSetpoint = false;
159
+
160
+ // At this point we should have all the data. Validate it.
161
+ if (!sys.board.valueMaps.scheduleTypes.valExists(schedType)) { sys.schedules.removeItemById(id); state.schedules.removeItemById(id); return Promise.reject(new InvalidEquipmentDataError(`Invalid schedule type; ${schedType}`, 'Schedule', schedType)); }
162
+ if (!sys.board.valueMaps.scheduleTimeTypes.valExists(startTimeType)) { sys.schedules.removeItemById(id); state.schedules.removeItemById(id); return Promise.reject(new InvalidEquipmentDataError(`Invalid start time type; ${startTimeType}`, 'Schedule', startTimeType)); }
163
+ if (!sys.board.valueMaps.scheduleTimeTypes.valExists(endTimeType)) { sys.schedules.removeItemById(id); state.schedules.removeItemById(id); return Promise.reject(new InvalidEquipmentDataError(`Invalid end time type; ${endTimeType}`, 'Schedule', endTimeType)); }
164
+ if (!sys.board.valueMaps.heatSources.valExists(heatSource)) { sys.schedules.removeItemById(id); state.schedules.removeItemById(id); return Promise.reject(new InvalidEquipmentDataError(`Invalid heat source: ${heatSource}`, 'Schedule', heatSource)); }
165
+ if (heatSetpoint < 0 || heatSetpoint > 104) { sys.schedules.removeItemById(id); state.schedules.removeItemById(id); return Promise.reject(new InvalidEquipmentDataError(`Invalid heat setpoint: ${heatSetpoint}`, 'Schedule', heatSetpoint)); }
166
+ if (sys.board.circuits.getCircuitReferences(true, true, false, true).find(elem => elem.id === circuit) === undefined) { sys.schedules.removeItemById(id); state.schedules.removeItemById(id); return Promise.reject(new InvalidEquipmentDataError(`Invalid circuit reference: ${circuit}`, 'Schedule', circuit)); }
167
+ if (typeof heatSource !== 'undefined' && !sys.circuits.getItemById(circuit).hasHeatSource) heatSource = undefined;
168
+
169
+ // If we make it here we can make it anywhere.
170
+ // let runOnce = (schedDays || (schedType !== 0 ? 0 : 0x80));
171
+ if (schedType === sys.board.valueMaps.scheduleTypes.getValue('runonce')) {
172
+ // make sure only 1 day is selected
173
+ let scheduleDays = sys.board.valueMaps.scheduleDays.transform(schedDays);
174
+ let s2 = sys.board.valueMaps.scheduleDays.toArray();
175
+ if (scheduleDays.days.length > 1) {
176
+ schedDays = scheduleDays.days[scheduleDays.days.length - 1].val; // get the earliest day in the week
177
+ }
178
+ else if (scheduleDays.days.length === 0) {
179
+ for (let i = 0; i < s2.length; i++) {
180
+ if (s2[i].days[0].name === 'sun') schedDays = s2[i].val;
181
+ }
182
+ }
183
+ // update end time incase egg timer changed
184
+ const eggTimer = sys.circuits.getInterfaceById(circuit).eggTimer || 720;
185
+ endTime = (startTime + eggTimer) % 1440; // remove days if we go past midnight
186
+ }
187
+
188
+
189
+ // If we have sunrise/sunset then adjust for the values; if heliotrope isn't set just ignore
190
+ if (state.heliotrope.isCalculated) {
191
+ const sunrise = state.heliotrope.sunrise.getHours() * 60 + state.heliotrope.sunrise.getMinutes();
192
+ const sunset = state.heliotrope.sunset.getHours() * 60 + state.heliotrope.sunset.getMinutes();
193
+ if (startTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunrise')) startTime = sunrise;
194
+ else if (startTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunset')) startTime = sunset;
195
+ if (endTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunrise')) endTime = sunrise;
196
+ else if (endTimeType === sys.board.valueMaps.scheduleTimeTypes.getValue('sunset')) endTime = sunset;
197
+ }
198
+ return new Promise<Schedule>((resolve, reject) => {
199
+ resolve(sys.schedules.getItemById(id));
200
+ });
201
+ }
202
+ public async deleteScheduleAsync(data: any): Promise<Schedule> {
203
+ let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
204
+ if (isNaN(id) || id < 0) return Promise.reject(new InvalidEquipmentIdError(`Invalid schedule id: ${data.id}`, data.id, 'Schedule'));
205
+ let sched = sys.schedules.getItemById(id);
206
+ let ssched = state.schedules.getItemById(id);
207
+ return new Promise<Schedule>((resolve, reject) => {
208
+ resolve(sched);
209
+ });
210
+ }
211
+ public async setEggTimerAsync(data?: any): Promise<EggTimer> {
212
+ let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
213
+ if (id <= 0) id = sys.schedules.getNextEquipmentId(new EquipmentIdRange(1, sys.equipment.maxSchedules));
214
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid schedule/eggTimer id: ${data.id} or all schedule/eggTimer ids filled (${sys.eggTimers.length + sys.schedules.length} used out of ${sys.equipment.maxSchedules})`, data.id, 'Schedule'));
215
+ let circuit = sys.circuits.getInterfaceById(data.circuit);
216
+ if (typeof circuit === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit id: ${data.circuit} for schedule id ${data.id}`, data.id, 'Schedule'));
217
+ return new Promise<EggTimer>((resolve, reject) => { resolve(sys.eggTimers.getItemById(id)); });
218
+ }
219
+ public async deleteEggTimerAsync(data: any): Promise<EggTimer> {
220
+ return new Promise<EggTimer>((resolve, reject) => {
221
+ let id = typeof data.id === 'undefined' ? -1 : parseInt(data.id, 10);
222
+ if (isNaN(id) || id < 0) reject(new InvalidEquipmentIdError(`Invalid eggTimer id: ${data.id}`, data.id, 'Schedule'));
223
+ let eggTimer = sys.eggTimers.getItemById(id);
224
+ resolve(eggTimer);
225
+ });
226
+ }
227
+ }
228
+ class AquaLinkSystemCommands extends SystemCommands {
229
+ public async cancelDelay() {
230
+ return new Promise<void>((resolve, reject) => {
231
+ resolve(state.data.delay);
232
+ });
233
+ }
234
+ public async setDateTimeAsync(obj: any): Promise<any> {
235
+ let dayOfWeek = function (): number {
236
+ // for IntelliTouch set date/time
237
+ if (state.time.toDate().getUTCDay() === 0)
238
+ return 0;
239
+ else
240
+ return Math.pow(2, state.time.toDate().getUTCDay() - 1);
241
+ }
242
+ return new Promise<any>((resolve, reject) => {
243
+ resolve({
244
+ time: state.time.format(),
245
+ adjustDST: sys.general.options.adjustDST,
246
+ clockSource: sys.general.options.clockSource
247
+ });
248
+ });
249
+ }
250
+ }
251
+ class AquaLinkBodyCommands extends BodyCommands {
252
+ public async setBodyAsync(obj: any): Promise<Body> {
253
+ try {
254
+ return new Promise<Body>((resolve, reject) => {
255
+ let manualHeat = sys.general.options.manualHeat;
256
+ if (typeof obj.manualHeat !== 'undefined') manualHeat = utils.makeBool(obj.manualHeat);
257
+ let body = sys.bodies.getItemById(obj.id, false);
258
+ let intellichemInstalled = sys.chemControllers.getItemByAddress(144, false).isActive;
259
+ resolve(body);
260
+ });
261
+
262
+ }
263
+ catch (err) { return Promise.reject(err); }
264
+ }
265
+ public async setHeatModeAsync(body: Body, mode: number): Promise<BodyTempState> {
266
+ return new Promise<BodyTempState>((resolve, reject) => {
267
+ const body1 = sys.bodies.getItemById(1);
268
+ const body2 = sys.bodies.getItemById(2);
269
+ const temp1 = body1.setPoint || 100;
270
+ const temp2 = body2.setPoint || 100;
271
+ let cool = body1.coolSetpoint || 0;
272
+ let mode1 = body1.heatMode;
273
+ let mode2 = body2.heatMode;
274
+ body.id === 1 ? mode1 = mode : mode2 = mode;
275
+ let bstate = state.temps.bodies.getItemById(body.id);
276
+ resolve(bstate);
277
+ });
278
+ }
279
+ public async setSetpoints(body: Body, obj: any): Promise<BodyTempState> {
280
+ return new Promise<BodyTempState>((resolve, reject) => {
281
+ let setPoint = typeof obj.setPoint !== 'undefined' ? parseInt(obj.setPoint, 10) : parseInt(obj.heatSetpoint, 10);
282
+ let coolSetPoint = typeof obj.coolSetPoint !== 'undefined' ? parseInt(obj.coolSetPoint, 10) : 0;
283
+ if (isNaN(setPoint)) return Promise.reject(new InvalidEquipmentDataError(`Invalid ${body.name} setpoint ${obj.setPoint || obj.heatSetpoint}`, 'body', obj));
284
+ const tempUnits = state.temps.units;
285
+ switch (tempUnits) {
286
+ case 0: // fahrenheit
287
+ {
288
+ if (setPoint < 40 || setPoint > 104) {
289
+ logger.warn(`Setpoint of ${setPoint} is outside acceptable range.`);
290
+ }
291
+ if (coolSetPoint < 40 || coolSetPoint > 104) {
292
+ logger.warn(`Cool Setpoint of ${setPoint} is outside acceptable range.`);
293
+ return;
294
+ }
295
+ break;
296
+ }
297
+ case 1: // celsius
298
+ {
299
+ if (setPoint < 4 || setPoint > 40) {
300
+ logger.warn(
301
+ `Setpoint of ${setPoint} is outside of acceptable range.`
302
+ );
303
+ return;
304
+ }
305
+ if (coolSetPoint < 4 || coolSetPoint > 40) {
306
+ logger.warn(`Cool SetPoint of ${coolSetPoint} is outside of acceptable range.`
307
+ );
308
+ return;
309
+ }
310
+ break;
311
+ }
312
+ }
313
+ const body1 = sys.bodies.getItemById(1);
314
+ const body2 = sys.bodies.getItemById(2);
315
+ let temp1 = body1.setPoint || tempUnits === 0 ? 40 : 4;
316
+ let temp2 = body2.setPoint || tempUnits === 0 ? 40 : 4;
317
+ let cool = coolSetPoint || body1.setPoint + 1;
318
+ body.id === 1 ? temp1 = setPoint : temp2 = setPoint;
319
+ const mode1 = body1.heatMode;
320
+ const mode2 = body2.heatMode;
321
+ let bstate = state.temps.bodies.getItemById(body.id);
322
+ resolve(bstate);
323
+ });
324
+ }
325
+ public async setHeatSetpointAsync(body: Body, setPoint: number): Promise<BodyTempState> {
326
+ return new Promise<BodyTempState>((resolve, reject) => {
327
+ const tempUnits = state.temps.units;
328
+ switch (tempUnits) {
329
+ case 0: // fahrenheit
330
+ if (setPoint < 40 || setPoint > 104) {
331
+ logger.warn(`Setpoint of ${setPoint} is outside acceptable range.`);
332
+ return;
333
+ }
334
+ break;
335
+ case 1: // celsius
336
+ if (setPoint < 4 || setPoint > 40) {
337
+ logger.warn(
338
+ `Setpoint of ${setPoint} is outside of acceptable range.`
339
+ );
340
+ return;
341
+ }
342
+ break;
343
+ }
344
+ const body1 = sys.bodies.getItemById(1);
345
+ const body2 = sys.bodies.getItemById(2);
346
+ let temp1 = body1.setPoint || 100;
347
+ let temp2 = body2.setPoint || 100;
348
+ body.id === 1 ? temp1 = setPoint : temp2 = setPoint;
349
+ const mode1 = body1.heatMode || 0;
350
+ const mode2 = body2.heatMode || 0;
351
+ let cool = body1.coolSetpoint || (body1.setPoint + 1);
352
+ let bstate = state.temps.bodies.getItemById(body.id);
353
+ resolve(bstate);
354
+ });
355
+ }
356
+ public async setCoolSetpointAsync(body: Body, setPoint: number): Promise<BodyTempState> {
357
+ return new Promise<BodyTempState>((resolve, reject) => {
358
+ // [16,34,136,4],[POOL HEAT Temp,SPA HEAT Temp,Heat Mode,Cool,2,56]
359
+ // 165,33,16,34,136,4,89,99,7,0,2,71 Request
360
+ // 165,33,34,16,1,1,136,1,130 Controller Response
361
+ const tempUnits = state.temps.units;
362
+ switch (tempUnits) {
363
+ case 0: // fahrenheit
364
+ if (setPoint < 40 || setPoint > 104) {
365
+ logger.warn(`Setpoint of ${setPoint} is outside acceptable range.`);
366
+ return;
367
+ }
368
+ break;
369
+ case 1: // celsius
370
+ if (setPoint < 4 || setPoint > 40) {
371
+ logger.warn(
372
+ `Setpoint of ${setPoint} is outside of acceptable range.`
373
+ );
374
+ return;
375
+ }
376
+ break;
377
+ }
378
+ const body1 = sys.bodies.getItemById(1);
379
+ const body2 = sys.bodies.getItemById(2);
380
+ let temp1 = body1.setPoint || 100;
381
+ let temp2 = body2.setPoint || 100;
382
+ const mode1 = body1.heatMode || 0;
383
+ const mode2 = body2.heatMode || 0;
384
+ const out = Outbound.create({
385
+ dest: 16,
386
+ action: 136,
387
+ payload: [temp1, temp2, mode2 << 2 | mode1, setPoint],
388
+ retries: 3,
389
+ response: true,
390
+ onComplete: (err, msg) => {
391
+ if (err) reject(err);
392
+ let bstate = state.temps.bodies.getItemById(body.id);
393
+ body.coolSetpoint = bstate.coolSetpoint = setPoint;
394
+ state.temps.emitEquipmentChange();
395
+ resolve(bstate);
396
+ }
397
+
398
+ });
399
+ //conn.queueSendMessage(out);
400
+ });
401
+ }
402
+ }
403
+ class AquaLinkCircuitCommands extends CircuitCommands {
404
+ public async setCircuitAsync(data: any): Promise<ICircuit> {
405
+ try {
406
+ let id = parseInt(data.id, 10);
407
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Circuit Id is invalid', data.id, 'Feature'));
408
+ if (id >= 255 || data.master === 1) return super.setCircuitAsync(data);
409
+ let circuit = sys.circuits.getInterfaceById(id);
410
+ // Alright check to see if we are adding a nixie circuit.
411
+ if (id === -1 || circuit.master !== 0) {
412
+ let circ = await super.setCircuitAsync(data);
413
+ return circ;
414
+ }
415
+ let typeByte = parseInt(data.type, 10) || circuit.type || sys.board.valueMaps.circuitFunctions.getValue('generic');
416
+ let nameByte = 3; // set default `Aux 1`
417
+ if (typeof data.nameId !== 'undefined') nameByte = data.nameId;
418
+ else if (typeof circuit.name !== 'undefined') nameByte = circuit.nameId;
419
+ return new Promise<ICircuit>(async (resolve, reject) => {
420
+ let circuit = sys.circuits.getInterfaceById(data.id);
421
+ let cstate = state.circuits.getInterfaceById(data.id);
422
+ circuit.nameId = cstate.nameId = nameByte;
423
+ circuit.name = cstate.name = sys.board.valueMaps.circuitNames.transform(nameByte).desc;
424
+ circuit.showInFeatures = cstate.showInFeatures = typeof data.showInFeatures !== 'undefined' ? data.showInFeatures : circuit.showInFeatures || true;
425
+ circuit.freeze = typeof data.freeze !== 'undefined' ? utils.makeBool(data.freeze) : circuit.freeze;
426
+ circuit.type = cstate.type = typeByte;
427
+ circuit.eggTimer = typeof data.eggTimer !== 'undefined' ? parseInt(data.eggTimer, 10) : circuit.eggTimer || 720;
428
+ circuit.dontStop = (typeof data.dontStop !== 'undefined') ? utils.makeBool(data.dontStop) : circuit.eggTimer === 1620;
429
+ cstate.isActive = circuit.isActive = true;
430
+ circuit.master = 0;
431
+ state.emitEquipmentChanges();
432
+ resolve(circuit);
433
+ });
434
+ }
435
+ catch (err) { logger.error(`setCircuitAsync error setting circuit ${JSON.stringify(data)}: ${err}`); return Promise.reject(err); }
436
+ }
437
+ public async deleteCircuitAsync(data: any): Promise<ICircuit> {
438
+ let circuit = sys.circuits.getItemById(data.id);
439
+ if (circuit.master === 1) return await super.deleteCircuitAsync(data);
440
+ data.nameId = 0;
441
+ data.functionId = sys.board.valueMaps.circuitFunctions.getValue('notused');
442
+ return this.setCircuitAsync(data);
443
+ }
444
+ public async setCircuitStateAsync(id: number, val: boolean, ignoreDelays?: boolean): Promise<ICircuitState> {
445
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('Circuit or Feature id not valid', id, 'Circuit'));
446
+ let c = sys.circuits.getInterfaceById(id);
447
+ if (c.master !== 0) return await super.setCircuitStateAsync(id, val);
448
+ if (id === 192 || c.type === 3) return await sys.board.circuits.setLightGroupThemeAsync(id - 191, val ? 1 : 0);
449
+ if (id >= 192) return await sys.board.circuits.setCircuitGroupStateAsync(id, val);
450
+
451
+ // for some dumb reason, if the spa is on and the pool circuit is desired to be on,
452
+ // it will ignore the packet.
453
+ // We can override that by emulating a click to turn off the spa instead of turning
454
+ // on the pool
455
+ if (sys.equipment.maxBodies > 1 && id === 6 && val && state.circuits.getItemById(1).isOn) {
456
+ id = 1;
457
+ val = false;
458
+ }
459
+ return new Promise<ICircuitState>((resolve, reject) => {
460
+ let cstate = state.circuits.getInterfaceById(id);
461
+ sys.board.circuits.setEndTime(c, cstate, val);
462
+ cstate.isOn = val;
463
+ state.emitEquipmentChanges();
464
+ resolve(cstate);
465
+ });
466
+ }
467
+ public async setLightGroupStateAsync(id: number, val: boolean): Promise<ICircuitGroupState> { return this.setCircuitGroupStateAsync(id, val); }
468
+ public async toggleCircuitStateAsync(id: number) {
469
+ let cstate = state.circuits.getInterfaceById(id);
470
+ if (cstate instanceof LightGroupState) {
471
+ return await this.setLightGroupThemeAsync(id, sys.board.valueMaps.lightThemes.getValue(cstate.isOn ? 'off' : 'on'));
472
+ }
473
+ return await this.setCircuitStateAsync(id, !cstate.isOn);
474
+ }
475
+ public async setLightGroupAsync(obj: any): Promise<LightGroup> {
476
+ let group: LightGroup = null;
477
+ let id = typeof obj.id !== 'undefined' ? parseInt(obj.id, 10) : -1;
478
+ if (id <= 0) {
479
+ // We are adding a circuit group.
480
+ id = sys.circuitGroups.getNextEquipmentId(sys.board.equipmentIds.circuitGroups);
481
+ }
482
+ if (typeof id === 'undefined') return Promise.reject(new InvalidEquipmentIdError(`Max circuit light group id exceeded`, id, 'LightGroup'));
483
+ if (isNaN(id) || !sys.board.equipmentIds.circuitGroups.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`Invalid circuit group id: ${obj.id}`, obj.id, 'LightGroup'));
484
+ group = sys.lightGroups.getItemById(id, true);
485
+
486
+ if (typeof obj.name !== 'undefined') group.name = obj.name;
487
+ if (typeof obj.eggTimer !== 'undefined') group.eggTimer = Math.min(Math.max(parseInt(obj.eggTimer, 10), 0), 1440);
488
+ group.dontStop = (group.eggTimer === 1440);
489
+ group.isActive = true;
490
+ if (typeof obj.circuits !== 'undefined') {
491
+ for (let i = 0; i < obj.circuits.length; i++) {
492
+ let cobj = obj.circuits[i];
493
+ let c: LightGroupCircuit;
494
+ if (typeof cobj.id !== 'undefined') c = group.circuits.getItemById(parseInt(cobj.id, 10), true);
495
+ else if (typeof cobj.circuit !== 'undefined') c = group.circuits.getItemByCircuitId(parseInt(cobj.circuit, 10), true);
496
+ else c = group.circuits.getItemByIndex(i, true, { id: i + 1 });
497
+ if (typeof cobj.circuit !== 'undefined') c.circuit = cobj.circuit;
498
+ //if (typeof cobj.lightingTheme !== 'undefined') c.lightingTheme = parseInt(cobj.lightingTheme, 10); // does this belong here?
499
+ if (typeof cobj.color !== 'undefined') c.color = parseInt(cobj.color, 10);
500
+ if (typeof cobj.swimDelay !== 'undefined') c.swimDelay = parseInt(cobj.swimDelay, 10);
501
+ if (typeof cobj.position !== 'undefined') c.position = parseInt(cobj.position, 10);
502
+ }
503
+ }
504
+ return new Promise<LightGroup>(async (resolve, reject) => {
505
+ try { resolve(group); }
506
+ catch (err) { reject(err); }
507
+ });
508
+ }
509
+ public async setLightThemeAsync(id: number, theme: number): Promise<ICircuitState> {
510
+ // Re-route this as we cannot set individual circuit themes in *Touch.
511
+ return this.setLightGroupThemeAsync(id, theme);
512
+ }
513
+ public async runLightGroupCommandAsync(obj: any): Promise<ICircuitState> {
514
+ // Do all our validation.
515
+ try {
516
+ let id = parseInt(obj.id, 10);
517
+ let cmd = typeof obj.command !== 'undefined' ? sys.board.valueMaps.lightGroupCommands.findItem(obj.command) : { val: 0, name: 'undefined' };
518
+ if (cmd.val === 0) return Promise.reject(new InvalidOperationError(`Light group command ${cmd.name} does not exist`, 'runLightGroupCommandAsync'));
519
+ if (isNaN(id)) return Promise.reject(new InvalidOperationError(`Light group ${id} does not exist`, 'runLightGroupCommandAsync'));
520
+ let grp = sys.lightGroups.getItemById(id);
521
+ let nop = sys.board.valueMaps.circuitActions.getValue(cmd.name);
522
+ let sgrp = state.lightGroups.getItemById(grp.id);
523
+ sgrp.action = nop;
524
+ sgrp.emitEquipmentChange();
525
+ switch (cmd.name) {
526
+ case 'colorset':
527
+ await this.sequenceLightGroupAsync(id, 'colorset');
528
+ break;
529
+ case 'colorswim':
530
+ await this.sequenceLightGroupAsync(id, 'colorswim');
531
+ break;
532
+ case 'colorhold':
533
+ await this.setLightGroupThemeAsync(id, 190);
534
+ break;
535
+ case 'colorrecall':
536
+ await this.setLightGroupThemeAsync(id, 191);
537
+ break;
538
+ case 'lightthumper':
539
+ await this.setLightGroupThemeAsync(id, 208);
540
+ break;
541
+ }
542
+ sgrp.action = 0;
543
+ sgrp.emitEquipmentChange();
544
+ return sgrp;
545
+ }
546
+ catch (err) { return Promise.reject(`Error runLightGroupCommandAsync ${err.message}`); }
547
+ }
548
+ public async runLightCommandAsync(obj: any): Promise<ICircuitState> {
549
+ // Do all our validation.
550
+ try {
551
+ let id = parseInt(obj.id, 10);
552
+ let cmd = typeof obj.command !== 'undefined' ? sys.board.valueMaps.lightCommands.findItem(obj.command) : { val: 0, name: 'undefined' };
553
+ if (cmd.val === 0) return Promise.reject(new InvalidOperationError(`Light command ${cmd.name} does not exist`, 'runLightCommandAsync'));
554
+ if (isNaN(id)) return Promise.reject(new InvalidOperationError(`Light ${id} does not exist`, 'runLightCommandAsync'));
555
+ let circ = sys.circuits.getItemById(id);
556
+ if (!circ.isActive) return Promise.reject(new InvalidOperationError(`Light circuit #${id} is not active`, 'runLightCommandAsync'));
557
+ let type = sys.board.valueMaps.circuitFunctions.transform(circ.type);
558
+ if (!type.isLight) return Promise.reject(new InvalidOperationError(`Circuit #${id} is not a light`, 'runLightCommandAsync'));
559
+ let nop = sys.board.valueMaps.circuitActions.getValue(cmd.name);
560
+ let slight = state.circuits.getItemById(circ.id);
561
+ slight.action = nop;
562
+ slight.emitEquipmentChange();
563
+ // Touch boards cannot change the theme or color of a single light.
564
+ slight.action = 0;
565
+ slight.emitEquipmentChange();
566
+ return slight;
567
+ }
568
+ catch (err) { return Promise.reject(`Error runLightCommandAsync ${err.message}`); }
569
+ }
570
+ public async setLightGroupThemeAsync(id = sys.board.equipmentIds.circuitGroups.start, theme: number): Promise<ICircuitState> {
571
+ return new Promise<ICircuitState>((resolve, reject) => {
572
+ const grp = sys.lightGroups.getItemById(id);
573
+ const sgrp = state.lightGroups.getItemById(id);
574
+ grp.lightingTheme = sgrp.lightingTheme = theme;
575
+ sgrp.action = sys.board.valueMaps.circuitActions.getValue('lighttheme');
576
+ sgrp.emitEquipmentChange();
577
+ try {
578
+ // Let everyone know we turned these on. The theme messages will come later.
579
+ for (let i = 0; i < grp.circuits.length; i++) {
580
+ let c = grp.circuits.getItemByIndex(i);
581
+ let cstate = state.circuits.getItemById(c.circuit);
582
+ // if theme is 'off' light groups should not turn on
583
+ }
584
+ let isOn = sys.board.valueMaps.lightThemes.getName(theme) === 'off' ? false : true;
585
+ sys.board.circuits.setEndTime(grp, sgrp, isOn);
586
+ sgrp.isOn = isOn;
587
+ switch (theme) {
588
+ case 0: // off
589
+ case 1: // on
590
+ break;
591
+ case 128: // sync
592
+ setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'sync'); });
593
+ break;
594
+ case 144: // swim
595
+ setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'swim'); });
596
+ break;
597
+ case 160: // swim
598
+ setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'set'); });
599
+ break;
600
+ case 190: // save
601
+ case 191: // recall
602
+ setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'other'); });
603
+ break;
604
+ default:
605
+ setImmediate(function () { sys.board.circuits.sequenceLightGroupAsync(grp.id, 'color'); });
606
+ // other themes for magicstream?
607
+ }
608
+ sgrp.action = 0;
609
+ sgrp.hasChanged = true; // Say we are dirty but we really are pure as the driven snow.
610
+ state.emitEquipmentChanges();
611
+ resolve(sgrp);
612
+ }
613
+ catch (err) {
614
+ logger.error(`error setting intellibrite theme: ${err.message}`);
615
+ reject(err);
616
+ }
617
+ });
618
+ }
619
+ }
620
+ class AquaLinkFeatureCommands extends FeatureCommands {
621
+ // todo: remove this in favor of setCircuitState only?
622
+ public async setFeatureStateAsync(id: number, val: boolean): Promise<ICircuitState> {
623
+ // Route this to the circuit state since this is the same call
624
+ // and the interface takes care of it all.
625
+ return this.board.circuits.setCircuitStateAsync(id, val);
626
+ }
627
+ public async toggleFeatureStateAsync(id: number) {
628
+ // Route this to the circuit state since this is the same call
629
+ // and the interface takes care of it all.
630
+ return this.board.circuits.toggleCircuitStateAsync(id);
631
+ }
632
+ public async setFeatureAsync(data: any): Promise<Feature> {
633
+ return new Promise<Feature>((resolve, reject) => {
634
+ let id = parseInt(data.id, 10);
635
+ let feature: Feature;
636
+ if (id <= 0) {
637
+ id = sys.features.getNextEquipmentId(sys.board.equipmentIds.features);
638
+ feature = sys.features.getItemById(id, false, { isActive: true, freeze: false });
639
+ }
640
+ else
641
+ feature = sys.features.getItemById(id, false);
642
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError('feature Id has not been defined', data.id, 'Feature'));
643
+ if (!sys.board.equipmentIds.features.isInRange(id)) return Promise.reject(new InvalidEquipmentIdError(`feature Id ${id}: is out of range.`, id, 'Feature'));
644
+ let typeByte = data.type || feature.type || sys.board.valueMaps.circuitFunctions.getValue('generic');
645
+ let nameByte = 3; // set default `Aux 1`
646
+ if (typeof data.nameId !== 'undefined') nameByte = data.nameId;
647
+ else if (typeof feature.name !== 'undefined') nameByte = feature.nameId;
648
+ feature = sys.features.getItemById(id);
649
+ let fstate = state.features.getItemById(data.id);
650
+ feature.nameId = fstate.nameId = nameByte;
651
+ // circuit.name = cstate.name = sys.board.valueMaps.circuitNames.get(nameByte).desc;
652
+ feature.name = fstate.name = sys.board.valueMaps.circuitNames.transform(nameByte).desc;
653
+ feature.type = fstate.type = typeByte;
654
+
655
+ feature.freeze = (typeof data.freeze !== 'undefined' ? utils.makeBool(data.freeze) : feature.freeze);
656
+ fstate.showInFeatures = feature.showInFeatures = (typeof data.showInFeatures !== 'undefined' ? utils.makeBool(data.showInFeatures) : feature.showInFeatures);
657
+ feature.eggTimer = typeof data.eggTimer !== 'undefined' ? parseInt(data.eggTimer, 10) : feature.eggTimer || 720;
658
+ feature.dontStop = (typeof data.dontStop !== 'undefined') ? utils.makeBool(data.dontStop) : feature.eggTimer === 1620;
659
+ let eggTimer = sys.eggTimers.find(elem => elem.circuit === id);
660
+ state.emitEquipmentChanges();
661
+ resolve(feature);
662
+ });
663
+ }
664
+ }
665
+ class AquaLinkChlorinatorCommands extends ChlorinatorCommands {
666
+ public async setChlorAsync(obj: any): Promise<ChlorinatorState> {
667
+ let id = parseInt(obj.id, 10);
668
+ let isAdd = false;
669
+ let chlor = sys.chlorinators.getItemById(id);
670
+ if (id <= 0 || isNaN(id)) {
671
+ isAdd = true;
672
+ chlor.master = utils.makeBool(obj.master) ? 1 : 0;
673
+ // Calculate an id for the chlorinator. The messed up part is that if a chlorinator is not attached to the OCP, its address
674
+ // cannot be set by the MUX. This will have to wait.
675
+ id = 1;
676
+ }
677
+ // If this is a Nixie chlorinator then go to the base class and handle it from there.
678
+ if (chlor.master === 1) return super.setChlorAsync(obj);
679
+ // RKS: I am not even sure this can be done with Touch as the master on the RS485 bus.
680
+ if (typeof chlor.master === 'undefined') chlor.master = 0;
681
+ let name = obj.name || chlor.name || 'IntelliChlor' + id;
682
+ let superChlorHours = parseInt(obj.superChlorHours, 10);
683
+ if (typeof obj.superChlorinate !== 'undefined') obj.superChlor = utils.makeBool(obj.superChlorinate);
684
+ let superChlorinate = typeof obj.superChlor === 'undefined' ? undefined : utils.makeBool(obj.superChlor);
685
+ let isDosing = typeof obj.isDosing !== 'undefined' ? utils.makeBool(obj.isDosing) : chlor.isDosing;
686
+ let disabled = typeof obj.disabled !== 'undefined' ? utils.makeBool(obj.disabled) : chlor.disabled;
687
+ let poolSetpoint = typeof obj.poolSetpoint !== 'undefined' ? parseInt(obj.poolSetpoint, 10) : chlor.poolSetpoint;
688
+ let spaSetpoint = typeof obj.spaSetpoint !== 'undefined' ? parseInt(obj.spaSetpoint, 10) : chlor.spaSetpoint;
689
+ let model = typeof obj.model !== 'undefined' ? obj.model : chlor.model;
690
+ let portId = typeof obj.portId !== 'undefined' ? parseInt(obj.portId, 10) : chlor.portId;
691
+ if (portId !== chlor.portId && sys.chlorinators.count(elem => elem.id !== chlor.id && elem.portId === portId && elem.master !== 2) > 0) return Promise.reject(new InvalidEquipmentDataError(`Another chlorinator is installed on port #${portId}. Only one chlorinator can be installed per port.`, 'Chlorinator', portId));
692
+ let saltTarget = typeof obj.saltTarget === 'number' ? parseInt(obj.saltTarget, 10) : chlor.saltTarget;
693
+
694
+ let chlorType = typeof obj.type !== 'undefined' ? sys.board.valueMaps.chlorinatorType.encode(obj.type) : chlor.type || 0;
695
+ if (isAdd) {
696
+ if (isNaN(poolSetpoint)) poolSetpoint = 50;
697
+ if (isNaN(spaSetpoint)) spaSetpoint = 10;
698
+ if (isNaN(superChlorHours)) superChlorHours = 8;
699
+ if (typeof superChlorinate === 'undefined') superChlorinate = false;
700
+ }
701
+ else {
702
+ if (isNaN(poolSetpoint)) poolSetpoint = chlor.poolSetpoint || 0;
703
+ if (isNaN(spaSetpoint)) spaSetpoint = chlor.spaSetpoint || 0;
704
+ if (isNaN(superChlorHours)) superChlorHours = chlor.superChlorHours;
705
+ if (typeof superChlorinate === 'undefined') superChlorinate = utils.makeBool(chlor.superChlor);
706
+ }
707
+ if (typeof obj.disabled !== 'undefined') chlor.disabled = utils.makeBool(obj.disabled);
708
+ if (typeof chlor.body === 'undefined') chlor.body = parseInt(obj.body, 10) || 32;
709
+ // Verify the data.
710
+ let body = sys.board.bodies.mapBodyAssociation(chlor.body);
711
+ if (typeof body === 'undefined') {
712
+ if (sys.equipment.shared) body = 32;
713
+ else if (!sys.equipment.dual) body = 1;
714
+ else return Promise.reject(new InvalidEquipmentDataError(`Chlorinator body association is not valid: ${body}`, 'chlorinator', body));
715
+ }
716
+ if (poolSetpoint > 100 || poolSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator poolSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', chlor.poolSetpoint));
717
+ if (spaSetpoint > 100 || spaSetpoint < 0) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator spaSetpoint is out of range: ${chlor.poolSetpoint}`, 'chlorinator', chlor.spaSetpoint));
718
+ if (typeof obj.ignoreSaltReading !== 'undefined') chlor.ignoreSaltReading = utils.makeBool(obj.ignoreSaltReading);
719
+
720
+ let _timeout: NodeJS.Timeout;
721
+ try {
722
+ let schlor = state.chlorinators.getItemById(id, true);
723
+ chlor.disabled = disabled;
724
+ chlor.saltTarget = saltTarget;
725
+ schlor.isActive = chlor.isActive = true;
726
+ schlor.superChlor = chlor.superChlor = superChlorinate;
727
+ schlor.poolSetpoint = chlor.poolSetpoint = poolSetpoint;
728
+ schlor.spaSetpoint = chlor.spaSetpoint = spaSetpoint;
729
+ schlor.superChlorHours = chlor.superChlorHours = superChlorHours;
730
+ schlor.body = chlor.body = body;
731
+ if (typeof chlor.address === 'undefined') chlor.address = 80; // 79 + id;
732
+ chlor.name = schlor.name = name;
733
+ schlor.model = chlor.model = model;
734
+ schlor.type = chlor.type = chlorType;
735
+ chlor.isDosing = isDosing;
736
+ chlor.portId = portId;
737
+ state.emitEquipmentChanges();
738
+ return state.chlorinators.getItemById(id);
739
+ } catch (err) {
740
+ logger.error(`AquaLink setChlorAsync Error: ${err.message}`);
741
+ return Promise.reject(err);
742
+ }
743
+ }
744
+ public async deleteChlorAsync(obj: any): Promise<ChlorinatorState> {
745
+ let id = parseInt(obj.id, 10);
746
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentDataError(`Chlorinator id is not valid: ${obj.id}`, 'chlorinator', obj.id));
747
+ let chlor = sys.chlorinators.getItemById(id);
748
+ if (chlor.master === 1) return await super.deleteChlorAsync(obj);
749
+ return new Promise<ChlorinatorState>((resolve, reject) => {
750
+ ncp.chlorinators.deleteChlorinatorAsync(id).then(() => { });
751
+ let cstate = state.chlorinators.getItemById(id, true);
752
+ chlor = sys.chlorinators.getItemById(id, true);
753
+ chlor.isActive = cstate.isActive = false;
754
+ sys.chlorinators.removeItemById(id);
755
+ state.chlorinators.removeItemById(id);
756
+ resolve(cstate);
757
+ });
758
+ }
759
+ }
760
+ class AquaLinkPumpCommands extends PumpCommands {
761
+ public async setPumpAsync(data: any): Promise<Pump> {
762
+ let pump: Pump;
763
+ let ntype;
764
+ let type;
765
+ let isAdd = false;
766
+ let id = (typeof data.id === 'undefined') ? -1 : parseInt(data.id, 10);
767
+ if (typeof data.id === 'undefined' || isNaN(id) || id <= 0) {
768
+ // We are adding a new pump
769
+ ntype = parseInt(data.type, 10);
770
+ type = sys.board.valueMaps.pumpTypes.transform(ntype);
771
+ // If this is one of the pumps that are not supported by touch send it to system board.
772
+ if (type.equipmentMaster === 1) return super.setPumpAsync(data);
773
+ if (typeof data.type === 'undefined' || isNaN(ntype) || typeof type.name === 'undefined') return Promise.reject(new InvalidEquipmentDataError('You must supply a pump type when creating a new pump', 'Pump', data));
774
+ isAdd = true;
775
+ pump = sys.pumps.getItemById(id, true);
776
+ }
777
+ else {
778
+ pump = sys.pumps.getItemById(id, false);
779
+ if (data.master > 0 || pump.master > 0) return await super.setPumpAsync(data);
780
+ ntype = typeof data.type === 'undefined' ? pump.type : parseInt(data.type, 10);
781
+ if (isNaN(ntype)) return Promise.reject(new InvalidEquipmentDataError(`Pump type ${data.type} is not valid`, 'Pump', data));
782
+ type = sys.board.valueMaps.pumpTypes.transform(ntype);
783
+ // changing type? clear out all props and add as new
784
+ if (ntype !== pump.type) {
785
+ isAdd = true;
786
+ //super.setType(pump, ntype);
787
+ pump = sys.pumps.getItemById(id, false); // refetch pump with new value
788
+ }
789
+ }
790
+ // Validate all the ids since in *Touch the address is determined from the id.
791
+ if (!isAdd) isAdd = sys.pumps.find(elem => elem.id === id) === undefined;
792
+ // Now lets validate the ids related to the type.
793
+ if (id === 9 && type.name !== 'ds') return Promise.reject(new InvalidEquipmentDataError(`The id for a ${type.desc} pump must be 9`, 'Pump', data));
794
+ else if (id === 10 && type.name !== 'ss') return Promise.reject(new InvalidEquipmentDataError(`The id for a ${type.desc} pump must be 10`, 'Pump', data));
795
+ else if (id > sys.equipment.maxPumps) return Promise.reject(new InvalidEquipmentDataError(`The id for a ${type.desc} must be less than ${sys.equipment.maxPumps}`, 'Pump', data));
796
+
797
+
798
+ // Need to do a check here if we are clearing out the circuits; id data.circuits === []
799
+ // extend will keep the original array
800
+ let bClearPumpCircuits = typeof data.circuits !== 'undefined' && data.circuits.length === 0;
801
+
802
+ if (!isAdd) data = extend(true, {}, pump.get(true), data, { id: id, type: ntype });
803
+ else data = extend(false, {}, data, { id: id, type: ntype });
804
+ if (!isAdd && bClearPumpCircuits) data.circuits = [];
805
+ data.name = data.name || pump.name || type.desc;
806
+ // We will not be sending message for ss type pumps.
807
+ if (type.name === 'ss') {
808
+ // The OCP doesn't deal with single speed pumps. Simply add it to the config.
809
+ data.circuits = [];
810
+ pump.set(pump);
811
+ let spump = state.pumps.getItemById(id, true);
812
+ for (let prop in spump) {
813
+ if (typeof data[prop] !== 'undefined') spump[prop] = data[prop];
814
+ }
815
+ spump.emitEquipmentChange();
816
+ return Promise.resolve(pump);
817
+ }
818
+ else if (type.name === 'ds') {
819
+ // We are going to set all the high speed circuits.
820
+ // RSG: TODO I don't know what the message is to set the high speed circuits. The following should
821
+ // be moved into the onComplete for the outbound message to set high speed circuits.
822
+ for (let prop in pump) {
823
+ if (typeof data[prop] !== 'undefined') pump[prop] = data[prop];
824
+ }
825
+ let spump = state.pumps.getItemById(id, true);
826
+ for (let prop in spump) {
827
+ if (typeof data[prop] !== 'undefined') spump[prop] = data[prop];
828
+ }
829
+ spump.emitEquipmentChange();
830
+ return Promise.resolve(pump);
831
+ }
832
+ else {
833
+ let arr = [];
834
+ return new Promise<Pump>((resolve, reject) => {
835
+ pump = sys.pumps.getItemById(id, true);
836
+ pump.set(data); // Sets all the data back to the pump.
837
+ let spump = state.pumps.getItemById(id, true);
838
+ spump.name = pump.name;
839
+ spump.type = pump.type;
840
+ spump.emitEquipmentChange();
841
+ resolve(pump);
842
+ });
843
+ }
844
+ }
845
+ public async deletePumpAsync(data: any): Promise<Pump> {
846
+ let id = parseInt(data.id, 10);
847
+ if (isNaN(id)) return Promise.reject(new InvalidEquipmentIdError(`deletePumpAsync: Pump ${id} is not valid.`, 0, `pump`));
848
+ let pump = sys.pumps.getItemById(id, false);
849
+ if (pump.master === 1) return super.deletePumpAsync(data);
850
+ return new Promise<Pump>((resolve, reject) => { resolve(sys.pumps.getItemById(id)); });
851
+ }
852
+ }
853
+ class AquaLinkHeaterCommands extends HeaterCommands {
854
+ public getInstalledHeaterTypes(body?: number): any {
855
+ let heaters = sys.heaters.get();
856
+ let types = sys.board.valueMaps.heaterTypes.toArray();
857
+ let inst = { total: 0 };
858
+ for (let i = 0; i < types.length; i++) if (types[i].name !== 'none') inst[types[i].name] = 0;
859
+ for (let i = 0; i < heaters.length; i++) {
860
+ let heater = heaters[i];
861
+ if (typeof body !== 'undefined' && heater.body !== 'undefined') {
862
+ if ((heater.body !== 32 && body !== heater.body + 1) || (heater.body === 32 && body > 2)) continue;
863
+ }
864
+ let type = types.find(elem => elem.val === heater.type);
865
+ if (typeof type !== 'undefined') {
866
+ if (inst[type.name] === 'undefined') inst[type.name] = 0;
867
+ inst[type.name] = inst[type.name] + 1;
868
+ if (heater.coolingEnabled === true && type.hasCoolSetpoint === true) inst['hasCoolSetpoint'] = true;
869
+ inst.total++;
870
+ }
871
+ }
872
+ return inst;
873
+ }
874
+ public isSolarInstalled(body?: number): boolean {
875
+ let heaters = sys.heaters.get();
876
+ let types = sys.board.valueMaps.heaterTypes.toArray();
877
+ for (let i = 0; i < heaters.length; i++) {
878
+ let heater = heaters[i];
879
+ if (typeof body !== 'undefined' && body !== heater.body) continue;
880
+ let type = types.find(elem => elem.val === heater.type);
881
+ if (typeof type !== 'undefined') {
882
+ switch (type.name) {
883
+ case 'solar':
884
+ return true;
885
+ }
886
+ }
887
+ }
888
+ }
889
+ public isHeatPumpInstalled(body?: number): boolean {
890
+ let heaters = sys.heaters.get();
891
+ let types = sys.board.valueMaps.heaterTypes.toArray();
892
+ for (let i = 0; i < heaters.length; i++) {
893
+ let heater = heaters[i];
894
+ if (typeof body !== 'undefined' && body !== heater.body) continue;
895
+ let type = types.find(elem => elem.val === heater.type);
896
+ if (typeof type !== 'undefined') {
897
+ switch (type.name) {
898
+ case 'heatpump':
899
+ return true;
900
+ }
901
+ }
902
+ }
903
+ }
904
+ public async setHeaterAsync(obj: any): Promise<Heater> {
905
+ if (obj.master === 1 || parseInt(obj.id, 10) > 255) return super.setHeaterAsync(obj);
906
+ return new Promise<Heater>((resolve, reject) => {
907
+ let id = typeof obj.id === 'undefined' ? -1 : parseInt(obj.id, 10);
908
+ if (isNaN(id)) return reject(new InvalidEquipmentIdError('Heater Id is not valid.', obj.id, 'Heater'));
909
+ let heater: Heater;
910
+ let address: number;
911
+ let htype;
912
+ heater.address = address;
913
+ heater.master = 0;
914
+ heater.body = sys.equipment.shared ? 32 : 0;
915
+ sys.board.heaters.updateHeaterServices();
916
+ sys.board.heaters.syncHeaterStates();
917
+ resolve(heater);
918
+ });
919
+ }
920
+ public async deleteHeaterAsync(obj: any): Promise<Heater> {
921
+ if (utils.makeBool(obj.master === 1 || parseInt(obj.id, 10) > 255)) return super.deleteHeaterAsync(obj);
922
+ return new Promise<Heater>((resolve, reject) => {
923
+ let id = parseInt(obj.id, 10);
924
+ if (isNaN(id)) return reject(new InvalidEquipmentIdError('Cannot delete. Heater Id is not valid.', obj.id, 'Heater'));
925
+ let heater = sys.heaters.getItemById(id);
926
+ heater.isActive = false;
927
+ sys.heaters.removeItemById(id);
928
+ state.heaters.removeItemById(id);
929
+ sys.board.heaters.updateHeaterServices();
930
+ sys.board.heaters.syncHeaterStates();
931
+ resolve(heater);
932
+ });
933
+ }
934
+ public updateHeaterServices() {
935
+ let htypes = sys.board.heaters.getInstalledHeaterTypes();
936
+ let solarInstalled = htypes.solar > 0;
937
+ let heatPumpInstalled = htypes.heatpump > 0;
938
+ let ultratempInstalled = htypes.ultratemp > 0;
939
+ let gasHeaterInstalled = htypes.gas > 0;
940
+ let hybridInstalled = htypes.hybrid > 0;
941
+ sys.board.valueMaps.heatModes.set(0, { name: 'off', desc: 'Off' });
942
+ sys.board.valueMaps.heatSources.set(0, { name: 'off', desc: 'Off' });
943
+ if (hybridInstalled) {
944
+ // Source Issue #390
945
+ // 1 = Heat Pump
946
+ // 2 = Gas Heater
947
+ // 3 = Hybrid
948
+ // 16 = Dual
949
+ sys.board.valueMaps.heatModes.set(1, { name: 'heatpump', desc: 'Heat Pump' });
950
+ sys.board.valueMaps.heatModes.set(2, { name: 'heater', desc: 'Gas Heat' });
951
+ sys.board.valueMaps.heatModes.set(3, { name: 'heatpumppref', desc: 'Hybrid' });
952
+ sys.board.valueMaps.heatModes.set(16, { name: 'dual', desc: 'Dual Heat' });
953
+
954
+ sys.board.valueMaps.heatSources.set(2, { name: 'heater', desc: 'Gas Heat' });
955
+ sys.board.valueMaps.heatSources.set(5, { name: 'heatpumppref', desc: 'Hybrid' });
956
+ sys.board.valueMaps.heatSources.set(20, { name: 'dual', desc: 'Dual Heat' });
957
+ sys.board.valueMaps.heatSources.set(21, { name: 'heatpump', desc: 'Heat Pump' });
958
+ }
959
+ else {
960
+ if (gasHeaterInstalled) {
961
+ sys.board.valueMaps.heatModes.set(1, { name: 'heater', desc: 'Heater' });
962
+ sys.board.valueMaps.heatSources.set(2, { name: 'heater', desc: 'Heater' });
963
+ }
964
+ else {
965
+ // no heaters (virtual controller)
966
+ sys.board.valueMaps.heatModes.delete(1);
967
+ sys.board.valueMaps.heatSources.delete(2);
968
+ }
969
+ if (solarInstalled && gasHeaterInstalled) {
970
+ sys.board.valueMaps.heatModes.set(2, { name: 'solarpref', desc: 'Solar Preferred' });
971
+ sys.board.valueMaps.heatModes.set(3, { name: 'solar', desc: 'Solar Only' });
972
+ sys.board.valueMaps.heatSources.set(5, { name: 'solarpref', desc: 'Solar Preferred' });
973
+ sys.board.valueMaps.heatSources.set(21, { name: 'solar', desc: 'Solar Only' });
974
+ }
975
+ else if (heatPumpInstalled && gasHeaterInstalled) {
976
+ sys.board.valueMaps.heatModes.set(2, { name: 'heatpumppref', desc: 'Heat Pump Preferred' });
977
+ sys.board.valueMaps.heatModes.set(3, { name: 'heatpump', desc: 'Heat Pump Only' });
978
+ sys.board.valueMaps.heatSources.set(5, { name: 'heatpumppref', desc: 'Heat Pump Preferred' });
979
+ sys.board.valueMaps.heatSources.set(21, { name: 'heatpump', desc: 'Heat Pump Only' });
980
+ }
981
+ else if (ultratempInstalled && gasHeaterInstalled) {
982
+ sys.board.valueMaps.heatModes.merge([
983
+ [2, { name: 'ultratemppref', desc: 'UltraTemp Pref' }],
984
+ [3, { name: 'ultratemp', desc: 'UltraTemp Only' }]
985
+ ]);
986
+ sys.board.valueMaps.heatSources.merge([
987
+ [5, { name: 'ultratemppref', desc: 'Ultratemp Pref', hasCoolSetpoint: htypes.hasCoolSetpoint }],
988
+ [21, { name: 'ultratemp', desc: 'Ultratemp Only', hasCoolSetpoint: htypes.hasCoolSetpoint }]
989
+ ])
990
+ }
991
+ else {
992
+ // only gas
993
+ sys.board.valueMaps.heatModes.delete(2);
994
+ sys.board.valueMaps.heatModes.delete(3);
995
+ sys.board.valueMaps.heatSources.delete(5);
996
+ sys.board.valueMaps.heatSources.delete(21);
997
+ }
998
+ }
999
+ sys.board.valueMaps.heatSources.set(32, { name: 'nochange', desc: 'No Change' });
1000
+ this.setActiveTempSensors();
1001
+ }
1002
+ }
1003
+