nodejs-poolcontroller 8.3.0 → 8.4.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 (107) hide show
  1. package/.eslintrc.json +36 -36
  2. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -84
  3. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -12
  4. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -28
  5. package/.github/ISSUE_TEMPLATE/config.yml +8 -8
  6. package/.github/copilot-instructions.md +63 -63
  7. package/.github/workflows/ghcr-publish.yml +67 -67
  8. package/157_issues.md +101 -0
  9. package/AGENTS.md +613 -0
  10. package/CONTRIBUTING.md +74 -74
  11. package/Changelog +292 -284
  12. package/Dockerfile +62 -62
  13. package/Gruntfile.js +40 -40
  14. package/LICENSE +661 -661
  15. package/README.md +329 -309
  16. package/anslq25/MessagesMock.ts +221 -221
  17. package/anslq25/boards/MockBoardFactory.ts +49 -49
  18. package/anslq25/boards/MockEasyTouchBoard.ts +696 -696
  19. package/anslq25/boards/MockSystemBoard.ts +216 -216
  20. package/anslq25/chemistry/MockChlorinator.ts +98 -98
  21. package/anslq25/pumps/MockPump.ts +83 -83
  22. package/app.ts +115 -115
  23. package/config/Config.ts +0 -0
  24. package/config/VersionCheck.ts +0 -0
  25. package/controller/Constants.ts +809 -805
  26. package/controller/Equipment.ts +2737 -2664
  27. package/controller/Errors.ts +181 -181
  28. package/controller/Lockouts.ts +549 -549
  29. package/controller/State.ts +3746 -3701
  30. package/controller/boards/AquaLinkBoard.ts +1175 -1003
  31. package/controller/boards/BoardFactory.ts +53 -53
  32. package/controller/boards/EasyTouchBoard.ts +3246 -3202
  33. package/controller/boards/IntelliCenterBoard.ts +4581 -3899
  34. package/controller/boards/IntelliComBoard.ts +69 -69
  35. package/controller/boards/IntelliTouchBoard.ts +382 -382
  36. package/controller/boards/NixieBoard.ts +1947 -1944
  37. package/controller/boards/SunTouchBoard.ts +401 -400
  38. package/controller/boards/SystemBoard.ts +5303 -5268
  39. package/controller/comms/Comms.ts +1278 -1255
  40. package/controller/comms/ScreenLogic.ts +1665 -1665
  41. package/controller/comms/messages/Messages.ts +1627 -1406
  42. package/controller/comms/messages/config/ChlorinatorMessage.ts +5 -0
  43. package/controller/comms/messages/config/CircuitGroupMessage.ts +0 -0
  44. package/controller/comms/messages/config/CircuitMessage.ts +0 -0
  45. package/controller/comms/messages/config/ConfigMessage.ts +6 -0
  46. package/controller/comms/messages/config/CoverMessage.ts +0 -0
  47. package/controller/comms/messages/config/CustomNameMessage.ts +31 -31
  48. package/controller/comms/messages/config/EquipmentMessage.ts +250 -210
  49. package/controller/comms/messages/config/ExternalMessage.ts +1051 -903
  50. package/controller/comms/messages/config/FeatureMessage.ts +0 -0
  51. package/controller/comms/messages/config/GeneralMessage.ts +65 -0
  52. package/controller/comms/messages/config/HeaterMessage.ts +0 -0
  53. package/controller/comms/messages/config/IntellichemMessage.ts +0 -0
  54. package/controller/comms/messages/config/OptionsMessage.ts +207 -174
  55. package/controller/comms/messages/config/PumpMessage.ts +427 -421
  56. package/controller/comms/messages/config/RemoteMessage.ts +0 -0
  57. package/controller/comms/messages/config/ScheduleMessage.ts +401 -390
  58. package/controller/comms/messages/config/SecurityMessage.ts +37 -13
  59. package/controller/comms/messages/config/ValveMessage.ts +0 -0
  60. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +0 -0
  61. package/controller/comms/messages/status/EquipmentStateMessage.ts +940 -822
  62. package/controller/comms/messages/status/HeaterStateMessage.ts +147 -135
  63. package/controller/comms/messages/status/IntelliChemStateMessage.ts +448 -448
  64. package/controller/comms/messages/status/IntelliValveStateMessage.ts +36 -36
  65. package/controller/comms/messages/status/NeptuneModbusStateMessage.ts +217 -0
  66. package/controller/comms/messages/status/PumpStateMessage.ts +0 -0
  67. package/controller/comms/messages/status/RegalModbusStateMessage.ts +410 -410
  68. package/controller/comms/messages/status/VersionMessage.ts +152 -41
  69. package/controller/nixie/Nixie.ts +173 -173
  70. package/controller/nixie/NixieEquipment.ts +104 -104
  71. package/controller/nixie/bodies/Body.ts +120 -120
  72. package/controller/nixie/bodies/Filter.ts +135 -135
  73. package/controller/nixie/chemistry/ChemController.ts +2756 -2724
  74. package/controller/nixie/chemistry/ChemDoser.ts +806 -806
  75. package/controller/nixie/chemistry/Chlorinator.ts +367 -367
  76. package/controller/nixie/circuits/Circuit.ts +478 -478
  77. package/controller/nixie/heaters/Heater.ts +843 -834
  78. package/controller/nixie/pumps/Pump.ts +1336 -1193
  79. package/controller/nixie/schedules/Schedule.ts +401 -401
  80. package/controller/nixie/valves/Valve.ts +170 -170
  81. package/defaultConfig.json +352 -352
  82. package/docker-compose.yml +32 -31
  83. package/logger/DataLogger.ts +448 -448
  84. package/logger/Logger.ts +459 -436
  85. package/package.json +58 -58
  86. package/sendSocket.js +32 -32
  87. package/tsconfig.json +26 -25
  88. package/types/express-multer.d.ts +32 -32
  89. package/web/Server.ts +1939 -1927
  90. package/web/bindings/aqualinkD.json +559 -559
  91. package/web/bindings/influxDB.json +1066 -1066
  92. package/web/bindings/mqtt.json +721 -721
  93. package/web/bindings/mqttAlt.json +746 -746
  94. package/web/bindings/rulesManager.json +54 -54
  95. package/web/bindings/smartThings-Hubitat.json +31 -31
  96. package/web/bindings/valveRelays.json +20 -20
  97. package/web/bindings/vera.json +25 -25
  98. package/web/interfaces/baseInterface.ts +188 -188
  99. package/web/interfaces/httpInterface.ts +148 -148
  100. package/web/interfaces/influxInterface.ts +283 -283
  101. package/web/interfaces/mqttInterface.ts +695 -695
  102. package/web/interfaces/ruleInterface.ts +101 -87
  103. package/web/services/config/Config.ts +1212 -1053
  104. package/web/services/config/ConfigSocket.ts +0 -0
  105. package/web/services/state/State.ts +21 -0
  106. package/web/services/state/StateSocket.ts +28 -0
  107. package/web/services/utilities/Utilities.ts +233 -233
@@ -1,1054 +1,1213 @@
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 fs from "fs";
19
- import * as path from "path";
20
- import * as express from "express";
21
- import * as extend from 'extend';
22
- import * as multer from 'multer';
23
- import { sys, LightGroup, ControllerType, Pump, Valve, Body, General, Circuit, ICircuit, Feature, CircuitGroup, CustomNameCollection, Schedule, Chlorinator, Heater, Screenlogic } from "../../../controller/Equipment";
24
- import { config } from "../../../config/Config";
25
- import { logger } from "../../../logger/Logger";
26
- import { utils } from "../../../controller/Constants";
27
- import { ServiceProcessError } from "../../../controller/Errors";
28
- import { state } from "../../../controller/State";
29
- import { stopPacketCaptureAsync, startPacketCapture } from '../../../app';
30
- import { conn } from "../../../controller/comms/Comms";
31
- import { webApp, BackupFile, RestoreFile } from "../../Server";
32
- import { release } from "os";
33
- import { ScreenLogicComms, sl } from "../../../controller/comms/ScreenLogic";
34
- import { screenlogic } from "node-screenlogic";
35
-
36
- export class ConfigRoute {
37
- public static initRoutes(app: express.Application) {
38
- app.get('/config/body/:body/heatModes', (req, res) => {
39
- return res.status(200).send(sys.bodies.getItemById(parseInt(req.params.body, 10)).getHeatModes());
40
- });
41
- app.get('/config/circuit/names', (req, res) => {
42
- let circuitNames = sys.board.circuits.getCircuitNames();
43
- return res.status(200).send(circuitNames);
44
- });
45
- app.get('/config/circuit/references', (req, res) => {
46
- let circuits = typeof req.query.circuits === 'undefined' || utils.makeBool(req.query.circuits);
47
- let features = typeof req.query.features === 'undefined' || utils.makeBool(req.query.features);
48
- let groups = typeof req.query.features === 'undefined' || utils.makeBool(req.query.groups);
49
- let virtual = typeof req.query.virtual === 'undefined' || utils.makeBool(req.query.virtual);
50
- return res.status(200).send(sys.board.circuits.getCircuitReferences(circuits, features, virtual, groups));
51
- });
52
-
53
- /******* CONFIGURATION PICK LISTS/REFERENCES and VALIDATION PARAMETERS *********/
54
- /// Returns an object that contains the general options for setting up the panel.
55
- app.get('/config/options/general', (req, res) => {
56
- let opts = {
57
- countries: sys.board.valueMaps.countries.toArray(),
58
- tempUnits: sys.board.valueMaps.tempUnits.toArray(),
59
- timeZones: sys.board.valueMaps.timeZones.toArray(),
60
- clockSources: sys.board.valueMaps.clockSources.toArray(),
61
- clockModes: sys.board.valueMaps.clockModes.toArray(),
62
- pool: sys.general.get(true),
63
- sensors: sys.board.system.getSensors(),
64
- systemUnits: sys.board.valueMaps.systemUnits.toArray()
65
- };
66
- return res.status(200).send(opts);
67
- });
68
- app.get('/config/options/rs485', async (req, res, next) => {
69
- try {
70
- let opts = { ports: [], local: [], screenlogic: {} }
71
- let cfg = config.getSection('controller');
72
- for (let section in cfg) {
73
- if (section.startsWith('comms')) {
74
- let cport = extend(true, { enabled: false, netConnect: false, mock: false }, cfg[section]);
75
- let port = conn.findPortById(cport.portId || 0);
76
- if (typeof cport.type === 'undefined'){
77
- cport.type = cport.netConnect ? 'netConnect' : cport.mock ? 'mock' : 'local'
78
- }
79
- if (typeof port !== 'undefined') cport.stats = port.stats;
80
- if (port.portId === 0 && port.type === 'screenlogic') {
81
- cport.screenlogic.stats = sl.stats;
82
- }
83
- opts.ports.push(cport);
84
- }
85
- // if (section.startsWith('screenlogic')){
86
- // let screenlogic = cfg[section];
87
- // screenlogic.types = [{ val: 'local', name: 'Local', desc: 'Local Screenlogic' }, { val: 'remote', name: 'Remote', desc: 'Remote Screenlogic' }];
88
- // screenlogic.stats = sl.stats;
89
- // opts.screenlogic = screenlogic;
90
- // }
91
- }
92
- opts.local = await conn.getLocalPortsAsync() || [];
93
- return res.status(200).send(opts);
94
- } catch (err) { next(err); }
95
- });
96
- // app.get('/config/options/screenlogic', async (req, res, next) => {
97
- // try {
98
- // let cfg = config.getSection('controller.screenlogic');
99
- // let data = {
100
- // cfg,
101
- // types: [{ val: 'local', name: 'Local', desc: 'Local Screenlogic' }, { val: 'remote', name: 'Remote', desc: 'Remote Screenlogic' }]
102
- // }
103
- // return res.status(200).send(data);
104
- // } catch (err) { next(err); }
105
- // });
106
- app.get('/config/options/screenlogic/search', async (req, res, next) => {
107
- try {
108
- let localUnits = await ScreenLogicComms.searchAsync();
109
- return res.status(200).send(localUnits);
110
- } catch (err) { next(err); }
111
- });
112
- app.get('/config/options/circuits', async (req, res, next) => {
113
- try {
114
- let opts = {
115
- maxCircuits: sys.equipment.maxCircuits,
116
- equipmentIds: sys.equipment.equipmentIds.circuits,
117
- invalidIds: sys.board.equipmentIds.invalidIds.get(),
118
- equipmentNames: sys.board.circuits.getCircuitNames(),
119
- functions: sys.board.circuits.getCircuitFunctions(),
120
- circuits: sys.circuits.get(),
121
- controllerType: sys.controllerType,
122
- servers: await sys.ncp.getREMServers()
123
- };
124
- return res.status(200).send(opts);
125
- } catch (err) { next(err); }
126
- });
127
- app.get('/config/options/circuitGroups', (req, res) => {
128
- let opts = {
129
- maxCircuitGroups: sys.equipment.maxCircuitGroups,
130
- equipmentNames: sys.board.circuits.getCircuitNames(),
131
- circuits: sys.board.circuits.getCircuitReferences(true, true, false),
132
- circuitGroups: sys.circuitGroups.get(),
133
- circuitStates: sys.board.valueMaps.groupCircuitStates.toArray()
134
- };
135
- return res.status(200).send(opts);
136
- });
137
- app.get('/config/options/lightGroups', (req, res) => {
138
- let opts = {
139
- maxLightGroups: sys.equipment.maxLightGroups,
140
- equipmentNames: sys.board.circuits.getCircuitNames(),
141
- themes: sys.board.circuits.getLightThemes(),
142
- colors: sys.board.valueMaps.lightColors.toArray(),
143
- circuits: sys.board.circuits.getLightReferences(),
144
- lightGroups: sys.lightGroups.get(),
145
- functions: sys.board.circuits.getCircuitFunctions()
146
- };
147
- return res.status(200).send(opts);
148
- });
149
- app.get('/config/options/features', (req, res) => {
150
- let opts = {
151
- maxFeatures: sys.equipment.maxFeatures,
152
- invalidIds: sys.board.equipmentIds.invalidIds.get(),
153
- equipmentIds: sys.equipment.equipmentIds.features,
154
- equipmentNames: sys.board.circuits.getCircuitNames(),
155
- functions: sys.board.features.getFeatureFunctions(),
156
- features: sys.features.get()
157
- };
158
- return res.status(200).send(opts);
159
- });
160
- app.get('/config/options/bodies', (req, res) => {
161
- let opts = {
162
- maxBodies: sys.equipment.maxBodies,
163
- bodyTypes: sys.board.valueMaps.bodies.toArray(),
164
- bodies: sys.bodies.get(),
165
- capacityUnits: sys.board.valueMaps.volumeUnits.toArray()
166
- };
167
- return res.status(200).send(opts);
168
- });
169
- app.get('/config/options/valves', async (req, res, next) => {
170
- try {
171
- let opts = {
172
- maxValves: sys.equipment.maxValves,
173
- valveTypes: sys.board.valueMaps.valveTypes.toArray(),
174
- circuits: sys.board.circuits.getCircuitReferences(true, true, true),
175
- valves: sys.valves.get(),
176
- servers: await sys.ncp.getREMServers()
177
- };
178
- opts.circuits.unshift({ id: 256, name: 'Unassigned', type: 0, equipmentType: 'circuit' });
179
- return res.status(200).send(opts);
180
- } catch (err) { next(err); }
181
- });
182
- app.get('/config/options/pumps', async (req, res, next) => {
183
- try {
184
- let opts: any = {
185
- maxPumps: sys.equipment.maxPumps,
186
- pumpUnits: sys.board.valueMaps.pumpUnits.toArray(),
187
- pumpTypes: sys.board.valueMaps.pumpTypes.toArray(),
188
- models: {
189
- ss: sys.board.valueMaps.pumpSSModels.toArray(),
190
- ds: sys.board.valueMaps.pumpDSModels.toArray(),
191
- vs: sys.board.valueMaps.pumpVSModels.toArray(),
192
- vf: sys.board.valueMaps.pumpVSModels.toArray(),
193
- vsf: sys.board.valueMaps.pumpVSFModels.toArray(),
194
- vssvrs: sys.board.valueMaps.pumpVSSVRSModels.toArray()
195
- },
196
- circuits: sys.board.circuits.getCircuitReferences(true, true, true, true),
197
- bodies: sys.board.valueMaps.pumpBodies.toArray(),
198
- pumps: sys.pumps.get(),
199
- servers: await sys.ncp.getREMServers(),
200
- rs485ports: await conn.listInstalledPorts()
201
- };
202
- // RKS: Why do we need the circuit names? We have the circuits. Is this so
203
- // that we can name the pump. I thought that *Touch uses the pump type as the name
204
- // plus a number.
205
- // RG: because I need the name/val of the Not Used circuit for displaying pumpCircuits that are
206
- // empty. EG A pump circuit can be not used even if all the circuits are used.
207
- if (sys.controllerType !== ControllerType.IntelliCenter) {
208
- opts.circuitNames = sys.board.circuits.getCircuitNames().filter(c => c.name === 'notused');
209
- }
210
- return res.status(200).send(opts);
211
- } catch (err) { next(err); }
212
- });
213
- app.get('/config/options/schedules', async (req, res, next) => {
214
- try {
215
- let opts = {
216
- maxSchedules: sys.equipment.maxSchedules,
217
- tempUnits: sys.board.valueMaps.tempUnits.transform(state.temps.units),
218
- scheduleTimeTypes: sys.board.valueMaps.scheduleTimeTypes.toArray(),
219
- scheduleTypes: sys.board.valueMaps.scheduleTypes.toArray(),
220
- scheduleDays: sys.board.valueMaps.scheduleDays.toArray(),
221
- heatSources: sys.board.valueMaps.heatSources.toArray(),
222
- circuits: sys.board.circuits.getCircuitReferences(true, true, false, true),
223
- schedules: sys.schedules.get(),
224
- clockMode: sys.general.options.clockMode || 12,
225
- displayTypes: sys.board.valueMaps.scheduleDisplayTypes.toArray(),
226
- bodies: [],
227
- eggTimers: sys.eggTimers.get() // needed for *Touch to not overwrite real schedules
228
- };
229
- // Now get all the body heat sources.
230
- for (let i = 0; i < sys.bodies.length; i++) {
231
- let body = sys.bodies.getItemByIndex(i);
232
- opts.bodies.push({ id: body.id, circuit: body.circuit, name: body.name, alias: body.alias, heatSources: sys.board.bodies.getHeatSources(body.id) });
233
- }
234
- return res.status(200).send(opts);
235
- } catch (err) { next(err); }
236
- });
237
- app.get('/config/options/heaters', async (req, res, next) => {
238
- try {
239
- let opts = {
240
- tempUnits: sys.board.valueMaps.tempUnits.transform(state.temps.units),
241
- bodies: sys.board.bodies.getBodyAssociations(),
242
- maxHeaters: sys.equipment.maxHeaters,
243
- heaters: sys.heaters.get(),
244
- heaterTypes: sys.board.valueMaps.heaterTypes.toArray(),
245
- heatModes: sys.board.valueMaps.heatModes.toArray(),
246
- coolDownDelay: sys.general.options.cooldownDelay,
247
- servers: [],
248
- rs485ports: await conn.listInstalledPorts()
249
- };
250
- // We only need the servers data when the controller is a Nixie controller. We don't need to
251
- // wait for this information if we are dealing with an OCP.
252
- if (sys.controllerType === ControllerType.Nixie) opts.servers = await sys.ncp.getREMServers();
253
- return res.status(200).send(opts);
254
- } catch (err) { next(err); }
255
- });
256
- app.get('/config/options/customNames', (req, res) => {
257
- let opts = {
258
- maxCustomNames: sys.equipment.maxCustomNames,
259
- customNames: sys.customNames.get()
260
- };
261
- return res.status(200).send(opts);
262
- });
263
- app.get('/config/options/chemControllers', async (req, res, next) => {
264
- try {
265
- let remServers = await sys.ncp.getREMServers();
266
- let alarms = {
267
- flow: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 1].includes(el.val)),
268
- pH: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 2, 4].includes(el.val)),
269
- orp: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 8, 16].includes(el.val)),
270
- pHTank: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 32].includes(el.val)),
271
- orpTank: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 64].includes(el.val)),
272
- probeFault: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 128].includes(el.val))
273
- }
274
- let warnings = {
275
- waterChemistry: sys.board.valueMaps.chemControllerWarnings.toArray().filter(el => [0, 1, 2].includes(el.val)),
276
- pHLockout: sys.board.valueMaps.chemControllerLimits.toArray().filter(el => [0, 1].includes(el.val)),
277
- pHDailyLimitReached: sys.board.valueMaps.chemControllerLimits.toArray().filter(el => [0, 2].includes(el.val)),
278
- orpDailyLimitReached: sys.board.valueMaps.chemControllerLimits.toArray().filter(el => [0, 4].includes(el.val)),
279
- invalidSetup: sys.board.valueMaps.chemControllerWarnings.toArray().filter(el => [0, 8].includes(el.val)),
280
- chlorinatorCommsError: sys.board.valueMaps.chemControllerWarnings.toArray().filter(el => [0, 16].includes(el.val)),
281
- }
282
- let opts = {
283
- types: sys.board.valueMaps.chemControllerTypes.toArray(),
284
- bodies: sys.board.bodies.getBodyAssociations(),
285
- tempUnits: sys.board.valueMaps.tempUnits.toArray(),
286
- status: sys.board.valueMaps.chemControllerStatus.toArray(),
287
- pumpTypes: sys.board.valueMaps.chemPumpTypes.toArray(),
288
- phSupplyTypes: sys.board.valueMaps.phSupplyTypes.toArray(),
289
- volumeUnits: sys.board.valueMaps.volumeUnits.toArray(),
290
- dosingMethods: sys.board.valueMaps.chemDosingMethods.toArray(),
291
- chlorDosingMethods: sys.board.valueMaps.chemChlorDosingMethods.toArray(),
292
- orpProbeTypes: sys.board.valueMaps.chemORPProbeTypes.toArray(),
293
- phProbeTypes: sys.board.valueMaps.chemPhProbeTypes.toArray(),
294
- flowSensorTypes: sys.board.valueMaps.flowSensorTypes.toArray(),
295
- acidTypes: sys.board.valueMaps.acidTypes.toArray(),
296
- remServers,
297
- dosingStatus: sys.board.valueMaps.chemControllerDosingStatus.toArray(),
298
- siCalcTypes: sys.board.valueMaps.siCalcTypes.toArray(),
299
- alarms,
300
- warnings,
301
- // waterFlow: sys.board.valueMaps.chemControllerWaterFlow.toArray(), // remove
302
- controllers: sys.chemControllers.get(),
303
- maxChemControllers: sys.equipment.maxChemControllers,
304
- doserTypes: sys.board.valueMaps.chemDoserTypes.toArray(),
305
- chlorinators: sys.chlorinators.get(),
306
- };
307
- return res.status(200).send(opts);
308
- }
309
- catch (err) { next(err); }
310
- });
311
- app.get('/config/options/chemDosers', async (req, res, next) => {
312
- try {
313
- let remServers = await sys.ncp.getREMServers();
314
- let opts = {
315
- bodies: sys.board.bodies.getBodyAssociations(),
316
- tempUnits: sys.board.valueMaps.tempUnits.toArray(),
317
- status: sys.board.valueMaps.chemDoserStatus.toArray(),
318
- pumpTypes: sys.board.valueMaps.chemPumpTypes.toArray(),
319
- volumeUnits: sys.board.valueMaps.volumeUnits.toArray(),
320
- flowSensorTypes: sys.board.valueMaps.flowSensorTypes.toArray(),
321
- remServers,
322
- dosingStatus: sys.board.valueMaps.chemDoserDosingStatus.toArray(),
323
- dosers: sys.chemDosers.get(),
324
- doserTypes: sys.board.valueMaps.chemDoserTypes.toArray(),
325
- maxChemDosers: sys.equipment.maxChemDosers
326
- };
327
- return res.status(200).send(opts);
328
- }
329
- catch (err) { next(err); }
330
- });
331
-
332
- app.get('/config/options/rem', async (req, res, next) => {
333
- try {
334
- let opts = {
335
- servers: await sys.ncp.getREMServers()
336
- }
337
- return res.status(200).send(opts);
338
- } catch (err) { next(err); }
339
- });
340
- app.get('/config/options/controllerType', async (req, res, next) => {
341
- try {
342
- let opts = {
343
- controllerType: sys.controllerType,
344
- type: state.controllerState,
345
- equipment: sys.equipment.get(),
346
- controllerTypes: sys.getAvailableControllerTypes()
347
- }
348
- return res.status(200).send(opts);
349
- } catch (err) { next(err); }
350
- });
351
- app.get('/config/options/anslq25ControllerType', async (req, res, next) => {
352
- try {
353
- let opts = {
354
- // controllerType: typeof sys.anslq25.controllerType === 'undefined' ? '' : sys.anslq25.controllerType,
355
- // model: typeof sys.anslq25.model === 'undefined' ? '' : sys.anslq25.model,
356
- // equipment: sys.equipment.get(),
357
- ...sys.anslq25.get(true),
358
- controllerTypes: sys.getAvailableControllerTypes(['easytouch', 'intellitouch', 'intellicenter']),
359
- rs485ports: await conn.listInstalledPorts()
360
- }
361
- return res.status(200).send(opts);
362
- } catch (err) { next(err); }
363
- });
364
- app.get('/config/options/chlorinators', async (req, res, next) => {
365
- try {
366
- let opts = {
367
- types: sys.board.valueMaps.chlorinatorType.toArray(),
368
- bodies: sys.board.bodies.getBodyAssociations(),
369
- chlorinators: sys.chlorinators.get(),
370
- maxChlorinators: sys.equipment.maxChlorinators,
371
- models: sys.board.valueMaps.chlorinatorModel.toArray(),
372
- equipmentMasters: sys.board.valueMaps.equipmentMaster.toArray(),
373
- rs485ports: await conn.listInstalledPorts()
374
- };
375
- return res.status(200).send(opts);
376
- } catch (err) { next(err); }
377
- });
378
- app.get('/config/options/dateTime', (req, res) => {
379
- let opts = {
380
- dow: sys.board.system.getDOW()
381
- }
382
- return res.status(200).send(opts);
383
- });
384
- app.get('/app/options/logger', (req, res) => {
385
- let opts = {
386
- logger: config.getSection('log')
387
- }
388
- return res.status(200).send(opts);
389
- });
390
- app.get('/app/all/', (req, res) => {
391
- let opts = config.getSection();
392
- return res.status(200).send(opts);
393
- });
394
- app.get('/config/options/tempSensors', (req, res) => {
395
- let opts = {
396
- tempUnits: sys.board.valueMaps.tempUnits.toArray(),
397
- sensors: sys.board.system.getSensors()
398
- };
399
- return res.status(200).send(opts);
400
- });
401
- app.get('/config/options/filters', async (req, res, next) => {
402
- try {
403
- let opts = {
404
- types: sys.board.valueMaps.filterTypes.toArray(),
405
- bodies: sys.board.bodies.getBodyAssociations(),
406
- filters: sys.filters.get(),
407
- areaUnits: sys.board.valueMaps.areaUnits.toArray(),
408
- pressureUnits: sys.board.valueMaps.pressureUnits.toArray(),
409
- circuits: sys.board.circuits.getCircuitReferences(true, true, true, false),
410
- servers: []
411
- };
412
- if (sys.controllerType === ControllerType.Nixie) opts.servers = await sys.ncp.getREMServers();
413
- return res.status(200).send(opts);
414
- } catch (err) { next(err); }
415
- });
416
- /******* END OF CONFIGURATION PICK LISTS/REFERENCES AND VALIDATION ***********/
417
- /******* ENDPOINTS FOR MODIFYING THE OUTDOOR CONTROL PANEL SETTINGS **********/
418
- app.put('/config/rem', async (req, res, next) => {
419
- try {
420
- // RSG: this is problematic because we now enable multiple rem type interfaces that may not be called REM.
421
- // This is now also a dupe of PUT /app/interface and should be consolidated
422
- // config.setSection('web.interfaces.rem', req.body);
423
- config.setInterface(req.body);
424
- }
425
- catch (err) { next(err); }
426
- })
427
- app.put('/config/tempSensors', async (req, res, next) => {
428
- try {
429
- await sys.board.system.setTempSensorsAsync(req.body);
430
- let opts = {
431
- tempUnits: sys.board.valueMaps.tempUnits.toArray(),
432
- sensors: sys.board.system.getSensors()
433
- };
434
- return res.status(200).send(opts);
435
- }
436
- catch (err) { next(err); }
437
- });
438
- app.put('/config/filter', async (req, res, next) => {
439
- try {
440
- let sfilter = await sys.board.filters.setFilterAsync(req.body);
441
- return res.status(200).send(sfilter.get(true));
442
- }
443
- catch (err) { next(err); }
444
- });
445
- app.put('/config/controllerType', async (req, res, next) => {
446
- try {
447
- let controller = await sys.board.setControllerType(req.body);
448
- return res.status(200).send(controller.get(true));
449
- } catch (err) { next(err); }
450
- });
451
- app.put('/config/anslq25ControllerType', async (req, res, next) => {
452
- try {
453
- // sys.anslq25ControllerType
454
- await sys.anslq25Board.setAnslq25Async(req.body);
455
- return res.status(200).send(sys.anslq25.get(true));
456
- } catch (err) { next(err); }
457
- });
458
- app.delete('/config/filter', async (req, res, next) => {
459
- try {
460
- let sfilter = await sys.board.filters.deleteFilterAsync(req.body);
461
- return res.status(200).send(sfilter.get(true));
462
- }
463
- catch (err) { next(err); }
464
- });
465
- app.put('/config/general', async (req, res, next) => {
466
- // Change the options for the pool.
467
- try {
468
- let rc = await sys.board.system.setGeneralAsync(req.body);
469
- let opts = {
470
- countries: sys.board.valueMaps.countries.toArray(),
471
- tempUnits: sys.board.valueMaps.tempUnits.toArray(),
472
- timeZones: sys.board.valueMaps.timeZones.toArray(),
473
- clockSources: sys.board.valueMaps.clockSources.toArray(),
474
- clockModes: sys.board.valueMaps.clockModes.toArray(),
475
- pool: sys.general.get(true),
476
- sensors: sys.board.system.getSensors()
477
- };
478
- return res.status(200).send(opts);
479
- }
480
- catch (err) { next(err); }
481
- });
482
-
483
- app.put('/config/valve', async (req, res, next) => {
484
- // Update a valve.
485
- try {
486
- let valve = await sys.board.valves.setValveAsync(req.body);
487
- return res.status(200).send((valve).get(true));
488
- }
489
- catch (err) { next(err); }
490
- });
491
- app.delete('/config/valve', async (req, res, next) => {
492
- // Update a valve.
493
- try {
494
- let valve = await sys.board.valves.deleteValveAsync(req.body);
495
- return res.status(200).send((valve).get(true));
496
- }
497
- catch (err) { next(err); }
498
- });
499
-
500
- app.put('/config/body', async (req, res, next) => {
501
- // Change the body attributes.
502
- try {
503
- let body = await sys.board.bodies.setBodyAsync(req.body);
504
- return res.status(200).send((body).get(true));
505
- }
506
- catch (err) { next(err); }
507
- });
508
- app.put('/config/circuit', async (req, res, next) => {
509
- // add/update a circuit
510
- try {
511
- let circuit = await sys.board.circuits.setCircuitAsync(req.body);
512
- return res.status(200).send((circuit).get(true));
513
- }
514
- catch (err) { next(err); }
515
- });
516
- app.delete('/config/circuit', async (req, res, next) => {
517
- // delete a circuit
518
- try {
519
- let circuit = await sys.board.circuits.deleteCircuitAsync(req.body);
520
- return res.status(200).send((circuit).get(true));
521
- }
522
- catch (err) { next(err); }
523
- });
524
- app.put('/config/feature', async (req, res, next) => {
525
- // add/update a feature
526
- try {
527
- let feature = await sys.board.features.setFeatureAsync(req.body);
528
- return res.status(200).send((feature).get(true));
529
- }
530
- catch (err) { next(err); }
531
- });
532
- app.delete('/config/feature', async (req, res, next) => {
533
- // delete a feature
534
- try {
535
- let feature = await sys.board.features.deleteFeatureAsync(req.body);
536
- return res.status(200).send((feature).get(true));
537
- }
538
- catch (err) { next(err); }
539
- });
540
- app.put('/config/circuitGroup', async (req, res, next) => {
541
- // add/update a circuitGroup
542
- try {
543
- let group = await sys.board.circuits.setCircuitGroupAsync(req.body);
544
- return res.status(200).send((group).get(true));
545
- }
546
- catch (err) { next(err); }
547
- });
548
- app.delete('/config/circuitGroup', async (req, res, next) => {
549
- try {
550
- let group = await sys.board.circuits.deleteCircuitGroupAsync(req.body);
551
- return res.status(200).send((group).get(true));
552
- }
553
- catch (err) { next(err); }
554
- });
555
- app.put('/config/lightGroup', async (req, res, next) => {
556
- try {
557
- let group = await sys.board.circuits.setLightGroupAsync(req.body);
558
- return res.status(200).send((group).get(true));
559
- }
560
- catch (err) { next(err); }
561
- });
562
- app.delete('/config/lightGroup', async (req, res, next) => {
563
- try {
564
- let group = await sys.board.circuits.deleteLightGroupAsync(req.body);
565
- return res.status(200).send((group).get(true));
566
- }
567
- catch (err) { next(err); }
568
- });
569
- app.put('/config/pump', async (req, res, next) => {
570
- // Change the pump attributes. This will add the pump if it doesn't exist, set
571
- // any affiliated circuits and maintain all attribututes of the pump.
572
- // RSG: Caveat - you have to send none or all of the pump circuits or any not included be deleted.
573
- try {
574
- let pump = await sys.board.pumps.setPumpAsync(req.body);
575
- return res.status(200).send((pump).get(true));
576
- }
577
- catch (err) { next(err); }
578
- });
579
- app.put('/config/pumpCircuit', async (req, res, next) => {
580
- try {
581
- let pmpId = parseInt(req.body.pumpId, 10);
582
- let circId = parseInt(req.body.circuitId, 10);
583
- let pmp: Pump;
584
- if (isNaN(pmpId)) {
585
- let pmpAddress = parseInt(req.body.address, 10);
586
- if (!isNaN(pmpAddress)) pmp = sys.pumps.find(x => x.address === pmpAddress);
587
- }
588
- else
589
- pmp = sys.pumps.find(x => x.id === pmpId);
590
- if (typeof pmp === 'undefined') throw new ServiceProcessError(`Pump not found`, '/config/pumpCircuit', 'Set circuit speed');
591
- let data = pmp.get(true);
592
- let c = typeof data.circuits !== 'undefined' && typeof data.circuits.find !== 'undefined' ? data.circuits.find(x => x.circuit === circId) : undefined;
593
- if (typeof c === 'undefined') throw new ServiceProcessError(`Circuit not found`, '/config/pumpCircuit', 'Set circuit speed');
594
- if (typeof req.body.speed !== 'undefined') {
595
- let speed = parseInt(req.body.speed, 10);
596
- if (isNaN(speed)) throw new ServiceProcessError(`Invalid circuit speed supplied`, '/config/pumpCircuit', 'Set circuit speed');
597
- c.speed = speed;
598
- }
599
- else if (typeof req.body.flow !== 'undefined') {
600
- let flow = parseInt(req.body.flow, 10);
601
- if (isNaN(flow)) throw new ServiceProcessError(`Invalid circuit flow supplied`, '/config/pumpCircuit', 'Set circuit flow');
602
- c.flow = flow;
603
- }
604
- else {
605
- throw new ServiceProcessError(`You must supply a target flow or speed`, '/config/pumpCircuit', 'Set circuit flow');
606
- }
607
- await sys.board.pumps.setPumpAsync(data);
608
- return res.status(200).send((pmp).get(true));
609
- } catch (err) { next(err); }
610
-
611
- });
612
- // RKS: 05-20-22 This is a remnant of the old web ui. It is not called and the setType method needed to go away.
613
- //app.delete('/config/pump/:pumpId', async (req, res, next) => {
614
- // try {
615
- // let pump = sys.pumps.getItemById(parseInt(req.params.pumpId, 10));
616
- // await sys.board.pumps.deletePumpAsync()
617
- // if (pump.type === 0) {
618
- // return res.status(500).send(`Pump ${pump.id} not active`);
619
- // }
620
- // pump.setType(0);
621
- // return res.status(200).send('OK');
622
- // } catch (err) { next(err); }
623
- //});
624
- app.delete('/config/pump', async (req, res, next) => {
625
- try {
626
- let pump = await sys.board.pumps.deletePumpAsync(req.body);
627
- return res.status(200).send((pump).get(true));
628
- }
629
- catch (err) { next(err); }
630
- });
631
- app.put('/config/customNames', async (req, res, next) => {
632
- try {
633
- let names = await sys.board.system.setCustomNamesAsync(req.body);
634
- return res.status(200).send(names.get());
635
- }
636
- catch (err) { next(err); }
637
- });
638
- app.put('/config/customName', async (req, res, next) => {
639
- try {
640
- let name = await sys.board.system.setCustomNameAsync(req.body);
641
- return res.status(200).send(name.get(true));
642
- }
643
- catch (err) { next(err); }
644
- });
645
- app.get('/config/schedule/:id', (req, res) => {
646
- let schedId = parseInt(req.params.id || '0', 10);
647
- let sched = sys.schedules.getItemById(schedId).get(true);
648
- return res.status(200).send(sched);
649
- });
650
- app.put('/config/schedule', async (req, res, next) => {
651
- try {
652
- let sched = await sys.board.schedules.setScheduleAsync(req.body);
653
- return res.status(200).send((sched as Schedule).get(true));
654
- }
655
- catch (err) { next(err); }
656
- });
657
- app.delete('/config/schedule', async (req, res, next) => {
658
- try {
659
- let sched = await sys.board.schedules.deleteScheduleAsync(req.body);
660
- return res.status(200).send((sched as Schedule).get(true));
661
- }
662
- catch (err) {
663
- //console.log(`Error deleting schedule... ${err}`);
664
- next(err);
665
- }
666
- });
667
- app.put('/config/chlorinator', async (req, res, next) => {
668
- try {
669
- let chlor = await sys.board.chlorinator.setChlorAsync(req.body);
670
- return res.status(200).send(sys.chlorinators.getItemById(chlor.id).get(true));
671
- }
672
- catch (err) { next(err); }
673
- });
674
- app.delete('/config/chlorinator', async (req, res, next) => {
675
- try {
676
- let chlor = await sys.board.chlorinator.deleteChlorAsync(req.body);
677
- return res.status(200).send(chlor.get(true));
678
- }
679
- catch (err) { next(err); }
680
- });
681
- app.put('/config/heater', async (req, res, next) => {
682
- try {
683
- let heater = await sys.board.heaters.setHeaterAsync(req.body);
684
- return res.status(200).send(sys.heaters.getItemById(heater.id).get(true));
685
- }
686
- catch (err) { next(err); }
687
- });
688
- app.delete('/config/heater', async (req, res, next) => {
689
- try {
690
- let heater = await sys.board.heaters.deleteHeaterAsync(req.body);
691
- return res.status(200).send((heater as Heater).get(true));
692
- }
693
- catch (err) { next(err); }
694
- });
695
-
696
- /***** END OF ENDPOINTS FOR MODIFYINC THE OUTDOOR CONTROL PANEL SETTINGS *****/
697
-
698
-
699
-
700
- app.get('/config/circuits/names', (req, res) => {
701
- let circuitNames = sys.board.circuits.getCircuitNames();
702
- return res.status(200).send(circuitNames);
703
- });
704
- app.get('/config/circuit/functions', (req, res) => {
705
- let circuitFunctions = sys.board.circuits.getCircuitFunctions();
706
- return res.status(200).send(circuitFunctions);
707
- });
708
- app.get('/config/features/functions', (req, res) => {
709
- let featureFunctions = sys.board.features.getFeatureFunctions();
710
- return res.status(200).send(featureFunctions);
711
- });
712
- app.get('/config/circuit/:id', (req, res) => {
713
- // todo: need getInterfaceById.get() in case features are requested here
714
- // todo: it seems to make sense to combine with /state/circuit/:id as they both have similiar/overlapping info
715
- return res.status(200).send(sys.circuits.getItemById(parseInt(req.params.id, 10)).get());
716
- });
717
- app.get('/config/circuit/:id/lightThemes', (req, res) => {
718
- let circuit = sys.circuits.getInterfaceById(parseInt(req.params.id, 10));
719
- let themes = typeof circuit !== 'undefined' && typeof circuit.getLightThemes === 'function' ? circuit.getLightThemes(circuit.type) : [];
720
- return res.status(200).send(themes);
721
- });
722
- app.get('/config/circuit/:id/lightCommands', (req, res) => {
723
- let circuit = sys.circuits.getInterfaceById(parseInt(req.params.id, 10));
724
- let commands = typeof circuit !== 'undefined' && typeof circuit.getLightThemes === 'function' ? circuit.getLightCommands(circuit.type) : [];
725
- return res.status(200).send(commands);
726
- });
727
-
728
- app.get('/config/chlorinator/:id', (req, res) => {
729
- return res.status(200).send(sys.chlorinators.getItemById(parseInt(req.params.id, 10)).get());
730
- });
731
- app.get('/config/chlorinators/search', async (req, res, next) => {
732
- // Change the options for the pool.
733
- try {
734
- //await sys.board.virtualChlorinatorController.search();
735
- return res.status(200).send(sys.chlorinators.getItemById(1).get());
736
- }
737
- catch (err) {
738
- next(err);
739
- }
740
- });
741
- app.put('/config/dateTime', async (req, res, next) => {
742
- try {
743
- let time = await sys.updateControllerDateTimeAsync(req.body);
744
- return res.status(200).send(time);
745
- }
746
- catch (err) { next(err); }
747
- });
748
- app.get('/config/lightGroups/themes', (req, res) => {
749
- // RSG: is this and /config/circuit/:id/lightThemes both needed?
750
- let grp = sys.lightGroups.getItemById(parseInt(req.body.id, 10));
751
- return res.status(200).send(grp.getLightThemes());
752
- });
753
- app.get('/config/lightGroups/commands', (req, res) => {
754
- let grp = sys.lightGroups.getItemById(parseInt(req.body.id, 10));
755
- return res.status(200).send(grp.getLightCommands());
756
- });
757
- app.get('/config/lightGroup/:id', (req, res) => {
758
- // if (sys.controllerType === ControllerType.IntelliCenter) {
759
- let grp = sys.lightGroups.getItemById(parseInt(req.params.id, 10));
760
- return res.status(200).send(grp.getExtended());
761
- // }
762
- // else
763
- // return res.status(200).send(sys.intellibrite.getExtended());
764
- });
765
- app.get('/config/lightGroup/colors', (req, res) => {
766
- return res.status(200).send(sys.board.valueMaps.lightColors.toArray());
767
- });
768
- app.put('/config/lightGroup/:id/setColors', async (req, res, next) => {
769
- try {
770
- let id = parseInt(req.params.id, 10);
771
- let grp = extend(true, { id: id }, req.body);
772
- await sys.board.circuits.setLightGroupAttribsAsync(grp);
773
- return res.status(200).send(sys.lightGroups.getItemById(id).getExtended());
774
- }
775
- catch (err) { next(err); }
776
- });
777
- app.get('/config/intellibrite/themes', (req, res) => {
778
- return res.status(200).send(sys.board.circuits.getLightThemes(16));
779
- });
780
- app.get('/config/circuitGroup/:id', (req, res) => {
781
- let grp = sys.circuitGroups.getItemById(parseInt(req.params.id, 10));
782
- return res.status(200).send(grp.getExtended());
783
- });
784
- /* app.get('/config/chemController/search', async (req, res, next) => {
785
- // Change the options for the pool.
786
- try {
787
- let result = await sys.board.virtualChemControllers.search();
788
- return res.status(200).send(result);
789
- }
790
- catch (err) {
791
- next(err);
792
- }
793
- }); */
794
- app.put('/config/chemController', async (req, res, next) => {
795
- try {
796
- let chem = await sys.board.chemControllers.setChemControllerAsync(req.body);
797
- return res.status(200).send(chem.get());
798
- }
799
- catch (err) { next(err); }
800
- });
801
- app.put('/config/chemDoser', async (req, res, next) => {
802
- try {
803
- let doser = await sys.board.chemDosers.setChemDoserAsync(req.body);
804
- return res.status(200).send(doser.get());
805
- }
806
- catch (err) { next(err); }
807
-
808
- });
809
- app.put('/config/chemController/calibrateDose', async (req, res, next) => {
810
- try {
811
- let schem = await sys.board.chemControllers.calibrateDoseAsync(req.body);
812
- return res.status(200).send(schem.getExtended());
813
- }
814
- catch (err) { next(err); }
815
- });
816
- app.put('/config/chemDoser/calibrateDose', async (req, res, next) => {
817
- try {
818
- let schem = await sys.board.chemDosers.calibrateDoseAsync(req.body);
819
- return res.status(200).send(schem.getExtended());
820
- }
821
- catch (err) { next(err); }
822
- });
823
- app.put('/config/chemController/feed', async (req, res, next) => {
824
- try {
825
- let chem = await sys.board.chemControllers.setChemControllerAsync(req.body);
826
- return res.status(200).send(chem.get());
827
- }
828
- catch (err) { next(err); }
829
- });
830
- app.delete('/config/chemController', async (req, res, next) => {
831
- try {
832
- let chem = await sys.board.chemControllers.deleteChemControllerAsync(req.body);
833
- return res.status(200).send(chem.get());
834
- }
835
- catch (err) { next(err); }
836
- });
837
- app.delete('/config/chemDoser', async (req, res, next) => {
838
- try {
839
- let doser = await sys.board.chemDosers.deleteChemDoserAsync(req.body);
840
- return res.status(200).send(doser.get());
841
- }
842
- catch (err) { next(err); }
843
-
844
- });
845
-
846
- /* app.get('/config/intellibrite', (req, res) => {
847
- return res.status(200).send(sys.intellibrite.getExtended());
848
- });
849
- app.get('/config/intellibrite/colors', (req, res) => {
850
- return res.status(200).send(sys.board.valueMaps.lightColors.toArray());
851
- });
852
- app.put('/config/intellibrite/setColors', (req, res) => {
853
- let grp = extend(true, { id: 0 }, req.body);
854
- sys.board.circuits.setIntelliBriteColors(new LightGroup(grp));
855
- return res.status(200).send('OK');
856
- }); */
857
- app.get('/config', (req, res) => {
858
- return res.status(200).send(sys.getSection('all'));
859
- });
860
- app.get('/config/:section', (req, res) => {
861
- return res.status(200).send(sys.getSection(req.params.section));
862
- });
863
-
864
-
865
- /******* ENDPOINTS FOR MANAGING THE poolController APPLICATION *********/
866
- app.put('/app/logger/setOptions', (req, res) => {
867
- logger.setOptions(req.body);
868
- return res.status(200).send(logger.options);
869
- });
870
- app.put('/app/logger/clearMessages', (req, res) => {
871
- logger.clearMessages();
872
- return res.status(200).send('OK');
873
- });
874
- app.get('/app/messages/broadcast/actions', (req, res) => {
875
- return res.status(200).send(sys.board.valueMaps.msgBroadcastActions.toArray());
876
- });
877
- app.put('/app/config/reload', (req, res) => {
878
- sys.board.reloadConfig();
879
- return res.status(200).send('OK');
880
- });
881
- app.put('/app/interface', async (req, res, next) => {
882
- try {
883
- let iface = await webApp.updateServerInterface(req.body);
884
- return res.status(200).send(iface);
885
- }
886
- catch (err) { next(err); }
887
- });
888
- app.put('/app/rs485Port', async (req, res, next) => {
889
- try {
890
- let port = await conn.setPortAsync(req.body);
891
- return res.status(200).send(port);
892
- }
893
- catch (err) { next(err); }
894
- });
895
- // app.put('/app/screenlogic', async (req, res, next) => {
896
- // try {
897
- // let screenlogic = await sl.setScreenlogicAsync(req.body);
898
- // return res.status(200).send(screenlogic);
899
- // }
900
- // catch (err) { next(err); }
901
- // });
902
- app.delete('/app/rs485Port', async (req, res, next) => {
903
- try {
904
- let port = await conn.deleteAuxPort(req.body);
905
- return res.status(200).send(port);
906
- }
907
- catch (err) { next(err); }
908
- });
909
- app.get('/app/config/startPacketCapture', (req, res) => {
910
- startPacketCapture(true);
911
- return res.status(200).send('OK');
912
- });
913
- app.get('/app/config/startPacketCaptureWithoutReset', (req, res) => {
914
- startPacketCapture(false);
915
- return res.status(200).send('OK');
916
- });
917
- app.get('/app/config/stopPacketCapture', async (req, res, next) => {
918
- try {
919
- let file = await stopPacketCaptureAsync();
920
- res.download(file);
921
- }
922
- catch (err) { next(err); }
923
- });
924
- app.get('/app/config/:section', (req, res) => {
925
- return res.status(200).send(config.getSection(req.params.section));
926
- });
927
- app.get('/app/config/options/backup', async (req, res, next) => {
928
- try {
929
- let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0 }, keepCount: 5, servers: [] });
930
- let servers = await sys.ncp.getREMServers();
931
- if (typeof servers !== 'undefined') {
932
- // Just in case somebody deletes the backup section and doesn't put it back properly.
933
- for (let i = 0; i < servers.length; i++) {
934
- let srv = servers[i];
935
- 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 });
936
- }
937
- for (let i = opts.servers.length - 1; i >= 0; i--) {
938
- let srv = opts.servers[i];
939
- if (typeof servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.splice(i, 1);
940
- }
941
- }
942
- if (typeof opts.servers === 'undefined') opts.servers = [];
943
- return res.status(200).send(opts);
944
- } catch (err) { next(err); }
945
- });
946
- app.get('/app/config/options/restore', async (req, res, next) => {
947
- try {
948
- let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0 }, keepCount: 5, servers: [], backupFiles: [] });
949
- let servers = await sys.ncp.getREMServers();
950
- if (typeof servers !== 'undefined') {
951
- for (let i = 0; i < servers.length; i++) {
952
- let srv = servers[i];
953
- if (typeof opts.servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.push({ name: srv.name, uuid: srv.uuid, backup: false });
954
- }
955
- for (let i = opts.servers.length - 1; i >= 0; i--) {
956
- let srv = opts.servers[i];
957
- if (typeof servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.splice(i, 1);
958
- }
959
- }
960
- if (typeof opts.servers === 'undefined') opts.servers = [];
961
- opts.backupFiles = await webApp.readBackupFiles();
962
- return res.status(200).send(opts);
963
- } catch (err) { next(err); }
964
-
965
- });
966
- app.put('/app/config/options/backup', async (req, res, next) => {
967
- try {
968
- config.setSection('controller.backups', req.body);
969
- let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0 }, keepCount: 5, servers: [] });
970
- webApp.autoBackup = utils.makeBool(opts.automatic);
971
- await webApp.checkAutoBackup();
972
- return res.status(200).send(opts);
973
- } catch (err) { next(err); }
974
-
975
- });
976
- app.put('/app/config/createBackup', async (req, res, next) => {
977
- try {
978
- let ret = await webApp.backupServer(req.body);
979
- res.download(ret.filePath);
980
- }
981
- catch (err) { next(err); }
982
- });
983
- app.delete('/app/backup/file', async (req, res, next) => {
984
- try {
985
- let opts = req.body;
986
- fs.unlinkSync(opts.filePath);
987
- return res.status(200).send(opts);
988
- }
989
- catch (err) { next(err); }
990
- });
991
- app.post('/app/backup/file', async (req, res, next) => {
992
- try {
993
- let file = multer({
994
- limits: { fileSize: 1000000 },
995
- storage: multer.memoryStorage()
996
- }).single('backupFile');
997
- file(req, res, async (err) => {
998
- try {
999
- if (err) { next(err); }
1000
- else {
1001
- // Validate the incoming data and save it off only if it is valid.
1002
- let bf = await BackupFile.fromBuffer(req.file.originalname, req.file.buffer);
1003
- if (typeof bf === 'undefined') {
1004
- err = new ServiceProcessError(`Invalid backup file: ${req.file.originalname}`, 'POST: app/backup/file', 'extractBackupOptions');
1005
- next(err);
1006
- }
1007
- else {
1008
- if (fs.existsSync(bf.filePath))
1009
- return next(new ServiceProcessError(`File already exists ${req.file.originalname}`, 'POST: app/backup/file', 'writeFile'));
1010
- else {
1011
- try {
1012
- fs.writeFileSync(bf.filePath, new Uint8Array(req.file.buffer));
1013
- } catch (e) { logger.error(`Error writing backup file ${e.message}`); }
1014
- }
1015
- return res.status(200).send(bf);
1016
- }
1017
- }
1018
- } catch (e) {
1019
- err = new ServiceProcessError(`Error uploading file: ${e.message}`, 'POST: app/backup/file', 'uploadFile');
1020
- next(err);
1021
- logger.error(`Error uploading file ${e.message}`);
1022
- }
1023
- });
1024
- } catch (err) { next(err); }
1025
- });
1026
- app.put('/app/restore/validate', async (req, res, next) => {
1027
- try {
1028
- // Validate all the restore options.
1029
- let opts = req.body;
1030
- let ctx = await webApp.validateRestore(opts);
1031
- return res.status(200).send(ctx);
1032
- } catch (err) { next(err); }
1033
- });
1034
- app.put('/app/restore/file', async (req, res, next) => {
1035
- try {
1036
- let opts = req.body;
1037
- let results = await webApp.restoreServers(opts);
1038
- return res.status(200).send(results);
1039
- } catch (err) { next(err); }
1040
- });
1041
- app.put('/app/anslq25', async(req, res, next) => {
1042
- try {
1043
- await sys.anslq25Board.setAnslq25Async(req.body);
1044
- return res.status(200).send(sys.anslq25.get(true));
1045
- } catch (err) { next(err); }
1046
- });
1047
- app.delete('/app/anslq25', async(req, res, next) => {
1048
- try {
1049
- await sys.anslq25Board.deleteAnslq25Async(req.body);
1050
- return res.status(200).send(sys.anslq25.get(true));
1051
- } catch (err) { next(err); }
1052
- });
1053
- }
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 fs from "fs";
19
+ import * as path from "path";
20
+ import * as express from "express";
21
+ import * as extend from 'extend';
22
+ import * as multer from 'multer';
23
+ import { sys, LightGroup, ControllerType, Pump, Valve, Body, General, Circuit, ICircuit, Feature, CircuitGroup, CustomNameCollection, Schedule, Chlorinator, Heater, Screenlogic } from "../../../controller/Equipment";
24
+ import { config } from "../../../config/Config";
25
+ import { logger } from "../../../logger/Logger";
26
+ import { utils } from "../../../controller/Constants";
27
+ import { ServiceProcessError } from "../../../controller/Errors";
28
+ import { state } from "../../../controller/State";
29
+ import { stopPacketCaptureAsync, startPacketCapture } from '../../../app';
30
+ import { conn } from "../../../controller/comms/Comms";
31
+ import { webApp, BackupFile, RestoreFile } from "../../Server";
32
+ import { release } from "os";
33
+ import { ScreenLogicComms, sl } from "../../../controller/comms/ScreenLogic";
34
+ import { screenlogic } from "node-screenlogic";
35
+
36
+ export class ConfigRoute {
37
+ private static securitySessions: Map<string, any> = new Map<string, any>();
38
+ private static isOcpWriteSecurityEnforced(): boolean {
39
+ // Intentionally hard-disabled until OCP security semantics are fully understood.
40
+ // This avoids accidentally locking users out of configuration writes.
41
+ return false;
42
+ }
43
+ private static getClientKey(req: express.Request): string {
44
+ const forwarded = (req.headers['x-forwarded-for'] || '') as string;
45
+ const forwardedIp = forwarded.split(',')[0].trim();
46
+ const connIp = ((req.connection as any) || {}).remoteAddress || '';
47
+ return forwardedIp || req.ip || connIp || 'unknown';
48
+ }
49
+ private static getSecuritySession(req: express.Request): any {
50
+ const key = ConfigRoute.getClientKey(req);
51
+ return ConfigRoute.securitySessions.get(key);
52
+ }
53
+ private static clearSecuritySession(req: express.Request): any {
54
+ const key = ConfigRoute.getClientKey(req);
55
+ ConfigRoute.securitySessions.delete(key);
56
+ return {
57
+ isAuthenticated: false,
58
+ isAdmin: false,
59
+ roleId: 0,
60
+ roleName: '',
61
+ ip: key
62
+ };
63
+ }
64
+ private static createSecuritySession(req: express.Request, role: any): any {
65
+ const key = ConfigRoute.getClientKey(req);
66
+ const session = {
67
+ isAuthenticated: true,
68
+ isAdmin: role.id === 1 || typeof role.name === 'string' && role.name.toLowerCase().indexOf('admin') >= 0,
69
+ roleId: role.id || 0,
70
+ roleName: role.name || '',
71
+ ip: key,
72
+ updatedAt: new Date().toISOString()
73
+ };
74
+ ConfigRoute.securitySessions.set(key, session);
75
+ return session;
76
+ }
77
+ private static getRoleForPin(pin: string): any {
78
+ const normalizedPin = (pin || '').toString().replace(/\D/g, '');
79
+ if (normalizedPin.length === 0) return undefined;
80
+ const roles = sys.security.roles.toArray();
81
+ for (let i = 0; i < roles.length; i++) {
82
+ const rolePin = ((roles[i] as any).pin || '').toString().replace(/\D/g, '');
83
+ if (rolePin.length > 0 && rolePin === normalizedPin) return roles[i];
84
+ }
85
+ return undefined;
86
+ }
87
+ private static validateWriteAccess(req: express.Request): { allowed: boolean; reason?: string; session?: any } {
88
+ if (!ConfigRoute.isOcpWriteSecurityEnforced()) return { allowed: true };
89
+ if (!sys.security.enabled) return { allowed: true };
90
+ const session = ConfigRoute.getSecuritySession(req);
91
+ if (typeof session === 'undefined' || !session.isAuthenticated) {
92
+ return { allowed: false, reason: 'Security is enabled; log in with an administrator PIN to change configuration.' };
93
+ }
94
+ if (!session.isAdmin) return { allowed: false, reason: 'Guest sessions are read-only.' };
95
+ return { allowed: true, session: session };
96
+ }
97
+ private static getSessionResponse(req: express.Request): any {
98
+ const session = ConfigRoute.getSecuritySession(req);
99
+ return {
100
+ enabled: sys.security.enabled,
101
+ session: typeof session === 'undefined' ? {
102
+ isAuthenticated: false,
103
+ isAdmin: false,
104
+ roleId: 0,
105
+ roleName: '',
106
+ ip: ConfigRoute.getClientKey(req)
107
+ } : session
108
+ };
109
+ }
110
+ public static initRoutes(app: express.Application) {
111
+ app.use('/config', (req, res, next) => {
112
+ const method = (req.method || '').toUpperCase();
113
+ if (method !== 'PUT' && method !== 'POST' && method !== 'DELETE') return next();
114
+ const reqPath = req.path || req.url || '';
115
+ if (
116
+ reqPath.startsWith('/security/login') ||
117
+ reqPath.startsWith('/security/logout') ||
118
+ reqPath.startsWith('/security/session')
119
+ ) {
120
+ return next();
121
+ }
122
+ const access = ConfigRoute.validateWriteAccess(req);
123
+ if (access.allowed) return next();
124
+ return res.status(403).send({
125
+ error: 'FORBIDDEN',
126
+ message: access.reason,
127
+ security: ConfigRoute.getSessionResponse(req)
128
+ });
129
+ });
130
+ app.get('/config/body/:body/heatModes', (req, res) => {
131
+ return res.status(200).send(sys.bodies.getItemById(parseInt(req.params.body, 10)).getHeatModes());
132
+ });
133
+ app.get('/config/circuit/names', (req, res) => {
134
+ let circuitNames = sys.board.circuits.getCircuitNames();
135
+ return res.status(200).send(circuitNames);
136
+ });
137
+ app.get('/config/circuit/references', (req, res) => {
138
+ let circuits = typeof req.query.circuits === 'undefined' || utils.makeBool(req.query.circuits);
139
+ let features = typeof req.query.features === 'undefined' || utils.makeBool(req.query.features);
140
+ let groups = typeof req.query.features === 'undefined' || utils.makeBool(req.query.groups);
141
+ let virtual = typeof req.query.virtual === 'undefined' || utils.makeBool(req.query.virtual);
142
+ return res.status(200).send(sys.board.circuits.getCircuitReferences(circuits, features, virtual, groups));
143
+ });
144
+
145
+ /******* CONFIGURATION PICK LISTS/REFERENCES and VALIDATION PARAMETERS *********/
146
+ /// Returns an object that contains the general options for setting up the panel.
147
+ app.get('/config/options/general', (req, res) => {
148
+ let opts = {
149
+ countries: sys.board.valueMaps.countries.toArray(),
150
+ tempUnits: sys.board.valueMaps.tempUnits.toArray(),
151
+ timeZones: sys.board.valueMaps.timeZones.toArray(),
152
+ clockSources: sys.board.valueMaps.clockSources.toArray(),
153
+ clockModes: sys.board.valueMaps.clockModes.toArray(),
154
+ pool: sys.general.get(true),
155
+ sensors: sys.board.system.getSensors(),
156
+ systemUnits: sys.board.valueMaps.systemUnits.toArray()
157
+ };
158
+ return res.status(200).send(opts);
159
+ });
160
+ app.get('/config/options/security', (req, res) => {
161
+ return res.status(200).send({
162
+ security: sys.security.get(true),
163
+ session: ConfigRoute.getSessionResponse(req).session
164
+ });
165
+ });
166
+ app.get('/config/options/alerts', (req, res) => {
167
+ return res.status(200).send({
168
+ alerts: sys.alerts.get(true),
169
+ // Existing app-side alert-related options still live in general options.
170
+ poolOptions: {
171
+ cooldownDelay: sys.general.options.cooldownDelay,
172
+ heaterStartDelay: sys.general.options.heaterStartDelay,
173
+ valveDelayTime: sys.general.options.valveDelayTime,
174
+ manualPriority: sys.general.options.manualPriority
175
+ },
176
+ runtime: {
177
+ chemControllers: state.chemControllers.getExtended(),
178
+ chemDosers: state.chemDosers.getExtended()
179
+ }
180
+ });
181
+ });
182
+ app.get('/config/options/rs485', async (req, res, next) => {
183
+ try {
184
+ let opts = { ports: [], local: [], screenlogic: {} }
185
+ let cfg = config.getSection('controller');
186
+ for (let section in cfg) {
187
+ if (section.startsWith('comms')) {
188
+ let cport = extend(true, { enabled: false, netConnect: false, mock: false }, cfg[section]);
189
+ let port = conn.findPortById(cport.portId || 0);
190
+ if (typeof cport.type === 'undefined'){
191
+ cport.type = cport.netConnect ? 'netConnect' : cport.mock ? 'mock' : 'local'
192
+ }
193
+ if (typeof port !== 'undefined') cport.stats = port.stats;
194
+ if (port.portId === 0 && port.type === 'screenlogic') {
195
+ cport.screenlogic.stats = sl.stats;
196
+ }
197
+ opts.ports.push(cport);
198
+ }
199
+ // if (section.startsWith('screenlogic')){
200
+ // let screenlogic = cfg[section];
201
+ // screenlogic.types = [{ val: 'local', name: 'Local', desc: 'Local Screenlogic' }, { val: 'remote', name: 'Remote', desc: 'Remote Screenlogic' }];
202
+ // screenlogic.stats = sl.stats;
203
+ // opts.screenlogic = screenlogic;
204
+ // }
205
+ }
206
+ opts.local = await conn.getLocalPortsAsync() || [];
207
+ return res.status(200).send(opts);
208
+ } catch (err) { next(err); }
209
+ });
210
+ // app.get('/config/options/screenlogic', async (req, res, next) => {
211
+ // try {
212
+ // let cfg = config.getSection('controller.screenlogic');
213
+ // let data = {
214
+ // cfg,
215
+ // types: [{ val: 'local', name: 'Local', desc: 'Local Screenlogic' }, { val: 'remote', name: 'Remote', desc: 'Remote Screenlogic' }]
216
+ // }
217
+ // return res.status(200).send(data);
218
+ // } catch (err) { next(err); }
219
+ // });
220
+ app.get('/config/options/screenlogic/search', async (req, res, next) => {
221
+ try {
222
+ let localUnits = await ScreenLogicComms.searchAsync();
223
+ return res.status(200).send(localUnits);
224
+ } catch (err) { next(err); }
225
+ });
226
+ app.get('/config/options/circuits', async (req, res, next) => {
227
+ try {
228
+ let opts = {
229
+ maxCircuits: sys.equipment.maxCircuits,
230
+ equipmentIds: sys.equipment.equipmentIds.circuits,
231
+ invalidIds: sys.board.equipmentIds.invalidIds.get(),
232
+ equipmentNames: sys.board.circuits.getCircuitNames(),
233
+ functions: sys.board.circuits.getCircuitFunctions(),
234
+ circuits: sys.circuits.get(),
235
+ controllerType: sys.controllerType,
236
+ servers: await sys.ncp.getREMServers()
237
+ };
238
+ return res.status(200).send(opts);
239
+ } catch (err) { next(err); }
240
+ });
241
+ app.get('/config/options/circuitGroups', (req, res) => {
242
+ let opts = {
243
+ maxCircuitGroups: sys.equipment.maxCircuitGroups,
244
+ equipmentNames: sys.board.circuits.getCircuitNames(),
245
+ circuits: sys.board.circuits.getCircuitReferences(true, true, false),
246
+ circuitGroups: sys.circuitGroups.get(),
247
+ circuitStates: sys.board.valueMaps.groupCircuitStates.toArray()
248
+ };
249
+ return res.status(200).send(opts);
250
+ });
251
+ app.get('/config/options/lightGroups', (req, res) => {
252
+ let opts = {
253
+ maxLightGroups: sys.equipment.maxLightGroups,
254
+ equipmentNames: sys.board.circuits.getCircuitNames(),
255
+ themes: sys.board.circuits.getLightThemes(),
256
+ colors: sys.board.valueMaps.lightColors.toArray(),
257
+ circuits: sys.board.circuits.getLightReferences(),
258
+ lightGroups: sys.lightGroups.get(),
259
+ functions: sys.board.circuits.getCircuitFunctions()
260
+ };
261
+ return res.status(200).send(opts);
262
+ });
263
+ app.get('/config/options/features', (req, res) => {
264
+ let opts = {
265
+ maxFeatures: sys.equipment.maxFeatures,
266
+ invalidIds: sys.board.equipmentIds.invalidIds.get(),
267
+ equipmentIds: sys.equipment.equipmentIds.features,
268
+ equipmentNames: sys.board.circuits.getCircuitNames(),
269
+ functions: sys.board.features.getFeatureFunctions(),
270
+ features: sys.features.get()
271
+ };
272
+ return res.status(200).send(opts);
273
+ });
274
+ app.get('/config/options/bodies', (req, res) => {
275
+ let opts = {
276
+ maxBodies: sys.equipment.maxBodies,
277
+ bodyTypes: sys.board.valueMaps.bodies.toArray(),
278
+ bodies: sys.bodies.get(),
279
+ capacityUnits: sys.board.valueMaps.volumeUnits.toArray()
280
+ };
281
+ return res.status(200).send(opts);
282
+ });
283
+ app.get('/config/options/valves', async (req, res, next) => {
284
+ try {
285
+ let opts = {
286
+ maxValves: sys.equipment.maxValves,
287
+ valveTypes: sys.board.valueMaps.valveTypes.toArray(),
288
+ circuits: sys.board.circuits.getCircuitReferences(true, true, true),
289
+ valves: sys.valves.get(),
290
+ servers: await sys.ncp.getREMServers()
291
+ };
292
+ opts.circuits.unshift({ id: 256, name: 'Unassigned', type: 0, equipmentType: 'circuit' });
293
+ return res.status(200).send(opts);
294
+ } catch (err) { next(err); }
295
+ });
296
+ app.get('/config/options/pumps', async (req, res, next) => {
297
+ try {
298
+ let opts: any = {
299
+ maxPumps: sys.equipment.maxPumps,
300
+ pumpUnits: sys.board.valueMaps.pumpUnits.toArray(),
301
+ pumpTypes: sys.board.valueMaps.pumpTypes.toArray(),
302
+ models: {
303
+ ss: sys.board.valueMaps.pumpSSModels.toArray(),
304
+ ds: sys.board.valueMaps.pumpDSModels.toArray(),
305
+ vs: sys.board.valueMaps.pumpVSModels.toArray(),
306
+ vf: sys.board.valueMaps.pumpVSModels.toArray(),
307
+ vsf: sys.board.valueMaps.pumpVSFModels.toArray(),
308
+ vssvrs: sys.board.valueMaps.pumpVSSVRSModels.toArray()
309
+ },
310
+ circuits: sys.board.circuits.getCircuitReferences(true, true, true, true),
311
+ bodies: sys.board.valueMaps.pumpBodies.toArray(),
312
+ pumps: sys.pumps.get(),
313
+ servers: await sys.ncp.getREMServers(),
314
+ rs485ports: await conn.listInstalledPorts()
315
+ };
316
+ // RKS: Why do we need the circuit names? We have the circuits. Is this so
317
+ // that we can name the pump. I thought that *Touch uses the pump type as the name
318
+ // plus a number.
319
+ // RG: because I need the name/val of the Not Used circuit for displaying pumpCircuits that are
320
+ // empty. EG A pump circuit can be not used even if all the circuits are used.
321
+ if (sys.controllerType !== ControllerType.IntelliCenter) {
322
+ opts.circuitNames = sys.board.circuits.getCircuitNames().filter(c => c.name === 'notused');
323
+ }
324
+ return res.status(200).send(opts);
325
+ } catch (err) { next(err); }
326
+ });
327
+ app.get('/config/options/schedules', async (req, res, next) => {
328
+ try {
329
+ let opts = {
330
+ maxSchedules: sys.equipment.maxSchedules,
331
+ tempUnits: sys.board.valueMaps.tempUnits.transform(state.temps.units),
332
+ scheduleTimeTypes: sys.board.valueMaps.scheduleTimeTypes.toArray(),
333
+ scheduleTypes: sys.board.valueMaps.scheduleTypes.toArray(),
334
+ scheduleDays: sys.board.valueMaps.scheduleDays.toArray(),
335
+ heatSources: sys.board.valueMaps.heatSources.toArray(),
336
+ circuits: sys.board.circuits.getCircuitReferences(true, true, false, true),
337
+ schedules: sys.schedules.get(),
338
+ clockMode: sys.general.options.clockMode || 12,
339
+ displayTypes: sys.board.valueMaps.scheduleDisplayTypes.toArray(),
340
+ bodies: [],
341
+ eggTimers: sys.eggTimers.get() // needed for *Touch to not overwrite real schedules
342
+ };
343
+ // Now get all the body heat sources.
344
+ for (let i = 0; i < sys.bodies.length; i++) {
345
+ let body = sys.bodies.getItemByIndex(i);
346
+ opts.bodies.push({ id: body.id, circuit: body.circuit, name: body.name, alias: body.alias, heatSources: sys.board.bodies.getHeatSources(body.id) });
347
+ }
348
+ return res.status(200).send(opts);
349
+ } catch (err) { next(err); }
350
+ });
351
+ app.get('/config/options/heaters', async (req, res, next) => {
352
+ try {
353
+ // Ensure heat mode/source valueMaps reflect the *current board* before returning picklists.
354
+ // Without this, startup can expose generic defaults until the first status/config packets arrive.
355
+ sys.board.heaters.updateHeaterServices();
356
+ let opts = {
357
+ tempUnits: sys.board.valueMaps.tempUnits.transform(state.temps.units),
358
+ bodies: sys.board.bodies.getBodyAssociations(),
359
+ maxHeaters: sys.equipment.maxHeaters,
360
+ heaters: sys.heaters.get(),
361
+ heaterTypes: sys.board.valueMaps.heaterTypes.toArray(),
362
+ equipmentMasters: sys.board.valueMaps.equipmentMaster.toArray(),
363
+ // Align with `/config/body/:id/heatModes` (body picklist). This ensures any board-specific
364
+ // filtering (e.g. IntelliCenter v3 preferred-mode suppression) is reflected consistently.
365
+ // Future improvement should return valid modes per body.
366
+ heatModes: sys.board.bodies.getHeatModes(1),
367
+ coolDownDelay: sys.general.options.cooldownDelay,
368
+ servers: [],
369
+ rs485ports: await conn.listInstalledPorts()
370
+ };
371
+ // We only need the servers data when the controller is a Nixie controller. We don't need to
372
+ // wait for this information if we are dealing with an OCP.
373
+ if (sys.controllerType === ControllerType.Nixie) opts.servers = await sys.ncp.getREMServers();
374
+ return res.status(200).send(opts);
375
+ } catch (err) { next(err); }
376
+ });
377
+ app.get('/config/options/customNames', (req, res) => {
378
+ let opts = {
379
+ maxCustomNames: sys.equipment.maxCustomNames,
380
+ customNames: sys.customNames.get()
381
+ };
382
+ return res.status(200).send(opts);
383
+ });
384
+ app.get('/config/options/chemControllers', async (req, res, next) => {
385
+ try {
386
+ let remServers = await sys.ncp.getREMServers();
387
+ let alarms = {
388
+ flow: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 1].includes(el.val)),
389
+ pH: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 2, 4].includes(el.val)),
390
+ orp: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 8, 16].includes(el.val)),
391
+ pHTank: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 32].includes(el.val)),
392
+ orpTank: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 64].includes(el.val)),
393
+ probeFault: sys.board.valueMaps.chemControllerAlarms.toArray().filter(el => [0, 128].includes(el.val))
394
+ }
395
+ let warnings = {
396
+ waterChemistry: sys.board.valueMaps.chemControllerWarnings.toArray().filter(el => [0, 1, 2].includes(el.val)),
397
+ pHLockout: sys.board.valueMaps.chemControllerLimits.toArray().filter(el => [0, 1].includes(el.val)),
398
+ pHDailyLimitReached: sys.board.valueMaps.chemControllerLimits.toArray().filter(el => [0, 2].includes(el.val)),
399
+ orpDailyLimitReached: sys.board.valueMaps.chemControllerLimits.toArray().filter(el => [0, 4].includes(el.val)),
400
+ invalidSetup: sys.board.valueMaps.chemControllerWarnings.toArray().filter(el => [0, 8].includes(el.val)),
401
+ chlorinatorCommsError: sys.board.valueMaps.chemControllerWarnings.toArray().filter(el => [0, 16].includes(el.val)),
402
+ }
403
+ let opts = {
404
+ types: sys.board.valueMaps.chemControllerTypes.toArray(),
405
+ bodies: sys.board.bodies.getBodyAssociations(),
406
+ tempUnits: sys.board.valueMaps.tempUnits.toArray(),
407
+ status: sys.board.valueMaps.chemControllerStatus.toArray(),
408
+ pumpTypes: sys.board.valueMaps.chemPumpTypes.toArray(),
409
+ phSupplyTypes: sys.board.valueMaps.phSupplyTypes.toArray(),
410
+ volumeUnits: sys.board.valueMaps.volumeUnits.toArray(),
411
+ dosingMethods: sys.board.valueMaps.chemDosingMethods.toArray(),
412
+ chlorDosingMethods: sys.board.valueMaps.chemChlorDosingMethods.toArray(),
413
+ orpProbeTypes: sys.board.valueMaps.chemORPProbeTypes.toArray(),
414
+ phProbeTypes: sys.board.valueMaps.chemPhProbeTypes.toArray(),
415
+ flowSensorTypes: sys.board.valueMaps.flowSensorTypes.toArray(),
416
+ acidTypes: sys.board.valueMaps.acidTypes.toArray(),
417
+ remServers,
418
+ dosingStatus: sys.board.valueMaps.chemControllerDosingStatus.toArray(),
419
+ siCalcTypes: sys.board.valueMaps.siCalcTypes.toArray(),
420
+ alarms,
421
+ warnings,
422
+ // waterFlow: sys.board.valueMaps.chemControllerWaterFlow.toArray(), // remove
423
+ controllers: sys.chemControllers.get(),
424
+ maxChemControllers: sys.equipment.maxChemControllers,
425
+ intellichemStandaloneSupported: sys.controllerType === ControllerType.Nixie,
426
+ doserTypes: sys.board.valueMaps.chemDoserTypes.toArray(),
427
+ chlorinators: sys.chlorinators.get(),
428
+ };
429
+ return res.status(200).send(opts);
430
+ }
431
+ catch (err) { next(err); }
432
+ });
433
+ app.get('/config/options/chemDosers', async (req, res, next) => {
434
+ try {
435
+ let remServers = await sys.ncp.getREMServers();
436
+ let opts = {
437
+ bodies: sys.board.bodies.getBodyAssociations(),
438
+ tempUnits: sys.board.valueMaps.tempUnits.toArray(),
439
+ status: sys.board.valueMaps.chemDoserStatus.toArray(),
440
+ pumpTypes: sys.board.valueMaps.chemPumpTypes.toArray(),
441
+ volumeUnits: sys.board.valueMaps.volumeUnits.toArray(),
442
+ flowSensorTypes: sys.board.valueMaps.flowSensorTypes.toArray(),
443
+ remServers,
444
+ dosingStatus: sys.board.valueMaps.chemDoserDosingStatus.toArray(),
445
+ dosers: sys.chemDosers.get(),
446
+ doserTypes: sys.board.valueMaps.chemDoserTypes.toArray(),
447
+ maxChemDosers: sys.equipment.maxChemDosers
448
+ };
449
+ return res.status(200).send(opts);
450
+ }
451
+ catch (err) { next(err); }
452
+ });
453
+
454
+ app.get('/config/options/rem', async (req, res, next) => {
455
+ try {
456
+ let opts = {
457
+ servers: await sys.ncp.getREMServers()
458
+ }
459
+ return res.status(200).send(opts);
460
+ } catch (err) { next(err); }
461
+ });
462
+ app.get('/config/options/controllerType', async (req, res, next) => {
463
+ try {
464
+ let opts = {
465
+ controllerType: sys.controllerType,
466
+ type: state.controllerState,
467
+ equipment: sys.equipment.get(),
468
+ controllerTypes: sys.getAvailableControllerTypes()
469
+ }
470
+ return res.status(200).send(opts);
471
+ } catch (err) { next(err); }
472
+ });
473
+ app.get('/config/options/anslq25ControllerType', async (req, res, next) => {
474
+ try {
475
+ let opts = {
476
+ // controllerType: typeof sys.anslq25.controllerType === 'undefined' ? '' : sys.anslq25.controllerType,
477
+ // model: typeof sys.anslq25.model === 'undefined' ? '' : sys.anslq25.model,
478
+ // equipment: sys.equipment.get(),
479
+ ...sys.anslq25.get(true),
480
+ controllerTypes: sys.getAvailableControllerTypes(['easytouch', 'intellitouch', 'intellicenter']),
481
+ rs485ports: await conn.listInstalledPorts()
482
+ }
483
+ return res.status(200).send(opts);
484
+ } catch (err) { next(err); }
485
+ });
486
+ app.get('/config/options/chlorinators', async (req, res, next) => {
487
+ try {
488
+ let opts = {
489
+ types: sys.board.valueMaps.chlorinatorType.toArray(),
490
+ bodies: sys.board.bodies.getBodyAssociations(),
491
+ chlorinators: sys.chlorinators.get(),
492
+ maxChlorinators: sys.equipment.maxChlorinators,
493
+ models: sys.board.valueMaps.chlorinatorModel.toArray(),
494
+ equipmentMasters: sys.board.valueMaps.equipmentMaster.toArray(),
495
+ rs485ports: await conn.listInstalledPorts()
496
+ };
497
+ return res.status(200).send(opts);
498
+ } catch (err) { next(err); }
499
+ });
500
+ app.get('/config/options/dateTime', (req, res) => {
501
+ let opts = {
502
+ dow: sys.board.system.getDOW()
503
+ }
504
+ return res.status(200).send(opts);
505
+ });
506
+ app.get('/app/options/logger', (req, res) => {
507
+ let opts = {
508
+ logger: config.getSection('log')
509
+ }
510
+ return res.status(200).send(opts);
511
+ });
512
+ app.get('/app/all/', (req, res) => {
513
+ let opts = config.getSection();
514
+ return res.status(200).send(opts);
515
+ });
516
+ app.get('/config/options/tempSensors', (req, res) => {
517
+ let opts = {
518
+ tempUnits: sys.board.valueMaps.tempUnits.toArray(),
519
+ sensors: sys.board.system.getSensors()
520
+ };
521
+ return res.status(200).send(opts);
522
+ });
523
+ app.get('/config/options/filters', async (req, res, next) => {
524
+ try {
525
+ let opts = {
526
+ types: sys.board.valueMaps.filterTypes.toArray(),
527
+ bodies: sys.board.bodies.getBodyAssociations(),
528
+ filters: sys.filters.get(),
529
+ areaUnits: sys.board.valueMaps.areaUnits.toArray(),
530
+ pressureUnits: sys.board.valueMaps.pressureUnits.toArray(),
531
+ circuits: sys.board.circuits.getCircuitReferences(true, true, true, false),
532
+ servers: []
533
+ };
534
+ if (sys.controllerType === ControllerType.Nixie) opts.servers = await sys.ncp.getREMServers();
535
+ return res.status(200).send(opts);
536
+ } catch (err) { next(err); }
537
+ });
538
+ /******* END OF CONFIGURATION PICK LISTS/REFERENCES AND VALIDATION ***********/
539
+ /******* ENDPOINTS FOR MODIFYING THE OUTDOOR CONTROL PANEL SETTINGS **********/
540
+ app.put('/config/rem', async (req, res, next) => {
541
+ try {
542
+ // RSG: this is problematic because we now enable multiple rem type interfaces that may not be called REM.
543
+ // This is now also a dupe of PUT /app/interface and should be consolidated
544
+ // config.setSection('web.interfaces.rem', req.body);
545
+ config.setInterface(req.body);
546
+ }
547
+ catch (err) { next(err); }
548
+ })
549
+ app.put('/config/tempSensors', async (req, res, next) => {
550
+ try {
551
+ await sys.board.system.setTempSensorsAsync(req.body);
552
+ let opts = {
553
+ tempUnits: sys.board.valueMaps.tempUnits.toArray(),
554
+ sensors: sys.board.system.getSensors()
555
+ };
556
+ return res.status(200).send(opts);
557
+ }
558
+ catch (err) { next(err); }
559
+ });
560
+ app.put('/config/filter', async (req, res, next) => {
561
+ try {
562
+ let sfilter = await sys.board.filters.setFilterAsync(req.body);
563
+ return res.status(200).send(sfilter.get(true));
564
+ }
565
+ catch (err) { next(err); }
566
+ });
567
+ app.put('/config/controllerType', async (req, res, next) => {
568
+ try {
569
+ let controller = await sys.board.setControllerType(req.body);
570
+ return res.status(200).send(controller.get(true));
571
+ } catch (err) { next(err); }
572
+ });
573
+ app.put('/config/anslq25ControllerType', async (req, res, next) => {
574
+ try {
575
+ // sys.anslq25ControllerType
576
+ await sys.anslq25Board.setAnslq25Async(req.body);
577
+ return res.status(200).send(sys.anslq25.get(true));
578
+ } catch (err) { next(err); }
579
+ });
580
+ app.delete('/config/filter', async (req, res, next) => {
581
+ try {
582
+ let sfilter = await sys.board.filters.deleteFilterAsync(req.body);
583
+ return res.status(200).send(sfilter.get(true));
584
+ }
585
+ catch (err) { next(err); }
586
+ });
587
+ app.put('/config/general', async (req, res, next) => {
588
+ // Change the options for the pool.
589
+ try {
590
+ let rc = await sys.board.system.setGeneralAsync(req.body);
591
+ let opts = {
592
+ countries: sys.board.valueMaps.countries.toArray(),
593
+ tempUnits: sys.board.valueMaps.tempUnits.toArray(),
594
+ timeZones: sys.board.valueMaps.timeZones.toArray(),
595
+ clockSources: sys.board.valueMaps.clockSources.toArray(),
596
+ clockModes: sys.board.valueMaps.clockModes.toArray(),
597
+ pool: sys.general.get(true),
598
+ sensors: sys.board.system.getSensors()
599
+ };
600
+ return res.status(200).send(opts);
601
+ }
602
+ catch (err) { next(err); }
603
+ });
604
+
605
+ app.put('/config/valve', async (req, res, next) => {
606
+ // Update a valve.
607
+ try {
608
+ let valve = await sys.board.valves.setValveAsync(req.body);
609
+ return res.status(200).send((valve).get(true));
610
+ }
611
+ catch (err) { next(err); }
612
+ });
613
+ app.delete('/config/valve', async (req, res, next) => {
614
+ // Update a valve.
615
+ try {
616
+ let valve = await sys.board.valves.deleteValveAsync(req.body);
617
+ return res.status(200).send((valve).get(true));
618
+ }
619
+ catch (err) { next(err); }
620
+ });
621
+
622
+ app.put('/config/body', async (req, res, next) => {
623
+ // Change the body attributes.
624
+ try {
625
+ let body = await sys.board.bodies.setBodyAsync(req.body);
626
+ return res.status(200).send((body).get(true));
627
+ }
628
+ catch (err) { next(err); }
629
+ });
630
+ app.put('/config/circuit', async (req, res, next) => {
631
+ // add/update a circuit
632
+ try {
633
+ let circuit = await sys.board.circuits.setCircuitAsync(req.body);
634
+ return res.status(200).send((circuit).get(true));
635
+ }
636
+ catch (err) { next(err); }
637
+ });
638
+ app.delete('/config/circuit', async (req, res, next) => {
639
+ // delete a circuit
640
+ try {
641
+ let circuit = await sys.board.circuits.deleteCircuitAsync(req.body);
642
+ return res.status(200).send((circuit).get(true));
643
+ }
644
+ catch (err) { next(err); }
645
+ });
646
+ app.put('/config/feature', async (req, res, next) => {
647
+ // add/update a feature
648
+ try {
649
+ let feature = await sys.board.features.setFeatureAsync(req.body);
650
+ return res.status(200).send((feature).get(true));
651
+ }
652
+ catch (err) { next(err); }
653
+ });
654
+ app.delete('/config/feature', async (req, res, next) => {
655
+ // delete a feature
656
+ try {
657
+ let feature = await sys.board.features.deleteFeatureAsync(req.body);
658
+ return res.status(200).send((feature).get(true));
659
+ }
660
+ catch (err) { next(err); }
661
+ });
662
+ app.put('/config/circuitGroup', async (req, res, next) => {
663
+ // add/update a circuitGroup
664
+ try {
665
+ let group = await sys.board.circuits.setCircuitGroupAsync(req.body);
666
+ return res.status(200).send((group).get(true));
667
+ }
668
+ catch (err) { next(err); }
669
+ });
670
+ app.delete('/config/circuitGroup', async (req, res, next) => {
671
+ try {
672
+ let group = await sys.board.circuits.deleteCircuitGroupAsync(req.body);
673
+ return res.status(200).send((group).get(true));
674
+ }
675
+ catch (err) { next(err); }
676
+ });
677
+ app.put('/config/lightGroup', async (req, res, next) => {
678
+ try {
679
+ let group = await sys.board.circuits.setLightGroupAsync(req.body);
680
+ return res.status(200).send((group).get(true));
681
+ }
682
+ catch (err) { next(err); }
683
+ });
684
+ app.delete('/config/lightGroup', async (req, res, next) => {
685
+ try {
686
+ let group = await sys.board.circuits.deleteLightGroupAsync(req.body);
687
+ return res.status(200).send((group).get(true));
688
+ }
689
+ catch (err) { next(err); }
690
+ });
691
+ app.put('/config/pump', async (req, res, next) => {
692
+ // Change the pump attributes. This will add the pump if it doesn't exist, set
693
+ // any affiliated circuits and maintain all attribututes of the pump.
694
+ // RSG: Caveat - you have to send none or all of the pump circuits or any not included be deleted.
695
+ try {
696
+ let pump = await sys.board.pumps.setPumpAsync(req.body);
697
+ return res.status(200).send((pump).get(true));
698
+ }
699
+ catch (err) { next(err); }
700
+ });
701
+ app.put('/config/pumpCircuit', async (req, res, next) => {
702
+ try {
703
+ let pmpId = parseInt(req.body.pumpId, 10);
704
+ let circId = parseInt(req.body.circuitId, 10);
705
+ let pmp: Pump;
706
+ if (isNaN(pmpId)) {
707
+ let pmpAddress = parseInt(req.body.address, 10);
708
+ if (!isNaN(pmpAddress)) pmp = sys.pumps.find(x => x.address === pmpAddress);
709
+ }
710
+ else
711
+ pmp = sys.pumps.find(x => x.id === pmpId);
712
+ if (typeof pmp === 'undefined') throw new ServiceProcessError(`Pump not found`, '/config/pumpCircuit', 'Set circuit speed');
713
+ let data = pmp.get(true);
714
+ let c = typeof data.circuits !== 'undefined' && typeof data.circuits.find !== 'undefined' ? data.circuits.find(x => x.circuit === circId) : undefined;
715
+ if (typeof c === 'undefined') throw new ServiceProcessError(`Circuit not found`, '/config/pumpCircuit', 'Set circuit speed');
716
+ if (typeof req.body.speed !== 'undefined') {
717
+ let speed = parseInt(req.body.speed, 10);
718
+ if (isNaN(speed)) throw new ServiceProcessError(`Invalid circuit speed supplied`, '/config/pumpCircuit', 'Set circuit speed');
719
+ c.speed = speed;
720
+ }
721
+ else if (typeof req.body.flow !== 'undefined') {
722
+ let flow = parseInt(req.body.flow, 10);
723
+ if (isNaN(flow)) throw new ServiceProcessError(`Invalid circuit flow supplied`, '/config/pumpCircuit', 'Set circuit flow');
724
+ c.flow = flow;
725
+ }
726
+ else {
727
+ throw new ServiceProcessError(`You must supply a target flow or speed`, '/config/pumpCircuit', 'Set circuit flow');
728
+ }
729
+ await sys.board.pumps.setPumpAsync(data);
730
+ return res.status(200).send((pmp).get(true));
731
+ } catch (err) { next(err); }
732
+
733
+ });
734
+ // RKS: 05-20-22 This is a remnant of the old web ui. It is not called and the setType method needed to go away.
735
+ //app.delete('/config/pump/:pumpId', async (req, res, next) => {
736
+ // try {
737
+ // let pump = sys.pumps.getItemById(parseInt(req.params.pumpId, 10));
738
+ // await sys.board.pumps.deletePumpAsync()
739
+ // if (pump.type === 0) {
740
+ // return res.status(500).send(`Pump ${pump.id} not active`);
741
+ // }
742
+ // pump.setType(0);
743
+ // return res.status(200).send('OK');
744
+ // } catch (err) { next(err); }
745
+ //});
746
+ app.delete('/config/pump', async (req, res, next) => {
747
+ try {
748
+ let pump = await sys.board.pumps.deletePumpAsync(req.body);
749
+ return res.status(200).send((pump).get(true));
750
+ }
751
+ catch (err) { next(err); }
752
+ });
753
+ app.put('/config/customNames', async (req, res, next) => {
754
+ try {
755
+ let names = await sys.board.system.setCustomNamesAsync(req.body);
756
+ return res.status(200).send(names.get());
757
+ }
758
+ catch (err) { next(err); }
759
+ });
760
+ app.put('/config/customName', async (req, res, next) => {
761
+ try {
762
+ let name = await sys.board.system.setCustomNameAsync(req.body);
763
+ return res.status(200).send(name.get(true));
764
+ }
765
+ catch (err) { next(err); }
766
+ });
767
+ app.get('/config/schedule/:id', (req, res) => {
768
+ let schedId = parseInt(req.params.id || '0', 10);
769
+ let sched = sys.schedules.getItemById(schedId).get(true);
770
+ return res.status(200).send(sched);
771
+ });
772
+ app.put('/config/schedule', async (req, res, next) => {
773
+ try {
774
+ let sched = await sys.board.schedules.setScheduleAsync(req.body);
775
+ return res.status(200).send((sched as Schedule).get(true));
776
+ }
777
+ catch (err) { next(err); }
778
+ });
779
+ app.delete('/config/schedule', async (req, res, next) => {
780
+ try {
781
+ let sched = await sys.board.schedules.deleteScheduleAsync(req.body);
782
+ return res.status(200).send((sched as Schedule).get(true));
783
+ }
784
+ catch (err) {
785
+ //console.log(`Error deleting schedule... ${err}`);
786
+ next(err);
787
+ }
788
+ });
789
+ app.put('/config/chlorinator', async (req, res, next) => {
790
+ try {
791
+ let chlor = await sys.board.chlorinator.setChlorAsync(req.body);
792
+ return res.status(200).send(sys.chlorinators.getItemById(chlor.id).get(true));
793
+ }
794
+ catch (err) { next(err); }
795
+ });
796
+ app.delete('/config/chlorinator', async (req, res, next) => {
797
+ try {
798
+ let chlor = await sys.board.chlorinator.deleteChlorAsync(req.body);
799
+ return res.status(200).send(chlor.get(true));
800
+ }
801
+ catch (err) { next(err); }
802
+ });
803
+ app.put('/config/heater', async (req, res, next) => {
804
+ try {
805
+ let heater = await sys.board.heaters.setHeaterAsync(req.body);
806
+ return res.status(200).send(sys.heaters.getItemById(heater.id).get(true));
807
+ }
808
+ catch (err) { next(err); }
809
+ });
810
+ app.delete('/config/heater', async (req, res, next) => {
811
+ try {
812
+ let heater = await sys.board.heaters.deleteHeaterAsync(req.body);
813
+ return res.status(200).send((heater as Heater).get(true));
814
+ }
815
+ catch (err) { next(err); }
816
+ });
817
+
818
+ /***** END OF ENDPOINTS FOR MODIFYINC THE OUTDOOR CONTROL PANEL SETTINGS *****/
819
+
820
+
821
+
822
+ app.get('/config/circuits/names', (req, res) => {
823
+ let circuitNames = sys.board.circuits.getCircuitNames();
824
+ return res.status(200).send(circuitNames);
825
+ });
826
+ app.get('/config/circuit/functions', (req, res) => {
827
+ let circuitFunctions = sys.board.circuits.getCircuitFunctions();
828
+ return res.status(200).send(circuitFunctions);
829
+ });
830
+ app.get('/config/features/functions', (req, res) => {
831
+ let featureFunctions = sys.board.features.getFeatureFunctions();
832
+ return res.status(200).send(featureFunctions);
833
+ });
834
+ app.get('/config/circuit/:id', (req, res) => {
835
+ // todo: need getInterfaceById.get() in case features are requested here
836
+ // todo: it seems to make sense to combine with /state/circuit/:id as they both have similiar/overlapping info
837
+ return res.status(200).send(sys.circuits.getItemById(parseInt(req.params.id, 10)).get());
838
+ });
839
+ app.get('/config/circuit/:id/lightThemes', (req, res) => {
840
+ let circuit = sys.circuits.getInterfaceById(parseInt(req.params.id, 10));
841
+ let themes = typeof circuit !== 'undefined' && typeof circuit.getLightThemes === 'function' ? circuit.getLightThemes(circuit.type) : [];
842
+ return res.status(200).send(themes);
843
+ });
844
+ app.get('/config/circuit/:id/lightCommands', (req, res) => {
845
+ let circuit = sys.circuits.getInterfaceById(parseInt(req.params.id, 10));
846
+ let commands = typeof circuit !== 'undefined' && typeof circuit.getLightThemes === 'function' ? circuit.getLightCommands(circuit.type) : [];
847
+ return res.status(200).send(commands);
848
+ });
849
+
850
+ app.get('/config/chlorinator/:id', (req, res) => {
851
+ return res.status(200).send(sys.chlorinators.getItemById(parseInt(req.params.id, 10)).get());
852
+ });
853
+ app.get('/config/chlorinators/search', async (req, res, next) => {
854
+ // Change the options for the pool.
855
+ try {
856
+ //await sys.board.virtualChlorinatorController.search();
857
+ return res.status(200).send(sys.chlorinators.getItemById(1).get());
858
+ }
859
+ catch (err) {
860
+ next(err);
861
+ }
862
+ });
863
+ app.put('/config/dateTime', async (req, res, next) => {
864
+ try {
865
+ let time = await sys.updateControllerDateTimeAsync(req.body);
866
+ return res.status(200).send(time);
867
+ }
868
+ catch (err) { next(err); }
869
+ });
870
+ app.get('/config/lightGroups/themes', (req, res) => {
871
+ // RSG: is this and /config/circuit/:id/lightThemes both needed?
872
+ let grp = sys.lightGroups.getItemById(parseInt(req.body.id, 10));
873
+ return res.status(200).send(grp.getLightThemes());
874
+ });
875
+ app.get('/config/lightGroups/commands', (req, res) => {
876
+ let grp = sys.lightGroups.getItemById(parseInt(req.body.id, 10));
877
+ return res.status(200).send(grp.getLightCommands());
878
+ });
879
+ app.get('/config/lightGroup/:id', (req, res) => {
880
+ // if (sys.controllerType === ControllerType.IntelliCenter) {
881
+ let grp = sys.lightGroups.getItemById(parseInt(req.params.id, 10));
882
+ return res.status(200).send(grp.getExtended());
883
+ // }
884
+ // else
885
+ // return res.status(200).send(sys.intellibrite.getExtended());
886
+ });
887
+ app.get('/config/lightGroup/colors', (req, res) => {
888
+ return res.status(200).send(sys.board.valueMaps.lightColors.toArray());
889
+ });
890
+ app.put('/config/lightGroup/:id/setColors', async (req, res, next) => {
891
+ try {
892
+ let id = parseInt(req.params.id, 10);
893
+ let grp = extend(true, { id: id }, req.body);
894
+ await sys.board.circuits.setLightGroupAttribsAsync(grp);
895
+ return res.status(200).send(sys.lightGroups.getItemById(id).getExtended());
896
+ }
897
+ catch (err) { next(err); }
898
+ });
899
+ app.get('/config/intellibrite/themes', (req, res) => {
900
+ return res.status(200).send(sys.board.circuits.getLightThemes(16));
901
+ });
902
+ app.get('/config/circuitGroup/:id', (req, res) => {
903
+ let grp = sys.circuitGroups.getItemById(parseInt(req.params.id, 10));
904
+ return res.status(200).send(grp.getExtended());
905
+ });
906
+ /* app.get('/config/chemController/search', async (req, res, next) => {
907
+ // Change the options for the pool.
908
+ try {
909
+ let result = await sys.board.virtualChemControllers.search();
910
+ return res.status(200).send(result);
911
+ }
912
+ catch (err) {
913
+ next(err);
914
+ }
915
+ }); */
916
+ app.put('/config/chemController', async (req, res, next) => {
917
+ try {
918
+ let chem = await sys.board.chemControllers.setChemControllerAsync(req.body);
919
+ return res.status(200).send(chem.get());
920
+ }
921
+ catch (err) { next(err); }
922
+ });
923
+ app.put('/config/chemDoser', async (req, res, next) => {
924
+ try {
925
+ let doser = await sys.board.chemDosers.setChemDoserAsync(req.body);
926
+ return res.status(200).send(doser.get());
927
+ }
928
+ catch (err) { next(err); }
929
+
930
+ });
931
+ app.put('/config/chemController/calibrateDose', async (req, res, next) => {
932
+ try {
933
+ let schem = await sys.board.chemControllers.calibrateDoseAsync(req.body);
934
+ return res.status(200).send(schem.getExtended());
935
+ }
936
+ catch (err) { next(err); }
937
+ });
938
+ app.put('/config/chemDoser/calibrateDose', async (req, res, next) => {
939
+ try {
940
+ let schem = await sys.board.chemDosers.calibrateDoseAsync(req.body);
941
+ return res.status(200).send(schem.getExtended());
942
+ }
943
+ catch (err) { next(err); }
944
+ });
945
+ app.put('/config/chemController/feed', async (req, res, next) => {
946
+ try {
947
+ let chem = await sys.board.chemControllers.setChemControllerAsync(req.body);
948
+ return res.status(200).send(chem.get());
949
+ }
950
+ catch (err) { next(err); }
951
+ });
952
+ app.delete('/config/chemController', async (req, res, next) => {
953
+ try {
954
+ let chem = await sys.board.chemControllers.deleteChemControllerAsync(req.body);
955
+ return res.status(200).send(chem.get());
956
+ }
957
+ catch (err) { next(err); }
958
+ });
959
+ app.delete('/config/chemDoser', async (req, res, next) => {
960
+ try {
961
+ let doser = await sys.board.chemDosers.deleteChemDoserAsync(req.body);
962
+ return res.status(200).send(doser.get());
963
+ }
964
+ catch (err) { next(err); }
965
+
966
+ });
967
+
968
+ /* app.get('/config/intellibrite', (req, res) => {
969
+ return res.status(200).send(sys.intellibrite.getExtended());
970
+ });
971
+ app.get('/config/intellibrite/colors', (req, res) => {
972
+ return res.status(200).send(sys.board.valueMaps.lightColors.toArray());
973
+ });
974
+ app.put('/config/intellibrite/setColors', (req, res) => {
975
+ let grp = extend(true, { id: 0 }, req.body);
976
+ sys.board.circuits.setIntelliBriteColors(new LightGroup(grp));
977
+ return res.status(200).send('OK');
978
+ }); */
979
+ app.get('/config/security/session', (req, res) => {
980
+ return res.status(200).send(ConfigRoute.getSessionResponse(req));
981
+ });
982
+ app.put('/config/security/login', (req, res) => {
983
+ if (!sys.security.enabled) {
984
+ return res.status(409).send({
985
+ error: 'SECURITY_DISABLED',
986
+ message: 'Panel security is disabled.',
987
+ security: ConfigRoute.getSessionResponse(req)
988
+ });
989
+ }
990
+ const role = ConfigRoute.getRoleForPin(((req.body || {}).pin || '').toString());
991
+ if (typeof role === 'undefined') {
992
+ return res.status(401).send({
993
+ error: 'INVALID_PIN',
994
+ message: 'PIN does not match a configured security role.'
995
+ });
996
+ }
997
+ return res.status(200).send({
998
+ enabled: sys.security.enabled,
999
+ session: ConfigRoute.createSecuritySession(req, role)
1000
+ });
1001
+ });
1002
+ app.put('/config/security/logout', (req, res) => {
1003
+ return res.status(200).send({
1004
+ enabled: sys.security.enabled,
1005
+ session: ConfigRoute.clearSecuritySession(req)
1006
+ });
1007
+ });
1008
+ app.get('/config', (req, res) => {
1009
+ return res.status(200).send(sys.getSection('all'));
1010
+ });
1011
+ app.get('/config/:section', (req, res) => {
1012
+ return res.status(200).send(sys.getSection(req.params.section));
1013
+ });
1014
+
1015
+
1016
+ /******* ENDPOINTS FOR MANAGING THE poolController APPLICATION *********/
1017
+ app.put('/app/logger/setOptions', (req, res) => {
1018
+ logger.setOptions(req.body);
1019
+ return res.status(200).send(logger.options);
1020
+ });
1021
+ app.put('/app/logger/clearMessages', (req, res) => {
1022
+ logger.clearMessages();
1023
+ return res.status(200).send('OK');
1024
+ });
1025
+ app.get('/app/messages/broadcast/actions', (req, res) => {
1026
+ return res.status(200).send(sys.board.valueMaps.msgBroadcastActions.toArray());
1027
+ });
1028
+ app.put('/app/config/reload', (req, res) => {
1029
+ sys.board.reloadConfig();
1030
+ return res.status(200).send('OK');
1031
+ });
1032
+ app.put('/app/interface', async (req, res, next) => {
1033
+ try {
1034
+ let iface = await webApp.updateServerInterface(req.body);
1035
+ return res.status(200).send(iface);
1036
+ }
1037
+ catch (err) { next(err); }
1038
+ });
1039
+ app.put('/app/rs485Port', async (req, res, next) => {
1040
+ try {
1041
+ let port = await conn.setPortAsync(req.body);
1042
+ return res.status(200).send(port);
1043
+ }
1044
+ catch (err) { next(err); }
1045
+ });
1046
+ // app.put('/app/screenlogic', async (req, res, next) => {
1047
+ // try {
1048
+ // let screenlogic = await sl.setScreenlogicAsync(req.body);
1049
+ // return res.status(200).send(screenlogic);
1050
+ // }
1051
+ // catch (err) { next(err); }
1052
+ // });
1053
+ app.delete('/app/rs485Port', async (req, res, next) => {
1054
+ try {
1055
+ let port = await conn.deleteAuxPort(req.body);
1056
+ return res.status(200).send(port);
1057
+ }
1058
+ catch (err) { next(err); }
1059
+ });
1060
+ app.get('/app/config/startPacketCapture', async (req, res, next) => {
1061
+ try {
1062
+ await startPacketCapture(true);
1063
+ return res.status(200).send('OK');
1064
+ } catch (err) { next(err); }
1065
+ });
1066
+ app.get('/app/config/startPacketCaptureWithoutReset', async (req, res, next) => {
1067
+ try {
1068
+ await startPacketCapture(false);
1069
+ return res.status(200).send('OK');
1070
+ } catch (err) { next(err); }
1071
+ });
1072
+ app.get('/app/config/stopPacketCapture', async (req, res, next) => {
1073
+ try {
1074
+ let file = await stopPacketCaptureAsync();
1075
+ if (typeof file !== 'string' || file.length === 0 || !fs.existsSync(file)) {
1076
+ logger.warn(`stopPacketCapture did not produce a valid backup file path`);
1077
+ return res.status(409).send('Packet capture is not active or no capture file is available.');
1078
+ }
1079
+ return res.download(file);
1080
+ }
1081
+ catch (err) { next(err); }
1082
+ });
1083
+ app.get('/app/config/:section', (req, res) => {
1084
+ return res.status(200).send(config.getSection(req.params.section));
1085
+ });
1086
+ app.get('/app/config/options/backup', async (req, res, next) => {
1087
+ try {
1088
+ let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0 }, keepCount: 5, servers: [] });
1089
+ let servers = await sys.ncp.getREMServers();
1090
+ if (typeof servers !== 'undefined') {
1091
+ // Just in case somebody deletes the backup section and doesn't put it back properly.
1092
+ for (let i = 0; i < servers.length; i++) {
1093
+ let srv = servers[i];
1094
+ 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 });
1095
+ }
1096
+ for (let i = opts.servers.length - 1; i >= 0; i--) {
1097
+ let srv = opts.servers[i];
1098
+ if (typeof servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.splice(i, 1);
1099
+ }
1100
+ }
1101
+ if (typeof opts.servers === 'undefined') opts.servers = [];
1102
+ return res.status(200).send(opts);
1103
+ } catch (err) { next(err); }
1104
+ });
1105
+ app.get('/app/config/options/restore', async (req, res, next) => {
1106
+ try {
1107
+ let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0 }, keepCount: 5, servers: [], backupFiles: [] });
1108
+ let servers = await sys.ncp.getREMServers();
1109
+ if (typeof servers !== 'undefined') {
1110
+ for (let i = 0; i < servers.length; i++) {
1111
+ let srv = servers[i];
1112
+ if (typeof opts.servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.push({ name: srv.name, uuid: srv.uuid, backup: false });
1113
+ }
1114
+ for (let i = opts.servers.length - 1; i >= 0; i--) {
1115
+ let srv = opts.servers[i];
1116
+ if (typeof servers.find(elem => elem.uuid === srv.uuid) === 'undefined') opts.servers.splice(i, 1);
1117
+ }
1118
+ }
1119
+ if (typeof opts.servers === 'undefined') opts.servers = [];
1120
+ opts.backupFiles = await webApp.readBackupFiles();
1121
+ return res.status(200).send(opts);
1122
+ } catch (err) { next(err); }
1123
+
1124
+ });
1125
+ app.put('/app/config/options/backup', async (req, res, next) => {
1126
+ try {
1127
+ config.setSection('controller.backups', req.body);
1128
+ let opts = config.getSection('controller.backups', { automatic: false, interval: { days: 30, hours: 0 }, keepCount: 5, servers: [] });
1129
+ webApp.autoBackup = utils.makeBool(opts.automatic);
1130
+ await webApp.checkAutoBackup();
1131
+ return res.status(200).send(opts);
1132
+ } catch (err) { next(err); }
1133
+
1134
+ });
1135
+ app.put('/app/config/createBackup', async (req, res, next) => {
1136
+ try {
1137
+ let ret = await webApp.backupServer(req.body);
1138
+ res.download(ret.filePath);
1139
+ }
1140
+ catch (err) { next(err); }
1141
+ });
1142
+ app.delete('/app/backup/file', async (req, res, next) => {
1143
+ try {
1144
+ let opts = req.body;
1145
+ fs.unlinkSync(opts.filePath);
1146
+ return res.status(200).send(opts);
1147
+ }
1148
+ catch (err) { next(err); }
1149
+ });
1150
+ app.post('/app/backup/file', async (req, res, next) => {
1151
+ try {
1152
+ let file = multer({
1153
+ limits: { fileSize: 1000000 },
1154
+ storage: multer.memoryStorage()
1155
+ }).single('backupFile');
1156
+ file(req, res, async (err) => {
1157
+ try {
1158
+ if (err) { next(err); }
1159
+ else {
1160
+ // Validate the incoming data and save it off only if it is valid.
1161
+ let bf = await BackupFile.fromBuffer(req.file.originalname, req.file.buffer);
1162
+ if (typeof bf === 'undefined') {
1163
+ err = new ServiceProcessError(`Invalid backup file: ${req.file.originalname}`, 'POST: app/backup/file', 'extractBackupOptions');
1164
+ next(err);
1165
+ }
1166
+ else {
1167
+ if (fs.existsSync(bf.filePath))
1168
+ return next(new ServiceProcessError(`File already exists ${req.file.originalname}`, 'POST: app/backup/file', 'writeFile'));
1169
+ else {
1170
+ try {
1171
+ fs.writeFileSync(bf.filePath, new Uint8Array(req.file.buffer));
1172
+ } catch (e) { logger.error(`Error writing backup file ${e.message}`); }
1173
+ }
1174
+ return res.status(200).send(bf);
1175
+ }
1176
+ }
1177
+ } catch (e) {
1178
+ err = new ServiceProcessError(`Error uploading file: ${e.message}`, 'POST: app/backup/file', 'uploadFile');
1179
+ next(err);
1180
+ logger.error(`Error uploading file ${e.message}`);
1181
+ }
1182
+ });
1183
+ } catch (err) { next(err); }
1184
+ });
1185
+ app.put('/app/restore/validate', async (req, res, next) => {
1186
+ try {
1187
+ // Validate all the restore options.
1188
+ let opts = req.body;
1189
+ let ctx = await webApp.validateRestore(opts);
1190
+ return res.status(200).send(ctx);
1191
+ } catch (err) { next(err); }
1192
+ });
1193
+ app.put('/app/restore/file', async (req, res, next) => {
1194
+ try {
1195
+ let opts = req.body;
1196
+ let results = await webApp.restoreServers(opts);
1197
+ return res.status(200).send(results);
1198
+ } catch (err) { next(err); }
1199
+ });
1200
+ app.put('/app/anslq25', async(req, res, next) => {
1201
+ try {
1202
+ await sys.anslq25Board.setAnslq25Async(req.body);
1203
+ return res.status(200).send(sys.anslq25.get(true));
1204
+ } catch (err) { next(err); }
1205
+ });
1206
+ app.delete('/app/anslq25', async(req, res, next) => {
1207
+ try {
1208
+ await sys.anslq25Board.deleteAnslq25Async(req.body);
1209
+ return res.status(200).send(sys.anslq25.get(true));
1210
+ } catch (err) { next(err); }
1211
+ });
1212
+ }
1054
1213
  }