nodejs-poolcontroller 7.2.0 → 7.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/Changelog +13 -0
  3. package/Dockerfile +1 -0
  4. package/README.md +5 -5
  5. package/app.ts +11 -0
  6. package/config/Config.ts +3 -0
  7. package/config/VersionCheck.ts +8 -4
  8. package/controller/Constants.ts +165 -9
  9. package/controller/Equipment.ts +186 -65
  10. package/controller/Errors.ts +22 -1
  11. package/controller/State.ts +273 -57
  12. package/controller/boards/EasyTouchBoard.ts +194 -95
  13. package/controller/boards/IntelliCenterBoard.ts +115 -42
  14. package/controller/boards/IntelliTouchBoard.ts +104 -30
  15. package/controller/boards/NixieBoard.ts +155 -53
  16. package/controller/boards/SystemBoard.ts +1529 -514
  17. package/controller/comms/Comms.ts +219 -42
  18. package/controller/comms/messages/Messages.ts +16 -4
  19. package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -3
  20. package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
  21. package/controller/comms/messages/config/CircuitMessage.ts +1 -1
  22. package/controller/comms/messages/config/CoverMessage.ts +1 -0
  23. package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
  24. package/controller/comms/messages/config/ExternalMessage.ts +43 -25
  25. package/controller/comms/messages/config/FeatureMessage.ts +8 -1
  26. package/controller/comms/messages/config/GeneralMessage.ts +8 -0
  27. package/controller/comms/messages/config/HeaterMessage.ts +15 -9
  28. package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
  29. package/controller/comms/messages/config/OptionsMessage.ts +13 -1
  30. package/controller/comms/messages/config/PumpMessage.ts +4 -20
  31. package/controller/comms/messages/config/RemoteMessage.ts +4 -0
  32. package/controller/comms/messages/config/ScheduleMessage.ts +11 -0
  33. package/controller/comms/messages/config/SecurityMessage.ts +1 -0
  34. package/controller/comms/messages/config/ValveMessage.ts +12 -2
  35. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +14 -6
  36. package/controller/comms/messages/status/EquipmentStateMessage.ts +78 -24
  37. package/controller/comms/messages/status/HeaterStateMessage.ts +25 -5
  38. package/controller/comms/messages/status/IntelliChemStateMessage.ts +55 -26
  39. package/controller/nixie/Nixie.ts +18 -16
  40. package/controller/nixie/NixieEquipment.ts +6 -6
  41. package/controller/nixie/bodies/Body.ts +7 -4
  42. package/controller/nixie/bodies/Filter.ts +7 -4
  43. package/controller/nixie/chemistry/ChemController.ts +800 -283
  44. package/controller/nixie/chemistry/Chlorinator.ts +22 -14
  45. package/controller/nixie/circuits/Circuit.ts +42 -7
  46. package/controller/nixie/heaters/Heater.ts +303 -30
  47. package/controller/nixie/pumps/Pump.ts +57 -30
  48. package/controller/nixie/schedules/Schedule.ts +10 -7
  49. package/controller/nixie/valves/Valve.ts +7 -5
  50. package/defaultConfig.json +32 -1
  51. package/issue_template.md +1 -1
  52. package/logger/DataLogger.ts +37 -22
  53. package/package.json +20 -18
  54. package/web/Server.ts +529 -31
  55. package/web/bindings/influxDB.json +157 -5
  56. package/web/bindings/mqtt.json +112 -13
  57. package/web/bindings/mqttAlt.json +109 -11
  58. package/web/interfaces/baseInterface.ts +2 -1
  59. package/web/interfaces/httpInterface.ts +2 -0
  60. package/web/interfaces/influxInterface.ts +103 -54
  61. package/web/interfaces/mqttInterface.ts +16 -5
  62. package/web/services/config/Config.ts +179 -43
  63. package/web/services/state/State.ts +51 -5
  64. package/web/services/state/StateSocket.ts +19 -2
@@ -17,7 +17,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
17
17
 
18
18
  import extend = require("extend");
19
19
  import { ClientOptions, InfluxDB, Point, WriteApi, WritePrecisionType } from '@influxdata/influxdb-client';
20
- import { utils } from '../../controller/Constants';
20
+ import { utils, Timestamp } from '../../controller/Constants';
21
21
  import { logger } from "../../logger/Logger";
22
22
  import { BaseInterfaceBindings, InterfaceContext, InterfaceEvent } from "./baseInterface";
23
23
  export class InfluxInterfaceBindings extends BaseInterfaceBindings {
@@ -25,33 +25,46 @@ export class InfluxInterfaceBindings extends BaseInterfaceBindings {
25
25
  super(cfg);
26
26
  }
27
27
  private writeApi: WriteApi;
28
- public context: InterfaceContext;
29
- public cfg;
30
- public events: InfluxInterfaceEvent[];
28
+ declare context: InterfaceContext;
29
+ declare cfg;
30
+ declare events: InfluxInterfaceEvent[];
31
31
  private init = () => {
32
32
  let baseOpts = extend(true, this.cfg.options, this.context.options);
33
+ let url = 'http://';
34
+ if (typeof baseOpts.protocol !== 'undefined' && baseOpts.protocol) url = baseOpts.protocol;
35
+ if (!url.endsWith('://')) url += '://';
36
+ url = `${url}${baseOpts.host}:${baseOpts.port}`;
37
+ let influxDB: InfluxDB;
38
+ let bucket;
39
+ let org;
33
40
  if (typeof baseOpts.host === 'undefined' || !baseOpts.host) {
34
41
  logger.warn(`Interface: ${this.cfg.name} has not resolved to a valid host.`);
35
42
  return;
36
43
  }
37
- if (typeof baseOpts.database === 'undefined' || !baseOpts.database) {
38
- logger.warn(`Interface: ${this.cfg.name} has not resolved to a valid database.`);
39
- return;
44
+ if (baseOpts.version === 1) {
45
+ if (typeof baseOpts.database === 'undefined' || !baseOpts.database) {
46
+ logger.warn(`Interface: ${this.cfg.name} has not resolved to a valid database.`);
47
+ return;
48
+ }
49
+ bucket = `${baseOpts.database}/${baseOpts.retentionPolicy}`;
50
+ const clientOptions: ClientOptions = {
51
+ url,
52
+ token: `${baseOpts.username}:${baseOpts.password}`,
53
+ }
54
+ influxDB = new InfluxDB(clientOptions);
40
55
  }
41
- // let opts = extend(true, baseOpts, e.options);
42
- let url = 'http';
43
- if (typeof baseOpts.protocol !== 'undefined' && baseOpts.protocol) url = baseOpts.protocol;
44
- url = `${url}://${baseOpts.host}:${baseOpts.port}`;
45
- // TODO: add username/password
46
- const bucket = `${baseOpts.database}/${baseOpts.retentionPolicy}`;
47
- const clientOptions: ClientOptions = {
48
- url,
49
- token: `${baseOpts.username}:${baseOpts.password}`,
56
+ else if (baseOpts.version === 2) {
57
+ org = baseOpts.org;
58
+ bucket = baseOpts.bucket;
59
+ const clientOptions: ClientOptions = {
60
+ url,
61
+ token: baseOpts.token,
62
+ }
63
+ influxDB = new InfluxDB(clientOptions);
50
64
  }
51
- const influxDB = new InfluxDB(clientOptions);
52
- this.writeApi = influxDB.getWriteApi('', bucket, 'ms' as WritePrecisionType);
53
-
54
-
65
+ this.writeApi = influxDB.getWriteApi(org, bucket, 'ms');
66
+
67
+
55
68
  // set global tags from context
56
69
  let baseTags = {}
57
70
  baseOpts.tags.forEach(tag => {
@@ -79,7 +92,7 @@ export class InfluxInterfaceBindings extends BaseInterfaceBindings {
79
92
  this.buildTokens(e.filter, evt, toks, e, data[0]);
80
93
  if (eval(this.replaceTokens(e.filter, toks)) === false) continue;
81
94
  }
82
- for (let j = 0; j < e.points.length; j++){
95
+ for (let j = 0; j < e.points.length; j++) {
83
96
  let _point = e.points[j];
84
97
  // Figure out whether we need to check the filter for each point.
85
98
  if (typeof _point.filter !== 'undefined') {
@@ -105,40 +118,68 @@ export class InfluxInterfaceBindings extends BaseInterfaceBindings {
105
118
  }
106
119
  })
107
120
  _point.fields.forEach(_field => {
108
- let sname = _field.name;
109
- this.buildTokens(sname, evt, toks, e, data[0]);
110
- //console.log(toks);
111
- sname = this.replaceTokens(sname, toks);
112
- let svalue = _field.value;
113
- this.buildTokens(svalue, evt, toks, e, data[0]);
114
- svalue = this.replaceTokens(svalue, toks);
115
- if (typeof sname !== 'undefined' && typeof svalue !== 'undefined' && !sname.includes('@bind') && !svalue.includes('@bind') && svalue !== null)
116
- switch (_field.type) {
117
- case 'int':
118
- case 'integer':
119
- let int = parseInt(svalue, 10);
120
- if (!isNaN(int)) point.intField(sname, int);
121
- // if (!isNaN(int) && typeof _point.storePrevState !== 'undefined' && _point.storePrevState) point2.intField(sname, int);
122
- break;
123
- case 'string':
124
- point.stringField(sname, svalue);
125
- // if (typeof _point.storePrevState !== 'undefined' && _point.storePrevState) point2.stringField(sname, svalue);
126
- break;
127
- case 'boolean':
128
- point.booleanField(sname, utils.makeBool(svalue));
129
- if (typeof _point.storePrevState !== 'undefined' && _point.storePrevState) point2.booleanField(sname, !utils.makeBool(svalue));
130
- break;
131
- case 'float':
132
- let float = parseFloat(svalue);
133
- if (!isNaN(float)) point.floatField(sname, float);
134
- // if (!isNaN(float) && typeof _point.storePrevState !== 'undefined' && _point.storePrevState) point2.intField(sname, int);
135
- break;
121
+ try {
122
+ let sname = _field.name;
123
+ this.buildTokens(sname, evt, toks, e, data[0]);
124
+ //console.log(toks);
125
+ sname = this.replaceTokens(sname, toks);
126
+ let svalue = _field.value;
127
+ this.buildTokens(svalue, evt, toks, e, data[0]);
128
+ svalue = this.replaceTokens(svalue, toks);
129
+ if (typeof sname !== 'undefined' && typeof svalue !== 'undefined' && !sname.includes('@bind') && !svalue.includes('@bind') && svalue !== null)
130
+ switch (_field.type) {
131
+ case 'int':
132
+ case 'integer':
133
+ let int = parseInt(svalue, 10);
134
+ if (!isNaN(int)) point.intField(sname, int);
135
+ // if (!isNaN(int) && typeof _point.storePrevState !== 'undefined' && _point.storePrevState) point2.intField(sname, int);
136
+ break;
137
+ case 'string':
138
+ point.stringField(sname, svalue);
139
+ // if (typeof _point.storePrevState !== 'undefined' && _point.storePrevState) point2.stringField(sname, svalue);
140
+ break;
141
+ case 'boolean':
142
+ point.booleanField(sname, utils.makeBool(svalue));
143
+ if (typeof _point.storePrevState !== 'undefined' && _point.storePrevState) point2.booleanField(sname, !utils.makeBool(svalue));
144
+ break;
145
+ case 'float':
146
+ let float = parseFloat(svalue);
147
+ if (!isNaN(float)) point.floatField(sname, float);
148
+ // if (!isNaN(float) && typeof _point.storePrevState !== 'undefined' && _point.storePrevState) point2.intField(sname, int);
149
+ break;
150
+ case 'timestamp':
151
+ case 'datetime':
152
+ case 'date':
153
+ //let dt = Date.parse(svalue.replace(/^["'](.+(?=["']$))["']$/, '$1'));
154
+ // RKS: 07-06-21 - I think this is missing the eval function around all of this. The strings still have the quotes around them. I think
155
+ // maybe we need to create a closure and execute it as a code segment for variable data.
156
+ let sdt = eval(svalue);
157
+ if (sdt !== null && typeof sdt !== 'undefined') {
158
+ let dt = Date.parse(sdt);
159
+ if (!isNaN(dt)) point.intField(sname, dt);
160
+ else if (svalue !== '') logger.warn(`Influx error parsing date from ${sname}: ${svalue}`);
161
+ }
162
+ break;
163
+ }
164
+ else {
165
+ logger.error(`InfluxDB point binding failure on ${evt}:${_field.name}/${_field.value} --> ${svalue || 'undefined'}`);
136
166
  }
137
- else {
138
- logger.error(`InfluxDB point binding failure on ${evt}:${_field.name}/${_field.value} --> ${svalue || 'undefined'}`);
139
- }
140
- })
141
- point.timestamp(new Date());
167
+ } catch (err) { logger.error(`Error binding InfluxDB point fields ${err.message}`); }
168
+ });
169
+ if (typeof _point.series !== 'undefined') {
170
+ try {
171
+ this.buildTokens(_point.series.value, evt, toks, e, data[0]);
172
+ let ser = eval(this.replaceTokens(_point.series.value, toks));
173
+ let ts = Date.parse(ser);
174
+ if (isNaN(ts)) {
175
+ logger.error(`Influx series timestamp is invalid ${ser}`);
176
+ }
177
+ else
178
+ point.timestamp(new Date(ts));
179
+ } catch (err) { logger.error(`Error parsing Influx point series for ${evt} - ${_point.series.value}`); }
180
+ }
181
+ else
182
+ point.timestamp(new Date());
142
183
  try {
143
184
 
144
185
  if (typeof _point.storePrevState !== 'undefined' && _point.storePrevState) {
@@ -147,10 +188,14 @@ export class InfluxInterfaceBindings extends BaseInterfaceBindings {
147
188
  let sec = ts.getSeconds() - 1;
148
189
  ts.setSeconds(sec);
149
190
  point2.timestamp(ts);
191
+ logger.silly(`Writing influx ${e.name} inverse data point ${point2.toString()})`)
150
192
  this.writeApi.writePoint(point2);
151
193
  }
152
194
  if (typeof point.toLineProtocol() !== 'undefined') {
195
+ logger.silly(`Writing influx ${e.name} data point ${point.toString()}`)
153
196
  this.writeApi.writePoint(point);
197
+ this.writeApi.flush()
198
+ .catch(error => { logger.error(error); });
154
199
  //logger.info(`INFLUX: ${point.toLineProtocol()}`)
155
200
  }
156
201
  else {
@@ -176,6 +221,7 @@ class InfluxInterfaceEvent extends InterfaceEvent {
176
221
 
177
222
  export interface IPoint {
178
223
  measurement: string;
224
+ series?: ISeries;
179
225
  tags: ITag[];
180
226
  fields: IFields[];
181
227
  storePrevState?: boolean;
@@ -190,3 +236,6 @@ export interface IFields {
190
236
  value: string;
191
237
  type: string;
192
238
  }
239
+ export interface ISeries {
240
+ value: string;
241
+ }
@@ -36,7 +36,7 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
36
36
  }
37
37
  private client: MqttClient;
38
38
  private topics: string[] = [];
39
- public events: MqttInterfaceEvent[];
39
+ declare events: MqttInterfaceEvent[];
40
40
  private subscribed: boolean; // subscribed to events or not
41
41
  private sentInitialMessages = false;
42
42
  private init = () => {
@@ -260,7 +260,7 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
260
260
  if (typeof t.lastSent === 'undefined') t.lastSent = [];
261
261
  let lm = t.lastSent.find(elem => elem.topic === topic);
262
262
  if (typeof lm === 'undefined' || lm.message !== message) {
263
- this.client.publish(topic, message, publishOptions);
263
+ setImmediate(() => { this.client.publish(topic, message, publishOptions); });
264
264
  logger.silly(`MQTT send:\ntopic: ${topic}\nmessage: ${message}\nopts:${JSON.stringify(publishOptions)}`);
265
265
  }
266
266
  if (typeof lm === 'undefined') t.lastSent.push({ topic: topic, message: message });
@@ -269,7 +269,7 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
269
269
  }
270
270
  else {
271
271
  logger.silly(`MQTT send:\ntopic: ${topic}\nmessage: ${message}\nopts:${JSON.stringify(publishOptions)}`);
272
- this.client.publish(topic, message, publishOptions);
272
+ setImmediate(() => { this.client.publish(topic, message, publishOptions); });
273
273
  if (typeof t.lastSent !== 'undefined') t.lastSent = undefined;
274
274
  }
275
275
 
@@ -370,7 +370,18 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
370
370
  logger.error(new ServiceParameterError(`Cannot set body setPoint. You must supply a valid id, circuit, name, or type for the body`, 'body', 'id', msg.id));
371
371
  return;
372
372
  }
373
- let tbody = await sys.board.bodies.setHeatSetpointAsync(body, parseInt(msg.setPoint, 10));
373
+ if (typeof msg.setPoint !== 'undefined' || typeof msg.heatSetpoint !== 'undefined') {
374
+ let setPoint = parseInt(msg.setPoint, 10) || parseInt(msg.heatSetpoint, 10);
375
+ if (!isNaN(setPoint)) {
376
+ await sys.board.bodies.setHeatSetpointAsync(body, setPoint);
377
+ }
378
+ }
379
+ if (typeof msg.coolSetpoint !== 'undefined') {
380
+ let setPoint = parseInt(msg.coolSetpoint, 10);
381
+ if (!isNaN(setPoint)) {
382
+ await sys.board.bodies.setCoolSetpointAsync(body, setPoint);
383
+ }
384
+ }
374
385
  }
375
386
  }
376
387
  catch (err) { logger.error(err); }
@@ -412,7 +423,7 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
412
423
  break;
413
424
  case 'settheme':
414
425
  try {
415
- let theme = await state.circuits.setLightThemeAsync(parseInt(msg.id, 10), parseInt(msg.theme, 10));
426
+ let theme = await state.circuits.setLightThemeAsync(parseInt(msg.id, 10), sys.board.valueMaps.lightThemes.encode(msg.theme));
416
427
  }
417
428
  catch (err) { logger.error(err); }
418
429
  break;
@@ -14,16 +14,21 @@ GNU Affero General Public License for more details.
14
14
  You should have received a copy of the GNU Affero General Public License
15
15
  along with this program. If not, see <http://www.gnu.org/licenses/>.
16
16
  */
17
+ import * as fs from "fs";
17
18
  import * as express from "express";
18
19
  import * as extend from 'extend';
20
+ import * as multer from 'multer';
21
+ import * as path from "path";
19
22
  import { sys, LightGroup, ControllerType, Pump, Valve, Body, General, Circuit, ICircuit, Feature, CircuitGroup, CustomNameCollection, Schedule, Chlorinator, Heater } from "../../../controller/Equipment";
20
23
  import { config } from "../../../config/Config";
21
24
  import { logger } from "../../../logger/Logger";
22
25
  import { utils } from "../../../controller/Constants";
26
+ import { ServiceProcessError } from "../../../controller/Errors";
23
27
  import { state } from "../../../controller/State";
24
28
  import { stopPacketCaptureAsync, startPacketCapture } from '../../../app';
25
29
  import { conn } from "../../../controller/comms/Comms";
26
- import { webApp } from "../../Server";
30
+ import { webApp, BackupFile, RestoreFile } from "../../Server";
31
+ import { release } from "os";
27
32
 
28
33
  export class ConfigRoute {
29
34
  public static initRoutes(app: express.Application) {
@@ -52,14 +57,15 @@ export class ConfigRoute {
52
57
  clockSources: sys.board.valueMaps.clockSources.toArray(),
53
58
  clockModes: sys.board.valueMaps.clockModes.toArray(),
54
59
  pool: sys.general.get(true),
55
- sensors: sys.board.system.getSensors()
60
+ sensors: sys.board.system.getSensors(),
61
+ systemUnits: sys.board.valueMaps.systemUnits.toArray()
56
62
  };
57
63
  return res.status(200).send(opts);
58
64
  });
59
65
  app.get('/config/options/rs485', (req, res) => {
60
66
  let opts = {
61
- port: config.getSection('comms', { enabled: false, netConnect: false }),
62
- portStatus: conn.buffer.counter
67
+ port: config.getSection('controller.comms', { enabled: false, netConnect: false }),
68
+ stats: conn.buffer.counter
63
69
  };
64
70
  return res.status(200).send(opts);
65
71
  });
@@ -115,7 +121,8 @@ export class ConfigRoute {
115
121
  let opts = {
116
122
  maxBodies: sys.equipment.maxBodies,
117
123
  bodyTypes: sys.board.valueMaps.bodies.toArray(),
118
- bodies: sys.bodies.get()
124
+ bodies: sys.bodies.get(),
125
+ capacityUnits: sys.board.valueMaps.volumeUnits.toArray()
119
126
  };
120
127
  return res.status(200).send(opts);
121
128
  });
@@ -239,6 +246,7 @@ export class ConfigRoute {
239
246
  phSupplyTypes: sys.board.valueMaps.phSupplyTypes.toArray(),
240
247
  volumeUnits: sys.board.valueMaps.volumeUnits.toArray(),
241
248
  dosingMethods: sys.board.valueMaps.chemDosingMethods.toArray(),
249
+ chlorDosingMethods: sys.board.valueMaps.chemChlorDosingMethods.toArray(),
242
250
  orpProbeTypes: sys.board.valueMaps.chemORPProbeTypes.toArray(),
243
251
  phProbeTypes: sys.board.valueMaps.chemPhProbeTypes.toArray(),
244
252
  flowSensorTypes: sys.board.valueMaps.flowSensorTypes.toArray(),
@@ -280,7 +288,9 @@ export class ConfigRoute {
280
288
  types: sys.board.valueMaps.chlorinatorType.toArray(),
281
289
  bodies: sys.board.bodies.getBodyAssociations(),
282
290
  chlorinators: sys.chlorinators.get(),
283
- maxChlorinators: sys.equipment.maxChlorinators
291
+ maxChlorinators: sys.equipment.maxChlorinators,
292
+ models: sys.board.valueMaps.chlorinatorModel.toArray(),
293
+ equipmentMasters: sys.board.valueMaps.equipmentMaster.toArray()
284
294
  };
285
295
  return res.status(200).send(opts);
286
296
  });
@@ -305,13 +315,16 @@ export class ConfigRoute {
305
315
  let opts = {
306
316
  interfaces: config.getSection('web.interfaces'),
307
317
  types: [
308
- {name: 'rem', desc: 'Relay Equipment Manager'},
309
- {name: 'mqtt', desc: 'MQTT'}
318
+ { name: 'rest', desc: 'Rest' },
319
+ { name: 'http', desc: 'Http' },
320
+ { name: 'rem', desc: 'Relay Equipment Manager' },
321
+ { name: 'mqtt', desc: 'MQTT' },
322
+ { name: 'influx', desc: 'InfluxDB' }
310
323
  ],
311
324
  protocols: [
312
325
  { val: 0, name: 'http://', desc: 'http://' },
313
326
  { val: 1, name: 'https://', desc: 'https://' },
314
- { val: 2, name: 'mqtt://', desc: 'mqtt://' },
327
+ { val: 2, name: 'mqtt://', desc: 'mqtt://' }
315
328
  ]
316
329
  }
317
330
  return res.status(200).send(opts);
@@ -330,6 +343,8 @@ export class ConfigRoute {
330
343
  bodies: sys.board.bodies.getBodyAssociations(),
331
344
  filters: sys.filters.get(),
332
345
  areaUnits: sys.board.valueMaps.areaUnits.toArray(),
346
+ pressureUnits: sys.board.valueMaps.pressureUnits.toArray(),
347
+ circuits: sys.board.circuits.getCircuitReferences(true, true, true, false),
333
348
  servers: []
334
349
  };
335
350
  if (sys.controllerType === ControllerType.Nixie) opts.servers = await sys.ncp.getREMServers();
@@ -338,14 +353,14 @@ export class ConfigRoute {
338
353
  });
339
354
  /******* END OF CONFIGURATION PICK LISTS/REFERENCES AND VALIDATION ***********/
340
355
  /******* ENDPOINTS FOR MODIFYING THE OUTDOOR CONTROL PANEL SETTINGS **********/
341
- app.put('/config/rem', async (req, res, next)=>{
356
+ app.put('/config/rem', async (req, res, next) => {
342
357
  try {
343
358
  // RSG: this is problematic because we now enable multiple rem type interfaces that may not be called REM.
344
359
  // This is now also a dupe of PUT /app/interface and should be consolidated
345
360
  // config.setSection('web.interfaces.rem', req.body);
346
361
  config.setInterface(req.body);
347
362
  }
348
- catch (err) {next(err);}
363
+ catch (err) { next(err); }
349
364
  })
350
365
  app.put('/config/tempSensors', async (req, res, next) => {
351
366
  try {
@@ -360,7 +375,7 @@ export class ConfigRoute {
360
375
  });
361
376
  app.put('/config/filter', async (req, res, next) => {
362
377
  try {
363
- let sfilter = sys.board.filters.setFilter(req.body);
378
+ let sfilter = await sys.board.filters.setFilterAsync(req.body);
364
379
  return res.status(200).send(sfilter.get(true));
365
380
  }
366
381
  catch (err) { next(err); }
@@ -373,7 +388,7 @@ export class ConfigRoute {
373
388
  });
374
389
  app.delete('/config/filter', async (req, res, next) => {
375
390
  try {
376
- let sfilter = sys.board.filters.deleteFilter(req.body);
391
+ let sfilter = await sys.board.filters.deleteFilterAsync(req.body);
377
392
  return res.status(200).send(sfilter.get(true));
378
393
  }
379
394
  catch (err) { next(err); }
@@ -587,7 +602,7 @@ export class ConfigRoute {
587
602
  });
588
603
  app.get('/config/circuit/:id/lightThemes', (req, res) => {
589
604
  let circuit = sys.circuits.getInterfaceById(parseInt(req.params.id, 10));
590
- let themes = typeof circuit !== 'undefined' && typeof circuit.getLightThemes === 'function' ? circuit.getLightThemes() : [];
605
+ let themes = typeof circuit !== 'undefined' && typeof circuit.getLightThemes === 'function' ? circuit.getLightThemes(circuit.type) : [];
591
606
  return res.status(200).send(themes);
592
607
  });
593
608
  app.get('/config/chlorinator/:id', (req, res) => {
@@ -725,7 +740,7 @@ export class ConfigRoute {
725
740
  // RSG: is this and /config/circuit/:id/lightThemes both needed?
726
741
 
727
742
  // if (sys.controllerType === ControllerType.IntelliCenter) {
728
- let grp = sys.lightGroups.getItemById(parseInt(req.params.id, 10));
743
+ let grp = sys.lightGroups.getItemById(parseInt(req.body.id, 10));
729
744
  return res.status(200).send(grp.getLightThemes());
730
745
  // }
731
746
  // else
@@ -758,16 +773,16 @@ export class ConfigRoute {
758
773
  let grp = sys.circuitGroups.getItemById(parseInt(req.params.id, 10));
759
774
  return res.status(200).send(grp.getExtended());
760
775
  });
761
- /* app.get('/config/chemController/search', async (req, res, next) => {
762
- // Change the options for the pool.
763
- try {
764
- let result = await sys.board.virtualChemControllers.search();
765
- return res.status(200).send(result);
766
- }
767
- catch (err) {
768
- next(err);
769
- }
770
- }); */
776
+ /* app.get('/config/chemController/search', async (req, res, next) => {
777
+ // Change the options for the pool.
778
+ try {
779
+ let result = await sys.board.virtualChemControllers.search();
780
+ return res.status(200).send(result);
781
+ }
782
+ catch (err) {
783
+ next(err);
784
+ }
785
+ }); */
771
786
  app.put('/config/chemController', async (req, res, next) => {
772
787
  try {
773
788
  let chem = await sys.board.chemControllers.setChemControllerAsync(req.body);
@@ -790,17 +805,17 @@ export class ConfigRoute {
790
805
  catch (err) { next(err); }
791
806
 
792
807
  });
793
- /* app.get('/config/intellibrite', (req, res) => {
794
- return res.status(200).send(sys.intellibrite.getExtended());
795
- });
796
- app.get('/config/intellibrite/colors', (req, res) => {
797
- return res.status(200).send(sys.board.valueMaps.lightColors.toArray());
798
- });
799
- app.put('/config/intellibrite/setColors', (req, res) => {
800
- let grp = extend(true, { id: 0 }, req.body);
801
- sys.board.circuits.setIntelliBriteColors(new LightGroup(grp));
802
- return res.status(200).send('OK');
803
- }); */
808
+ /* app.get('/config/intellibrite', (req, res) => {
809
+ return res.status(200).send(sys.intellibrite.getExtended());
810
+ });
811
+ app.get('/config/intellibrite/colors', (req, res) => {
812
+ return res.status(200).send(sys.board.valueMaps.lightColors.toArray());
813
+ });
814
+ app.put('/config/intellibrite/setColors', (req, res) => {
815
+ let grp = extend(true, { id: 0 }, req.body);
816
+ sys.board.circuits.setIntelliBriteColors(new LightGroup(grp));
817
+ return res.status(200).send('OK');
818
+ }); */
804
819
  app.get('/config', (req, res) => {
805
820
  return res.status(200).send(sys.getSection('all'));
806
821
  });
@@ -826,11 +841,18 @@ export class ConfigRoute {
826
841
  return res.status(200).send('OK');
827
842
  });
828
843
  app.put('/app/interface', async (req, res, next) => {
829
- try{
830
- await webApp.updateServerInterface(req.body);
831
- return res.status(200).send('OK');
832
- }
833
- catch (err) {next(err);}
844
+ try {
845
+ let iface = await webApp.updateServerInterface(req.body);
846
+ return res.status(200).send(iface);
847
+ }
848
+ catch (err) { next(err); }
849
+ });
850
+ app.put('/app/rs485Port', async (req, res, next) => {
851
+ try {
852
+ let port = await conn.setPortAsync(req.body);
853
+ return res.status(200).send(port);
854
+ }
855
+ catch (err) { next(err); }
834
856
  });
835
857
  app.get('/app/config/startPacketCapture', (req, res) => {
836
858
  startPacketCapture(true);
@@ -840,15 +862,129 @@ export class ConfigRoute {
840
862
  startPacketCapture(false);
841
863
  return res.status(200).send('OK');
842
864
  });
843
- app.get('/app/config/stopPacketCapture', async (req, res,next) => {
865
+ app.get('/app/config/stopPacketCapture', async (req, res, next) => {
844
866
  try {
845
867
  let file = await stopPacketCaptureAsync();
846
868
  res.download(file);
847
869
  }
848
- catch (err) {next(err);}
870
+ catch (err) { next(err); }
849
871
  });
850
872
  app.get('/app/config/:section', (req, res) => {
851
873
  return res.status(200).send(config.getSection(req.params.section));
852
874
  });
875
+ app.get('/app/config/options/backup', async (req, res, next) => {
876
+ try {
877
+ let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0, keepCount: 5, servers: [] } });
878
+ let servers = await sys.ncp.getREMServers();
879
+ if (typeof servers !== 'undefined') {
880
+ // Just in case somebody deletes the backup section and doesn't put it back properly.
881
+ for (let i = 0; i < servers.length; i++) {
882
+ let srv = servers[i];
883
+ if (typeof opts.servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.push({ name: srv.name, uuid: srv.uuid, backup: false, host: srv.interface.options.host });
884
+ }
885
+ for (let i = opts.servers.length - 1; i >= 0; i--) {
886
+ let srv = opts.servers[i];
887
+ if (typeof servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.splice(i, 1);
888
+ }
889
+ }
890
+ if (typeof opts.servers === 'undefined') opts.servers = [];
891
+ return res.status(200).send(opts);
892
+ } catch (err) { next(err); }
893
+ });
894
+ app.get('/app/config/options/restore', async (req, res, next) => {
895
+ try {
896
+ let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0, keepCount: 5, servers: [], backupFiles: [] } });
897
+ let servers = await sys.ncp.getREMServers();
898
+ if (typeof servers !== 'undefined') {
899
+ for (let i = 0; i < servers.length; i++) {
900
+ let srv = servers[i];
901
+ if (typeof opts.servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.push({ name: srv.name, uuid: srv.uuid, backup: false });
902
+ }
903
+ for (let i = opts.servers.length - 1; i >= 0; i--) {
904
+ let srv = opts.servers[i];
905
+ if (typeof servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.splice(i, 1);
906
+ }
907
+ }
908
+ if (typeof opts.servers === 'undefined') opts.servers = [];
909
+ opts.backupFiles = await webApp.readBackupFiles();
910
+ return res.status(200).send(opts);
911
+ } catch (err) { next(err); }
912
+
913
+ });
914
+ app.put('/app/config/options/backup', async (req, res, next) => {
915
+ try {
916
+ config.setSection('controller.backups', req.body);
917
+ let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0, keepCount: 5, servers: [] } });
918
+ webApp.autoBackup = utils.makeBool(opts.automatic);
919
+ await webApp.checkAutoBackup();
920
+ return res.status(200).send(opts);
921
+ } catch (err) { next(err); }
922
+
923
+ });
924
+ app.put('/app/config/createBackup', async (req, res, next) => {
925
+ try {
926
+ let ret = await webApp.backupServer(req.body);
927
+ res.download(ret.filePath);
928
+ }
929
+ catch (err) { next(err); }
930
+ });
931
+ app.delete('/app/backup/file', async (req, res, next) => {
932
+ try {
933
+ let opts = req.body;
934
+ fs.unlinkSync(opts.filePath);
935
+ return res.status(200).send(opts);
936
+ }
937
+ catch (err) { next(err); }
938
+ });
939
+ app.post('/app/backup/file', async (req, res, next) => {
940
+ try {
941
+ let file = multer({
942
+ limits: { fileSize: 1000000 },
943
+ storage: multer.memoryStorage()
944
+ }).single('backupFile');
945
+ file(req, res, async (err) => {
946
+ try {
947
+ if (err) { next(err); }
948
+ else {
949
+ // Validate the incoming data and save it off only if it is valid.
950
+ let bf = await BackupFile.fromBuffer(req.file.originalname, req.file.buffer);
951
+ if (typeof bf === 'undefined') {
952
+ err = new ServiceProcessError(`Invalid backup file: ${req.file.originalname}`, 'POST: app/backup/file', 'extractBackupOptions');
953
+ next(err);
954
+ }
955
+ else {
956
+ if (fs.existsSync(bf.filePath))
957
+ return next(new ServiceProcessError(`File already exists ${req.file.originalname}`, 'POST: app/backup/file', 'writeFile'));
958
+ else {
959
+ try {
960
+ fs.writeFileSync(bf.filePath, req.file.buffer);
961
+ } catch (e) { logger.error(`Error writing backup file ${e.message}`); }
962
+ }
963
+ return res.status(200).send(bf);
964
+ }
965
+ }
966
+ } catch (e) {
967
+ err = new ServiceProcessError(`Error uploading file: ${e.message}`, 'POST: app/backup/file', 'uploadFile');
968
+ next(err);
969
+ logger.error(e);
970
+ }
971
+ });
972
+ } catch (err) { next(err); }
973
+ });
974
+ app.put('/app/restore/validate', async (req, res, next) => {
975
+ try {
976
+ // Validate all the restore options.
977
+ let opts = req.body;
978
+ let ctx = await webApp.validateRestore(opts);
979
+ return res.status(200).send(ctx);
980
+ } catch (err) { next(err); }
981
+ });
982
+ app.put('/app/restore/file', async (req, res, next) => {
983
+ try {
984
+ let opts = req.body;
985
+ let results = await webApp.restoreServers(opts);
986
+ return res.status(200).send(results);
987
+ } catch (err) { next(err); }
988
+ });
853
989
  }
854
990
  }