nodejs-poolcontroller 7.7.0 → 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 (82) hide show
  1. package/.eslintrc.json +26 -35
  2. package/Changelog +22 -0
  3. package/README.md +7 -3
  4. package/anslq25/MessagesMock.ts +218 -0
  5. package/anslq25/boards/MockBoardFactory.ts +50 -0
  6. package/anslq25/boards/MockEasyTouchBoard.ts +696 -0
  7. package/anslq25/boards/MockSystemBoard.ts +217 -0
  8. package/anslq25/chemistry/MockChlorinator.ts +75 -0
  9. package/anslq25/pumps/MockPump.ts +84 -0
  10. package/app.ts +10 -14
  11. package/config/Config.ts +13 -9
  12. package/config/VersionCheck.ts +6 -2
  13. package/controller/Constants.ts +58 -25
  14. package/controller/Equipment.ts +224 -41
  15. package/controller/Errors.ts +2 -1
  16. package/controller/Lockouts.ts +34 -2
  17. package/controller/State.ts +491 -48
  18. package/controller/boards/AquaLinkBoard.ts +6 -3
  19. package/controller/boards/BoardFactory.ts +5 -1
  20. package/controller/boards/EasyTouchBoard.ts +1971 -1751
  21. package/controller/boards/IntelliCenterBoard.ts +1311 -1688
  22. package/controller/boards/IntelliComBoard.ts +7 -1
  23. package/controller/boards/IntelliTouchBoard.ts +153 -42
  24. package/controller/boards/NixieBoard.ts +209 -66
  25. package/controller/boards/SunTouchBoard.ts +393 -0
  26. package/controller/boards/SystemBoard.ts +1862 -1543
  27. package/controller/comms/Comms.ts +539 -138
  28. package/controller/comms/ScreenLogic.ts +1663 -0
  29. package/controller/comms/messages/Messages.ts +242 -60
  30. package/controller/comms/messages/config/ChlorinatorMessage.ts +4 -3
  31. package/controller/comms/messages/config/CircuitGroupMessage.ts +5 -2
  32. package/controller/comms/messages/config/CircuitMessage.ts +81 -13
  33. package/controller/comms/messages/config/ConfigMessage.ts +3 -1
  34. package/controller/comms/messages/config/CoverMessage.ts +2 -1
  35. package/controller/comms/messages/config/CustomNameMessage.ts +2 -1
  36. package/controller/comms/messages/config/EquipmentMessage.ts +5 -1
  37. package/controller/comms/messages/config/ExternalMessage.ts +33 -3
  38. package/controller/comms/messages/config/FeatureMessage.ts +2 -1
  39. package/controller/comms/messages/config/GeneralMessage.ts +2 -1
  40. package/controller/comms/messages/config/HeaterMessage.ts +3 -1
  41. package/controller/comms/messages/config/IntellichemMessage.ts +2 -1
  42. package/controller/comms/messages/config/OptionsMessage.ts +12 -6
  43. package/controller/comms/messages/config/PumpMessage.ts +9 -12
  44. package/controller/comms/messages/config/RemoteMessage.ts +80 -13
  45. package/controller/comms/messages/config/ScheduleMessage.ts +43 -3
  46. package/controller/comms/messages/config/SecurityMessage.ts +2 -1
  47. package/controller/comms/messages/config/ValveMessage.ts +43 -26
  48. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +8 -7
  49. package/controller/comms/messages/status/EquipmentStateMessage.ts +93 -20
  50. package/controller/comms/messages/status/HeaterStateMessage.ts +24 -5
  51. package/controller/comms/messages/status/IntelliChemStateMessage.ts +7 -4
  52. package/controller/comms/messages/status/IntelliValveStateMessage.ts +2 -1
  53. package/controller/comms/messages/status/PumpStateMessage.ts +72 -4
  54. package/controller/comms/messages/status/VersionMessage.ts +2 -1
  55. package/controller/nixie/Nixie.ts +15 -4
  56. package/controller/nixie/NixieEquipment.ts +1 -0
  57. package/controller/nixie/chemistry/ChemController.ts +300 -129
  58. package/controller/nixie/chemistry/ChemDoser.ts +806 -0
  59. package/controller/nixie/chemistry/Chlorinator.ts +133 -129
  60. package/controller/nixie/circuits/Circuit.ts +171 -30
  61. package/controller/nixie/heaters/Heater.ts +337 -173
  62. package/controller/nixie/pumps/Pump.ts +264 -236
  63. package/controller/nixie/schedules/Schedule.ts +9 -3
  64. package/defaultConfig.json +45 -5
  65. package/logger/Logger.ts +38 -9
  66. package/package.json +13 -9
  67. package/web/Server.ts +235 -122
  68. package/web/bindings/aqualinkD.json +114 -59
  69. package/web/bindings/homeassistant.json +437 -0
  70. package/web/bindings/influxDB.json +15 -0
  71. package/web/bindings/mqtt.json +28 -9
  72. package/web/bindings/mqttAlt.json +15 -0
  73. package/web/interfaces/baseInterface.ts +58 -7
  74. package/web/interfaces/httpInterface.ts +5 -2
  75. package/web/interfaces/influxInterface.ts +9 -2
  76. package/web/interfaces/mqttInterface.ts +234 -74
  77. package/web/interfaces/ruleInterface.ts +87 -0
  78. package/web/services/config/Config.ts +140 -33
  79. package/web/services/config/ConfigSocket.ts +2 -1
  80. package/web/services/state/State.ts +144 -3
  81. package/web/services/state/StateSocket.ts +65 -14
  82. package/web/services/utilities/Utilities.ts +189 -1
@@ -1,5 +1,6 @@
1
1
  /* nodejs-poolController. An application to control pool equipment.
2
- Copyright (C) 2016, 2017, 2018, 2019, 2020. Russell Goldin, tagyoureit. russ.goldin@gmail.com
2
+ Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
+ Russell Goldin, tagyoureit. russ.goldin@gmail.com
3
4
 
4
5
  This program is free software: you can redistribute it and/or modify
5
6
  it under the terms of the GNU Affero General Public License as
@@ -28,7 +29,7 @@ export class StateSocket {
28
29
  await state.circuits.toggleCircuitStateAsync(parseInt(data.id, 10));
29
30
  // return res.status(200).send(cstate);
30
31
  }
31
- catch (err) {logger.error(err);}
32
+ catch (err) {logger.error(`Socket processing error /state/circuit/toggleState: ${err.message}`);}
32
33
  });
33
34
  sock.on('/state/body/heatMode', async (data: any) => {
34
35
  // RKS: 06-24-20 -- Changed this so that users can send in the body id, circuit id, or the name.
@@ -52,7 +53,8 @@ export class StateSocket {
52
53
  }
53
54
  await sys.board.bodies.setHeatModeAsync(body, mode);
54
55
  // return res.status(200).send(tbody);
55
- } catch (err) { logger.error(err); }
56
+ }
57
+ catch (err) { logger.error(`Socket processing error /state/body/heatmode: ${err.message}`); }
56
58
  });
57
59
  sock.on('/state/body/setPoint', async (data: any) => {
58
60
  // RKS: 06-24-20 -- Changed this so that users can send in the body id, circuit id, or the name.
@@ -65,14 +67,14 @@ export class StateSocket {
65
67
  }
66
68
  await sys.board.bodies.setHeatSetpointAsync(body, parseInt(data.setPoint, 10));
67
69
  // return res.status(200).send(tbody);
68
- } catch (err) { logger.error(err); }
70
+ } catch (err) { logger.error(`Socket processing error /state/body/setPoint: ${err.message}`); }
69
71
  });
70
72
  sock.on('/temps', async (data: any) => {
71
73
  try {
72
74
  data = JSON.parse(data);
73
75
  await sys.board.system.setTempsAsync(data).catch(err => logger.error(err));
74
76
  }
75
- catch (err) { logger.error(err); }
77
+ catch (err) { logger.error(`Socket processing error /temps: ${err.message}`); }
76
78
  });
77
79
 
78
80
  sock.on('/chlorinator', async (data: any) => {
@@ -98,7 +100,7 @@ export class StateSocket {
98
100
  schlor.emitEquipmentChange();
99
101
  }
100
102
  }
101
- catch (err) { logger.error(err); }
103
+ catch (err) { logger.error(`Socket processing error /chlorinator: ${err.message}`); }
102
104
  });
103
105
  sock.on('/filter', async (data: any) => {
104
106
  try {
@@ -112,9 +114,7 @@ export class StateSocket {
112
114
  await sys.board.filters.setFilterPressure(filter.id, data.pressure, data.pressureUnits || pu.name);
113
115
  sfilter.emitEquipmentChange();
114
116
  }
115
-
116
-
117
- } catch (err) { logger.error(err); }
117
+ } catch (err) { logger.error(`Socket processing error /filter: ${err.message}`); }
118
118
  });
119
119
  sock.on('/chemController', async (data: any) => {
120
120
  try {
@@ -160,7 +160,25 @@ export class StateSocket {
160
160
  if (!isNaN(parseFloat(data.orpTank.capacity))) scontroller.orp.tank.capacity = controller.orp.tank.capacity = parseFloat(data.orpTank.capacity);
161
161
  if (typeof data.orpTank.units === 'string') scontroller.orp.tank.units = controller.orp.tank.units = data.orpTank.units;
162
162
  }
163
+ if (typeof data.acidPump !== 'undefined' || typeof data.orpPump !== 'undefined') {
164
+ let chem = typeof data.acidPump !== 'undefined' ? controller.ph : controller.orp;
165
+ let schem = typeof data.acidPump !== 'undefined' ? scontroller.ph : scontroller.orp;
166
+ let vals = typeof data.acidPump !== 'undefined' ? data.acidPump : data.orpPump;
167
+ let pump = chem.pump;
168
+ switch (sys.board.valueMaps.chemPumpTypes.getName(pump.type)) {
169
+ case 'ezo-pmp':
170
+ if (typeof vals.dispense !== 'undefined') {
171
+ pump.ratedFlow = typeof vals.dispense.ratedFlow === 'number' ? vals.dispense.ratedFlow : pump.ratedFlow;
172
+ }
173
+ if (typeof vals.tank !== 'undefined') {
174
+ if (!isNaN(parseFloat(vals.tank.level))) schem.tank.level = parseFloat(vals.tank.level);
175
+ if (!isNaN(parseFloat(vals.tank.capacity))) schem.tank.capacity = chem.tank.capacity = parseFloat(vals.tank.capacity);
176
+ if (typeof vals.tank.units === 'string') schem.tank.units = chem.tank.units = vals.tank.units;
177
+ }
178
+ break;
179
+ }
163
180
 
181
+ }
164
182
  // Need to build this out to include the type of controller. If this is REM Chem we
165
183
  // will send the whole rest of the nut over to it. Intellichem will only let us
166
184
  // set specific values.
@@ -169,7 +187,7 @@ export class StateSocket {
169
187
  }
170
188
  }
171
189
  }
172
- catch (err) { logger.error(err); }
190
+ catch (err) { logger.error(`Socket processing error /chemController: ${err.message}`); }
173
191
  });
174
192
  sock.on('/circuit', async (data: any) => {
175
193
  try {
@@ -189,7 +207,7 @@ export class StateSocket {
189
207
  await sys.board.features.setFeatureStateAsync(id, utils.makeBool(data.isOn || typeof data.state));
190
208
  }
191
209
  }
192
- catch (err) { logger.error(err); }
210
+ catch (err) { logger.error(`Socket processing error /feature: ${err.message}`); }
193
211
  });
194
212
  sock.on('/circuitGroup', async (data: any) => {
195
213
  try {
@@ -199,7 +217,7 @@ export class StateSocket {
199
217
  await sys.board.circuits.setCircuitGroupStateAsync(id, utils.makeBool(data.isOn || typeof data.state));
200
218
  }
201
219
  }
202
- catch (err) { logger.error(err); }
220
+ catch (err) { logger.error(`Socket processing error /circuitGroup: ${err.message}`); }
203
221
  });
204
222
  sock.on('/lightGroup', async (data: any) => {
205
223
  try {
@@ -210,9 +228,42 @@ export class StateSocket {
210
228
  }
211
229
  if (!isNaN(id) && typeof data.theme !== 'undefined') await sys.board.circuits.setLightGroupThemeAsync(id, data.theme);
212
230
  }
213
- catch (err) { logger.error(err); }
231
+ catch (err) { logger.error(`Socket processing error /lightGroup: ${err.message}`); }
232
+ });
233
+ sock.on('/panelMode', async (data: any) => {
234
+ try {
235
+ data = JSON.parse(data);
236
+ let obj = {};
237
+ if (typeof data.isOn !== 'undefined' && !utils.makeBool(data.isOn)) return; // This is just in case it is sent by REM for a toggle button.
238
+ if (typeof data.timeout !== 'undefined' && !isNaN(data.timeout) && data.timeout) {
239
+ switch (data.timeUnits.toLowerCase()) {
240
+ case 'min':
241
+ case 'mins':
242
+ case 'm':
243
+ case 'minute':
244
+ case 'minutes':
245
+ data.timeout = data.timeout * 60;
246
+ break;
247
+ case 'hr':
248
+ case 'hrs':
249
+ case 'h':
250
+ case 'hour':
251
+ data.timeout = data.timeout * 3600;
252
+ break;
253
+ }
254
+ }
255
+ if (typeof data.mode === 'undefined' || data.mode === 'toggle') {
256
+ if (state.mode === 0) {
257
+ if (typeof data.timeout !== 'undefined' && !isNaN(data.timeout) && data.timeout)
258
+ data.mode = 'timeout';
259
+ else data.mode = 'service';
260
+ await sys.board.system.setPanelModeAsync(data);
261
+ }
262
+ else sys.board.system.setPanelModeAsync({ mode: 'auto' });
263
+ }
264
+ else await sys.board.system.setPanelModeAsync(data);
265
+ } catch (err) { logger.error(`Socket processing error /panelMode: ${err.message}`); }
214
266
  });
215
-
216
267
  /*
217
268
  app.get('/state/chemController/:id', (req, res) => {
218
269
  res.status(200).send(state.chemControllers.getItemById(parseInt(req.params.id, 10)).getExtended());
@@ -1,5 +1,6 @@
1
1
  /* nodejs-poolController. An application to control pool equipment.
2
- Copyright (C) 2016, 2017, 2018, 2019, 2020. Russell Goldin, tagyoureit. russ.goldin@gmail.com
2
+ Copyright (C) 2016, 2017, 2018, 2019, 2020, 2021, 2022.
3
+ Russell Goldin, tagyoureit. russ.goldin@gmail.com
3
4
 
4
5
  This program is free software: you can redistribute it and/or modify
5
6
  it under the terms of the GNU Affero General Public License as
@@ -14,11 +15,19 @@ GNU Affero General Public License for more details.
14
15
  You should have received a copy of the GNU Affero General Public License
15
16
  along with this program. If not, see <http://www.gnu.org/licenses/>.
16
17
  */
18
+ import * as path from "path";
19
+ import * as fs from "fs";
20
+ import * as multer from 'multer';
17
21
  import * as express from 'express';
18
22
  import { SsdpServer} from '../../Server';
19
23
  import { state } from "../../../controller/State";
20
24
  import { sys } from "../../../controller/Equipment";
21
25
  import { webApp } from "../../Server";
26
+ import { config } from "../../../config/Config";
27
+ import { logger } from "../../../logger/Logger";
28
+ import { ServiceParameterError, ServiceProcessError } from "../../../controller/Errors";
29
+ import { BindingsFile } from "../../interfaces/baseInterface";
30
+ import { Utils, utils } from "../../../controller/Constants";
22
31
  const extend = require("extend");
23
32
  export class UtilitiesRoute {
24
33
 
@@ -41,5 +50,184 @@ export class UtilitiesRoute {
41
50
  }
42
51
  return res.status(200).send(arr);
43
52
  });
53
+ app.put('/app/interfaces/add', async (req, res, next) => {
54
+ try {
55
+ let faces = config.getSection('web.interfaces');
56
+ let opts: any = {};
57
+ switch (req.body.type) {
58
+ case 'rule':
59
+ opts = {};
60
+ break;
61
+ case 'rem':
62
+ opts = {
63
+ options: { protocol: 'http://', host: '', port: 8080, headers: { "content-type": "application/json" } },
64
+ socket: {
65
+ transports: ['websocket'], allowEIO3: true, upgrade: false,
66
+ reconnectionDelay: 2000, reconnection: true, reconnectionDelayMax: 20000
67
+ }
68
+ }
69
+ break;
70
+ case 'http':
71
+ case 'rest':
72
+ opts = {
73
+ options: { protocol: 'http://', host: '', port: 80 }
74
+ }
75
+ break;
76
+ case 'influx':
77
+ case 'influxdb':
78
+ opts = {
79
+ options: {
80
+ version: 1,
81
+ protocol: 'http',
82
+ database: 'pool',
83
+ port: 8601,
84
+ retentionPolicy: 'autogen'
85
+ }
86
+ }
87
+ break;
88
+ case 'influxdb2':
89
+ opts = {
90
+ options: {
91
+ version: 2,
92
+ protocol: 'http',
93
+ port: 9999,
94
+ database: 'pool',
95
+ bucket: '57ec4eed2d90a50b',
96
+ token: '...LuyM84JJx93Qvc7tfaXPbI_mFFjRBjaA==',
97
+ org: 'njsPC-org'
98
+ }
99
+ }
100
+ break;
101
+ case 'mqtt':
102
+ opts = {
103
+ options: {
104
+ protocol: 'mqtt://', host: '', port: 1883, username: '', password: '',
105
+ selfSignedCertificate: false,
106
+ rootTopic: "pool/@bind=(state.equipment.model).replace(' ','-').replace(' / ','').toLowerCase();",
107
+ retain: true, qos: 0, changesOnly: true
108
+ }
109
+ }
110
+ break;
111
+ default:
112
+ return Promise.reject(new ServiceParameterError(`An invalid type was specified ${req.body.type}`, 'PUT: /app/interfaces/add', 'type', req.body.type));
113
+ }
114
+ opts.uuid = utils.uuid();
115
+ opts.isCustom = true;
116
+ // Now lets create a name for the element. This would have been much easier if it were an array but alas we are stuck.
117
+ let name = req.body.name.replace(/^[^a-zA-Z_$]|[^0-9a-zA-Z_$]/g, '_');
118
+ if (name.length === 0) return Promise.reject(new ServiceParameterError(`An invalid name was specified ${req.body.name}`, 'PUT: /app/interfaces/add', 'name', req.body.name));
119
+ if (name.charAt(0) >= '0' && name.charAt(0) <= '9') name = 'i' + name;
120
+ let fnEnsureUnique = (name, ord?: number): string => {
121
+ let isUnique = true;
122
+ for (let fname in faces) {
123
+ if (fname === (typeof ord !== 'undefined' ? `${name}_${ord}` : name)) {
124
+ isUnique = false;
125
+ break;
126
+ }
127
+ }
128
+ if (!isUnique) name = fnEnsureUnique(name, typeof ord === 'undefined' ? 0 : ord++);
129
+ return typeof ord !== 'undefined' ? `${name}_${ord}` : name;
130
+ }
131
+ name = fnEnsureUnique(name);
132
+ opts = extend(true, {}, opts, req.body);
133
+ config.setSection(`web.interfaces.${name}`, opts);
134
+ res.status(200).send({ id: name, opts: opts });
135
+ } catch (err) { next(err); }
136
+ });
137
+ app.delete('/app/interface', async (req, res, next) => {
138
+ try {
139
+ let faces = config.getSection('web.interfaces');
140
+ let deleted;
141
+ for (let fname in faces) {
142
+ let iface = faces[fname];
143
+ if (typeof req.body.id !== 'undefined') {
144
+ if (fname === req.body.id) {
145
+ deleted = iface;
146
+ iface.enabled = false;
147
+ await webApp.updateServerInterface(iface);
148
+ config.removeSection(`web.interfaces.${fname}`);
149
+ }
150
+ }
151
+ else if (typeof req.body.uuid !== 'undefined') {
152
+ if (req.body.uuid.toLowerCase() === iface.uuid.toLowerCase()) {
153
+ deleted = iface;
154
+ iface.enabled = false;
155
+ await webApp.updateServerInterface(iface);
156
+ config.removeSection(`web.interfaces.${fname}`);
157
+ }
158
+ }
159
+ }
160
+ return res.status(200).send(deleted);
161
+ } catch (err) { next(err); }
162
+ });
163
+ app.get('/app/options/interfaces', async (req, res, next) => {
164
+ try {
165
+ // todo: move bytevaluemaps out to a proper location; add additional definitions
166
+ let opts = {
167
+ interfaces: config.getSection('web.interfaces'),
168
+ types: [
169
+ { name: 'rule', desc: 'Rule', hasBindings: true, hasUrl: false },
170
+ { name: 'rest', desc: 'Rest', hasBindings: true },
171
+ { name: 'http', desc: 'Http', hasBindings: true },
172
+ { name: 'rem', desc: 'Relay Equipment Manager', hasBindings: false },
173
+ { name: 'mqtt', desc: 'MQTT', hasBindings: true },
174
+ { name: 'influx', desc: 'InfluxDB', hasBindings: true },
175
+ { name: 'influxdb2', desc: 'InfluxDB2', hasBindings: true}
176
+ ],
177
+ protocols: [
178
+ { val: 0, name: 'http://', desc: 'http://' },
179
+ { val: 1, name: 'https://', desc: 'https://' },
180
+ { val: 2, name: 'mqtt://', desc: 'mqtt://' }
181
+ ],
182
+ files: []
183
+ }
184
+ // Read all the files in the custom bindings directory.
185
+ let cpath = path.posix.join(process.cwd(), '/web/bindings/custom/');
186
+ let files = fs.readdirSync(cpath);
187
+ for (let i = 0; i < files.length; i++) {
188
+ if (path.extname(files[i]) === '.json') {
189
+ let bf = await BindingsFile.fromFile(path.posix.join(process.cwd(), 'web/bindings/'), `custom/${files[i]}`);
190
+ if (typeof bf !== 'undefined') opts.files.push(bf.options);
191
+ }
192
+ }
193
+ return res.status(200).send(opts);
194
+ } catch (err) { next(err); }
195
+ });
196
+ app.post('/app/interfaceBindings/file', async (req, res, next) => {
197
+ try {
198
+ let file = multer({
199
+ limits: { fileSize: 1000000 },
200
+ storage: multer.memoryStorage()
201
+ }).single('bindingsFile');
202
+ file(req, res, async (err) => {
203
+ try {
204
+ if (err) { next(err); }
205
+ else {
206
+ // Validate the incoming data and save it off only if it is valid.
207
+ let bf = await BindingsFile.fromBuffer(req.file.originalname, req.file.buffer);
208
+ if (typeof bf === 'undefined') {
209
+ err = new ServiceProcessError(`Invalid bindings file: ${req.file.originalname}`, 'POST: app/bindings/file', 'extractBindingOptions');
210
+ next(err);
211
+ }
212
+ else {
213
+ if (fs.existsSync(bf.filePath))
214
+ return next(new ServiceProcessError(`File already exists ${req.file.originalname}`, 'POST: app/bindings/file', 'writeFile'));
215
+ else {
216
+ try {
217
+ fs.writeFileSync(bf.filePath, req.file.buffer);
218
+ } catch (e) { logger.error(`Error writing bindings file ${e.message}`); }
219
+ }
220
+ return res.status(200).send(bf);
221
+ }
222
+ }
223
+ } catch (e) {
224
+ err = new ServiceProcessError(`Error uploading file: ${e.message}`, 'POST: app/backup/file', 'uploadFile');
225
+ next(err);
226
+ logger.error(e);
227
+ }
228
+ });
229
+ } catch (err) { next(err); }
230
+ });
231
+
44
232
  }
45
233
  }