nodejs-poolcontroller 7.3.0 → 7.6.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 (60) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +1 -1
  2. package/Changelog +23 -0
  3. package/README.md +5 -5
  4. package/app.ts +2 -0
  5. package/config/Config.ts +3 -0
  6. package/config/VersionCheck.ts +8 -4
  7. package/controller/Constants.ts +88 -0
  8. package/controller/Equipment.ts +246 -66
  9. package/controller/Errors.ts +24 -1
  10. package/controller/Lockouts.ts +423 -0
  11. package/controller/State.ts +314 -54
  12. package/controller/boards/EasyTouchBoard.ts +107 -59
  13. package/controller/boards/IntelliCenterBoard.ts +186 -125
  14. package/controller/boards/IntelliTouchBoard.ts +104 -30
  15. package/controller/boards/NixieBoard.ts +721 -159
  16. package/controller/boards/SystemBoard.ts +2370 -1108
  17. package/controller/comms/Comms.ts +85 -10
  18. package/controller/comms/messages/Messages.ts +10 -4
  19. package/controller/comms/messages/config/ChlorinatorMessage.ts +13 -4
  20. package/controller/comms/messages/config/CircuitGroupMessage.ts +6 -0
  21. package/controller/comms/messages/config/CoverMessage.ts +1 -0
  22. package/controller/comms/messages/config/EquipmentMessage.ts +4 -0
  23. package/controller/comms/messages/config/ExternalMessage.ts +44 -26
  24. package/controller/comms/messages/config/FeatureMessage.ts +8 -1
  25. package/controller/comms/messages/config/GeneralMessage.ts +8 -0
  26. package/controller/comms/messages/config/HeaterMessage.ts +15 -9
  27. package/controller/comms/messages/config/IntellichemMessage.ts +4 -1
  28. package/controller/comms/messages/config/OptionsMessage.ts +13 -1
  29. package/controller/comms/messages/config/PumpMessage.ts +4 -20
  30. package/controller/comms/messages/config/RemoteMessage.ts +4 -0
  31. package/controller/comms/messages/config/ScheduleMessage.ts +11 -0
  32. package/controller/comms/messages/config/SecurityMessage.ts +1 -0
  33. package/controller/comms/messages/config/ValveMessage.ts +13 -3
  34. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +2 -3
  35. package/controller/comms/messages/status/EquipmentStateMessage.ts +78 -24
  36. package/controller/comms/messages/status/HeaterStateMessage.ts +42 -9
  37. package/controller/comms/messages/status/IntelliChemStateMessage.ts +37 -26
  38. package/controller/nixie/Nixie.ts +18 -16
  39. package/controller/nixie/bodies/Body.ts +4 -1
  40. package/controller/nixie/chemistry/ChemController.ts +80 -77
  41. package/controller/nixie/chemistry/Chlorinator.ts +9 -8
  42. package/controller/nixie/circuits/Circuit.ts +55 -6
  43. package/controller/nixie/heaters/Heater.ts +192 -32
  44. package/controller/nixie/pumps/Pump.ts +146 -84
  45. package/controller/nixie/schedules/Schedule.ts +3 -2
  46. package/controller/nixie/valves/Valve.ts +1 -1
  47. package/defaultConfig.json +32 -1
  48. package/issue_template.md +1 -1
  49. package/logger/DataLogger.ts +37 -22
  50. package/package.json +20 -18
  51. package/web/Server.ts +520 -29
  52. package/web/bindings/influxDB.json +96 -8
  53. package/web/bindings/mqtt.json +151 -40
  54. package/web/bindings/mqttAlt.json +114 -4
  55. package/web/interfaces/httpInterface.ts +2 -0
  56. package/web/interfaces/influxInterface.ts +36 -19
  57. package/web/interfaces/mqttInterface.ts +14 -3
  58. package/web/services/config/Config.ts +171 -44
  59. package/web/services/state/State.ts +49 -5
  60. package/web/services/state/StateSocket.ts +18 -1
@@ -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 => {
@@ -175,10 +188,14 @@ export class InfluxInterfaceBindings extends BaseInterfaceBindings {
175
188
  let sec = ts.getSeconds() - 1;
176
189
  ts.setSeconds(sec);
177
190
  point2.timestamp(ts);
191
+ logger.silly(`Writing influx ${e.name} inverse data point ${point2.toString()})`)
178
192
  this.writeApi.writePoint(point2);
179
193
  }
180
194
  if (typeof point.toLineProtocol() !== 'undefined') {
195
+ logger.silly(`Writing influx ${e.name} data point ${point.toString()}`)
181
196
  this.writeApi.writePoint(point);
197
+ this.writeApi.flush()
198
+ .catch(error => { logger.error(error); });
182
199
  //logger.info(`INFLUX: ${point.toLineProtocol()}`)
183
200
  }
184
201
  else {
@@ -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 = () => {
@@ -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,7 +57,8 @@ 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
  });
@@ -106,7 +112,7 @@ export class ConfigRoute {
106
112
  invalidIds: sys.board.equipmentIds.invalidIds.get(),
107
113
  equipmentIds: sys.equipment.equipmentIds.features,
108
114
  equipmentNames: sys.board.circuits.getCircuitNames(),
109
- functions: sys.board.valueMaps.featureFunctions.toArray(),
115
+ functions: sys.board.features.getFeatureFunctions(),
110
116
  features: sys.features.get()
111
117
  };
112
118
  return res.status(200).send(opts);
@@ -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
  });
@@ -282,7 +289,8 @@ export class ConfigRoute {
282
289
  bodies: sys.board.bodies.getBodyAssociations(),
283
290
  chlorinators: sys.chlorinators.get(),
284
291
  maxChlorinators: sys.equipment.maxChlorinators,
285
- models: sys.board.valueMaps.chlorinatorModel.toArray()
292
+ models: sys.board.valueMaps.chlorinatorModel.toArray(),
293
+ equipmentMasters: sys.board.valueMaps.equipmentMaster.toArray()
286
294
  };
287
295
  return res.status(200).send(opts);
288
296
  });
@@ -307,13 +315,16 @@ export class ConfigRoute {
307
315
  let opts = {
308
316
  interfaces: config.getSection('web.interfaces'),
309
317
  types: [
310
- {name: 'rem', desc: 'Relay Equipment Manager'},
311
- {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' }
312
323
  ],
313
324
  protocols: [
314
325
  { val: 0, name: 'http://', desc: 'http://' },
315
326
  { val: 1, name: 'https://', desc: 'https://' },
316
- { val: 2, name: 'mqtt://', desc: 'mqtt://' },
327
+ { val: 2, name: 'mqtt://', desc: 'mqtt://' }
317
328
  ]
318
329
  }
319
330
  return res.status(200).send(opts);
@@ -332,6 +343,8 @@ export class ConfigRoute {
332
343
  bodies: sys.board.bodies.getBodyAssociations(),
333
344
  filters: sys.filters.get(),
334
345
  areaUnits: sys.board.valueMaps.areaUnits.toArray(),
346
+ pressureUnits: sys.board.valueMaps.pressureUnits.toArray(),
347
+ circuits: sys.board.circuits.getCircuitReferences(true, true, true, false),
335
348
  servers: []
336
349
  };
337
350
  if (sys.controllerType === ControllerType.Nixie) opts.servers = await sys.ncp.getREMServers();
@@ -340,14 +353,14 @@ export class ConfigRoute {
340
353
  });
341
354
  /******* END OF CONFIGURATION PICK LISTS/REFERENCES AND VALIDATION ***********/
342
355
  /******* ENDPOINTS FOR MODIFYING THE OUTDOOR CONTROL PANEL SETTINGS **********/
343
- app.put('/config/rem', async (req, res, next)=>{
356
+ app.put('/config/rem', async (req, res, next) => {
344
357
  try {
345
358
  // RSG: this is problematic because we now enable multiple rem type interfaces that may not be called REM.
346
359
  // This is now also a dupe of PUT /app/interface and should be consolidated
347
360
  // config.setSection('web.interfaces.rem', req.body);
348
361
  config.setInterface(req.body);
349
362
  }
350
- catch (err) {next(err);}
363
+ catch (err) { next(err); }
351
364
  })
352
365
  app.put('/config/tempSensors', async (req, res, next) => {
353
366
  try {
@@ -362,7 +375,7 @@ export class ConfigRoute {
362
375
  });
363
376
  app.put('/config/filter', async (req, res, next) => {
364
377
  try {
365
- let sfilter = sys.board.filters.setFilter(req.body);
378
+ let sfilter = await sys.board.filters.setFilterAsync(req.body);
366
379
  return res.status(200).send(sfilter.get(true));
367
380
  }
368
381
  catch (err) { next(err); }
@@ -375,7 +388,7 @@ export class ConfigRoute {
375
388
  });
376
389
  app.delete('/config/filter', async (req, res, next) => {
377
390
  try {
378
- let sfilter = sys.board.filters.deleteFilter(req.body);
391
+ let sfilter = await sys.board.filters.deleteFilterAsync(req.body);
379
392
  return res.status(200).send(sfilter.get(true));
380
393
  }
381
394
  catch (err) { next(err); }
@@ -579,8 +592,8 @@ export class ConfigRoute {
579
592
  return res.status(200).send(circuitFunctions);
580
593
  });
581
594
  app.get('/config/features/functions', (req, res) => {
582
- let circuitFunctions = sys.board.circuits.getCircuitFunctions();
583
- return res.status(200).send(circuitFunctions);
595
+ let featureFunctions = sys.board.features.getFeatureFunctions();
596
+ return res.status(200).send(featureFunctions);
584
597
  });
585
598
  app.get('/config/circuit/:id', (req, res) => {
586
599
  // todo: need getInterfaceById.get() in case features are requested here
@@ -589,7 +602,7 @@ export class ConfigRoute {
589
602
  });
590
603
  app.get('/config/circuit/:id/lightThemes', (req, res) => {
591
604
  let circuit = sys.circuits.getInterfaceById(parseInt(req.params.id, 10));
592
- 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) : [];
593
606
  return res.status(200).send(themes);
594
607
  });
595
608
  app.get('/config/chlorinator/:id', (req, res) => {
@@ -727,7 +740,7 @@ export class ConfigRoute {
727
740
  // RSG: is this and /config/circuit/:id/lightThemes both needed?
728
741
 
729
742
  // if (sys.controllerType === ControllerType.IntelliCenter) {
730
- let grp = sys.lightGroups.getItemById(parseInt(req.params.id, 10));
743
+ let grp = sys.lightGroups.getItemById(parseInt(req.body.id, 10));
731
744
  return res.status(200).send(grp.getLightThemes());
732
745
  // }
733
746
  // else
@@ -760,16 +773,16 @@ export class ConfigRoute {
760
773
  let grp = sys.circuitGroups.getItemById(parseInt(req.params.id, 10));
761
774
  return res.status(200).send(grp.getExtended());
762
775
  });
763
- /* app.get('/config/chemController/search', async (req, res, next) => {
764
- // Change the options for the pool.
765
- try {
766
- let result = await sys.board.virtualChemControllers.search();
767
- return res.status(200).send(result);
768
- }
769
- catch (err) {
770
- next(err);
771
- }
772
- }); */
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
+ }); */
773
786
  app.put('/config/chemController', async (req, res, next) => {
774
787
  try {
775
788
  let chem = await sys.board.chemControllers.setChemControllerAsync(req.body);
@@ -792,17 +805,17 @@ export class ConfigRoute {
792
805
  catch (err) { next(err); }
793
806
 
794
807
  });
795
- /* app.get('/config/intellibrite', (req, res) => {
796
- return res.status(200).send(sys.intellibrite.getExtended());
797
- });
798
- app.get('/config/intellibrite/colors', (req, res) => {
799
- return res.status(200).send(sys.board.valueMaps.lightColors.toArray());
800
- });
801
- app.put('/config/intellibrite/setColors', (req, res) => {
802
- let grp = extend(true, { id: 0 }, req.body);
803
- sys.board.circuits.setIntelliBriteColors(new LightGroup(grp));
804
- return res.status(200).send('OK');
805
- }); */
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
+ }); */
806
819
  app.get('/config', (req, res) => {
807
820
  return res.status(200).send(sys.getSection('all'));
808
821
  });
@@ -828,11 +841,11 @@ export class ConfigRoute {
828
841
  return res.status(200).send('OK');
829
842
  });
830
843
  app.put('/app/interface', async (req, res, next) => {
831
- try{
832
- await webApp.updateServerInterface(req.body);
833
- return res.status(200).send('OK');
834
- }
835
- 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); }
836
849
  });
837
850
  app.put('/app/rs485Port', async (req, res, next) => {
838
851
  try {
@@ -849,15 +862,129 @@ export class ConfigRoute {
849
862
  startPacketCapture(false);
850
863
  return res.status(200).send('OK');
851
864
  });
852
- app.get('/app/config/stopPacketCapture', async (req, res,next) => {
865
+ app.get('/app/config/stopPacketCapture', async (req, res, next) => {
853
866
  try {
854
867
  let file = await stopPacketCaptureAsync();
855
868
  res.download(file);
856
869
  }
857
- catch (err) {next(err);}
870
+ catch (err) { next(err); }
858
871
  });
859
872
  app.get('/app/config/:section', (req, res) => {
860
873
  return res.status(200).send(config.getSection(req.params.section));
861
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
+ });
862
989
  }
863
990
  }
@@ -15,6 +15,8 @@ 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
17
  import * as express from "express";
18
+ import * as extend from "extend";
19
+
18
20
  import { state, ICircuitState, LightGroupState, ICircuitGroupState, ChemicalDoseState } from "../../../controller/State";
19
21
  import { sys } from "../../../controller/Equipment";
20
22
  import { utils } from '../../../controller/Constants';
@@ -79,7 +81,21 @@ export class StateRoute {
79
81
  catch (err) { next(err); }
80
82
 
81
83
  });
82
- app.search('/state/chemController/:id/doseHistory/ph', async (req, res, next) => {
84
+ app.get('/state/chemController/:id/doseLog/ph', async (req, res, next) => {
85
+ try {
86
+ let schem = state.chemControllers.getItemById(parseInt(req.params.id));
87
+ let filter = req.body || {};
88
+ let dh = await DataLogger.readFromEndAsync(`chemDosage_${schem.ph.chemType}.log`, ChemicalDoseState, (lineNumber: number, entry: ChemicalDoseState, arr: ChemicalDoseState[]): boolean => {
89
+ if (entry.id !== schem.id) return false;
90
+ if (typeof filter.lines !== 'undefined' && filter.lines <= arr.length) return false;
91
+ if (typeof filter.date !== 'undefined' && entry.end < filter.date) return false;
92
+ return true;
93
+ });
94
+ return res.status(200).send(dh);
95
+ }
96
+ catch (err) { next(err); }
97
+ });
98
+ app.search('/state/chemController/:id/doseLog/ph', async (req, res, next) => {
83
99
  try {
84
100
  let schem = state.chemControllers.getItemById(parseInt(req.params.id));
85
101
  let filter = req.body || {};
@@ -93,6 +109,34 @@ export class StateRoute {
93
109
  }
94
110
  catch (err) { next(err); }
95
111
  });
112
+ app.get('/state/chemController/:id/doseLog/orp', async (req, res, next) => {
113
+ try {
114
+ let schem = state.chemControllers.getItemById(parseInt(req.params.id));
115
+ let filter = req.body || {};
116
+ let dh = await DataLogger.readFromEndAsync(`chemDosage_orp.log`, ChemicalDoseState, (lineNumber: number, entry: ChemicalDoseState, arr: ChemicalDoseState[]): boolean => {
117
+ if (entry.id !== schem.id) return false;
118
+ if (typeof filter.lines !== 'undefined' && filter.lines <= arr.length) return false;
119
+ if (typeof filter.date !== 'undefined' && entry.end < filter.date) return false;
120
+ return true;
121
+ });
122
+ return res.status(200).send(dh);
123
+ }
124
+ catch (err) { next(err); }
125
+ });
126
+ app.search('/state/chemController/:id/doseLog/orp', async (req, res, next) => {
127
+ try {
128
+ let schem = state.chemControllers.getItemById(parseInt(req.params.id));
129
+ let filter = req.body || {};
130
+ let dh = DataLogger.readFromEnd(`chemDosage_orp.log`, ChemicalDoseState, (lineNumber: number, entry: ChemicalDoseState, arr: ChemicalDoseState[]): boolean => {
131
+ if (entry.id !== schem.id) return;
132
+ if (typeof filter.lines !== 'undefined' && filter.lines <= arr.length) return false;
133
+ if (typeof filter.date !== 'undefined' && entry.end < filter.date) return false;
134
+ return true;
135
+ });
136
+ return res.status(200).send(dh);
137
+ }
138
+ catch (err) { next(err); }
139
+ });
96
140
  app.put('/state/chemController/cancelDosing', async (req, res, next) => {
97
141
  try {
98
142
  let schem = await sys.board.chemControllers.cancelDosingAsync(req.body);
@@ -109,7 +153,7 @@ export class StateRoute {
109
153
  });
110
154
 
111
155
  app.get('/state/chlorinator/:id', (req, res) => {
112
- res.status(200).send(state.chlorinators.getItemById(parseInt(req.params.id, 10)).get());
156
+ res.status(200).send(state.chlorinators.getItemById(parseInt(req.params.id, 10), false).getExtended());
113
157
  });
114
158
  app.get('/state/circuit/:id', (req, res) => {
115
159
  res.status(200).send(state.circuits.getItemById(parseInt(req.params.id, 10)).get());
@@ -165,8 +209,8 @@ export class StateRoute {
165
209
  catch (err) {next(err);}
166
210
  });
167
211
  app.put('/state/circuit/setTheme', async (req, res, next) => {
168
- try {
169
- let theme = await state.circuits.setLightThemeAsync(parseInt(req.body.id, 10), parseInt(req.body.theme, 10));
212
+ try {
213
+ let theme = await state.circuits.setLightThemeAsync(parseInt(req.body.id, 10), sys.board.valueMaps.lightThemes.encode(req.body.theme));
170
214
  return res.status(200).send(theme.get(true));
171
215
  }
172
216
  catch (err) { next(err); }
@@ -229,7 +273,7 @@ export class StateRoute {
229
273
  if (typeof req.body.heatSetpoint !== 'undefined' && !isNaN(parseInt(req.body.heatSetpoint, 10)))
230
274
  await sys.board.bodies.setHeatSetpointAsync(body, parseInt(req.body.heatSetpoint, 10));
231
275
  else if (typeof req.body.setPoint !== 'undefined' && !isNaN(parseInt(req.body.setPoint, 10)))
232
- await sys.board.bodies.setHeatSetpointAsync(body, parseInt(req.body.heatSetpoint, 10));
276
+ await sys.board.bodies.setHeatSetpointAsync(body, parseInt(req.body.setpoint, 10));
233
277
  let tbody = state.temps.bodies.getItemById(body.id);
234
278
  return res.status(200).send(tbody.get(true));
235
279
  } catch (err) { next(err); }
@@ -74,6 +74,7 @@ export class StateSocket {
74
74
  }
75
75
  catch (err) { logger.error(err); }
76
76
  });
77
+
77
78
  sock.on('/chlorinator', async (data: any) => {
78
79
  try {
79
80
  data = JSON.parse(data);
@@ -99,6 +100,22 @@ export class StateSocket {
99
100
  }
100
101
  catch (err) { logger.error(err); }
101
102
  });
103
+ sock.on('/filter', async (data: any) => {
104
+ try {
105
+ data = JSON.parse(data);
106
+ let id = parseInt(data.id, 10);
107
+ let filter = sys.filters.find(elem => elem.id === id);
108
+ if (typeof filter !== 'undefined' && filter.isActive && !isNaN(filter.id)) {
109
+ let sfilter = state.filters.getItemById(filter.id, filter.isActive)
110
+ let pu = sys.board.valueMaps.pressureUnits.transform(filter.pressureUnits);
111
+ if (typeof data.pressure !== 'undefined')
112
+ await sys.board.filters.setFilterPressure(filter.id, data.pressure, data.pressureUnits || pu.name);
113
+ sfilter.emitEquipmentChange();
114
+ }
115
+
116
+
117
+ } catch (err) { logger.error(err); }
118
+ });
102
119
  sock.on('/chemController', async (data: any) => {
103
120
  try {
104
121
  //console.log(`chemController: ${data}`);
@@ -144,7 +161,7 @@ export class StateSocket {
144
161
  if (typeof data.orpTank.units === 'string') scontroller.orp.tank.units = controller.orp.tank.units = data.orpTank.units;
145
162
  }
146
163
 
147
- // Need to build this out to include the type of controller. If this is Homegrown or REM Chem we
164
+ // Need to build this out to include the type of controller. If this is REM Chem we
148
165
  // will send the whole rest of the nut over to it. Intellichem will only let us
149
166
  // set specific values.
150
167
  if (controller.type === 3) {