nodejs-poolcontroller 7.5.1 → 7.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/.github/ISSUE_TEMPLATE/1-bug-report.yml +84 -0
  2. package/.github/ISSUE_TEMPLATE/2-docs.md +12 -0
  3. package/.github/ISSUE_TEMPLATE/3-proposal.md +28 -0
  4. package/.github/ISSUE_TEMPLATE/config.yml +8 -0
  5. package/Changelog +19 -0
  6. package/Dockerfile +3 -3
  7. package/README.md +13 -8
  8. package/app.ts +1 -1
  9. package/config/Config.ts +38 -2
  10. package/config/VersionCheck.ts +27 -12
  11. package/controller/Constants.ts +2 -1
  12. package/controller/Equipment.ts +193 -9
  13. package/controller/Errors.ts +10 -0
  14. package/controller/Lockouts.ts +503 -0
  15. package/controller/State.ts +269 -64
  16. package/controller/boards/AquaLinkBoard.ts +1000 -0
  17. package/controller/boards/BoardFactory.ts +4 -0
  18. package/controller/boards/EasyTouchBoard.ts +468 -144
  19. package/controller/boards/IntelliCenterBoard.ts +466 -307
  20. package/controller/boards/IntelliTouchBoard.ts +37 -5
  21. package/controller/boards/NixieBoard.ts +671 -141
  22. package/controller/boards/SystemBoard.ts +1397 -641
  23. package/controller/comms/Comms.ts +462 -362
  24. package/controller/comms/messages/Messages.ts +174 -30
  25. package/controller/comms/messages/config/ChlorinatorMessage.ts +6 -3
  26. package/controller/comms/messages/config/CircuitMessage.ts +1 -0
  27. package/controller/comms/messages/config/ExternalMessage.ts +10 -8
  28. package/controller/comms/messages/config/HeaterMessage.ts +141 -29
  29. package/controller/comms/messages/config/OptionsMessage.ts +9 -2
  30. package/controller/comms/messages/config/PumpMessage.ts +53 -35
  31. package/controller/comms/messages/config/ScheduleMessage.ts +33 -25
  32. package/controller/comms/messages/config/ValveMessage.ts +2 -2
  33. package/controller/comms/messages/status/ChlorinatorStateMessage.ts +38 -86
  34. package/controller/comms/messages/status/EquipmentStateMessage.ts +59 -23
  35. package/controller/comms/messages/status/HeaterStateMessage.ts +57 -3
  36. package/controller/comms/messages/status/IntelliChemStateMessage.ts +56 -8
  37. package/controller/comms/messages/status/PumpStateMessage.ts +23 -1
  38. package/controller/nixie/Nixie.ts +1 -1
  39. package/controller/nixie/bodies/Body.ts +3 -0
  40. package/controller/nixie/chemistry/ChemController.ts +164 -51
  41. package/controller/nixie/chemistry/Chlorinator.ts +137 -88
  42. package/controller/nixie/circuits/Circuit.ts +51 -19
  43. package/controller/nixie/heaters/Heater.ts +241 -31
  44. package/controller/nixie/pumps/Pump.ts +488 -206
  45. package/controller/nixie/schedules/Schedule.ts +91 -35
  46. package/controller/nixie/valves/Valve.ts +1 -1
  47. package/defaultConfig.json +20 -0
  48. package/package.json +21 -21
  49. package/web/Server.ts +94 -49
  50. package/web/bindings/aqualinkD.json +505 -0
  51. package/web/bindings/influxDB.json +71 -1
  52. package/web/bindings/mqtt.json +98 -39
  53. package/web/bindings/mqttAlt.json +59 -1
  54. package/web/interfaces/baseInterface.ts +1 -0
  55. package/web/interfaces/httpInterface.ts +23 -2
  56. package/web/interfaces/influxInterface.ts +45 -10
  57. package/web/interfaces/mqttInterface.ts +114 -54
  58. package/web/services/config/Config.ts +55 -132
  59. package/web/services/state/State.ts +81 -4
  60. package/web/services/state/StateSocket.ts +4 -4
  61. package/web/services/utilities/Utilities.ts +8 -6
  62. package/.github/ISSUE_TEMPLATE/bug_report.md +0 -52
  63. package/config copy.json +0 -300
  64. package/issue_template.md +0 -52
@@ -1,42 +1,42 @@
1
1
  {
2
- "context": {
3
- "name": "MQTT",
4
- "options": {
5
- "formatter": [
6
- {
7
- "transform": ".toLowerCase()"
8
- },
9
- {
10
- "regexkey": "\\s",
11
- "replace": "",
12
- "description": "Remove whitespace"
13
- },
14
- {
15
- "regexkey": "\\/",
16
- "replace": "",
17
- "description": "Remove /"
18
- },
19
- {
20
- "regexkey": "\\+",
21
- "replace": "",
22
- "description": "Remove +"
23
- },
24
- {
25
- "regexkey": "\\$",
26
- "replace": "",
27
- "description": "Remove $"
28
- },
29
- {
30
- "regexkey": "\\#",
31
- "replace": "",
32
- "description": "Remove #"
33
- }
34
- ],
35
- "rootTopic-DIRECTIONS": "You can override the root topic by renaming _rootTopic to rootTopic",
36
- "_rootTopic": "@bind=(state.equipment.alias).replace(' ','-').replace('/','').toLowerCase();",
37
- "clientId": "@bind='mqttjs_njsPC_'+Math.random().toString(16).substr(2, 8);"
2
+ "context": {
3
+ "name": "MQTT",
4
+ "options": {
5
+ "formatter": [
6
+ {
7
+ "transform": ".toLowerCase()"
8
+ },
9
+ {
10
+ "regexkey": "\\s",
11
+ "replace": "",
12
+ "description": "Remove whitespace"
13
+ },
14
+ {
15
+ "regexkey": "\\/",
16
+ "replace": "",
17
+ "description": "Remove /"
18
+ },
19
+ {
20
+ "regexkey": "\\+",
21
+ "replace": "",
22
+ "description": "Remove +"
23
+ },
24
+ {
25
+ "regexkey": "\\$",
26
+ "replace": "",
27
+ "description": "Remove $"
28
+ },
29
+ {
30
+ "regexkey": "\\#",
31
+ "replace": "",
32
+ "description": "Remove #"
38
33
  }
39
- },
34
+ ],
35
+ "rootTopic-DIRECTIONS": "You can override the root topic by renaming _rootTopic to rootTopic",
36
+ "_rootTopic": "@bind=(state.equipment.alias).replace(' ','-').replace('/','').toLowerCase();",
37
+ "clientId": "@bind='mqttjs_njsPC_'+Math.random().toString(16).substr(2, 8);"
38
+ }
39
+ },
40
40
  "events": [
41
41
  {
42
42
  "name": "config",
@@ -67,7 +67,7 @@
67
67
  },
68
68
  {
69
69
  "topic": "state/startTime",
70
- "message": "@bind=data.startTime;"
70
+ "message": "@bind=data.startTime;"
71
71
  }
72
72
  ]
73
73
  },
@@ -184,7 +184,50 @@
184
184
  "topic": "state/temps/solar",
185
185
  "message": "{\"temp\":@bind=data.solar;}",
186
186
  "description": "Send solar temp.",
187
- "filter": "@bind=typeof data.solar === 'undefined';"
187
+ "filter": "@bind=typeof data.solar !== 'undefined';"
188
+ },
189
+ {
190
+ "topic": "state/temps/solarSensor2",
191
+ "message": "{\"temp\":@bind=data.solarSensor2;}",
192
+ "description": "Solar temp",
193
+ "filter": "@bind=typeof data.solarSensor2 !== 'undefined';"
194
+ },
195
+ {
196
+ "topic": "state/temps/solarSensor3",
197
+ "message": "{\"temp\":@bind=data.solarSensor3;}",
198
+ "description": "Solar temp",
199
+ "filter": "@bind=typeof data.solarSensor3 !== 'undefined';"
200
+ },
201
+ {
202
+ "topic": "state/temps/solarSensor4",
203
+ "message": "{\"temp\":@bind=data.solarSensor4;}",
204
+ "description": "Solar temp",
205
+ "filter": "@bind=typeof data.solarSensor4 !== 'undefined';"
206
+ },
207
+
208
+ {
209
+ "topic": "state/temps/waterSensor1",
210
+ "message": "{\"temp\":@bind=data.waterSensor1;}",
211
+ "description": "Water temp sensor 1",
212
+ "filter": "@bind=typeof data.waterSensor1 !== 'undefined';"
213
+ },
214
+ {
215
+ "topic": "state/temps/waterSensor2",
216
+ "message": "{\"temp\":@bind=data.waterSensor2;}",
217
+ "description": "Water temp sensor 2",
218
+ "filter": "@bind=typeof data.waterSensor2 !== 'undefined';"
219
+ },
220
+ {
221
+ "topic": "state/temps/waterSensor3",
222
+ "message": "{\"temp\":@bind=data.waterSensor3;}",
223
+ "description": "Water temp sensor 3",
224
+ "filter": "@bind=typeof data.waterSensor3 !== 'undefined';"
225
+ },
226
+ {
227
+ "topic": "state/temps/waterSensor4",
228
+ "message": "{\"temp\":@bind=data.waterSensor4;}",
229
+ "description": "Water temp sensor 4",
230
+ "filter": "@bind=typeof data.waterSensor4 !== 'undefined';"
188
231
  },
189
232
  {
190
233
  "topic": "state/temps/units",
@@ -303,6 +346,11 @@
303
346
  "message": "{\"type\":@bind=data.type;}",
304
347
  "description": "Send type."
305
348
  },
349
+ {
350
+ "topic": "state/chlorinators/@bind=data.id;/@bind=data.name;/model",
351
+ "message": "{\"type\":@bind=data.model;}",
352
+ "description": "Send Model."
353
+ },
306
354
  {
307
355
  "topic": "state/chlorinators/@bind=data.id;/@bind=data.name;/targetOutput",
308
356
  "message": "{\"targetOutput\":@bind=data.targetOutput;}",
@@ -582,6 +630,17 @@
582
630
  }
583
631
  ]
584
632
  },
633
+ {
634
+ "name": "chemicalDose",
635
+ "description": "Event when a chemical is being dosed",
636
+ "topics": [
637
+ {
638
+ "topic": "state/chemControllers/@bind=data.id;/@bind=data.chem;",
639
+ "message": "@bind=data;",
640
+ "enabled": true
641
+ }
642
+ ]
643
+ },
585
644
  {
586
645
  "name": "filter",
587
646
  "description": "Populate the filter topic",
@@ -92,7 +92,7 @@
92
92
  {
93
93
  "topic": "state/circuits/@bind=data.id;/endTime",
94
94
  "message": "@bind=data.endTime;",
95
- "description": "The end time for the circuit."
95
+ "description": "The end time for the circuit."
96
96
  },
97
97
  {
98
98
  "topic": "state/circuits/@bind=data.id;/lightingTheme",
@@ -295,6 +295,48 @@
295
295
  "description": "Solar temp",
296
296
  "filter": "@bind=typeof data.solar !== 'undefined';"
297
297
  },
298
+ {
299
+ "topic": "state/temps/solarSensor2",
300
+ "message": "@bind=data.solarSensor2;",
301
+ "description": "Solar temp",
302
+ "filter": "@bind=typeof data.solarSensor2 !== 'undefined';"
303
+ },
304
+ {
305
+ "topic": "state/temps/solarSensor3",
306
+ "message": "@bind=data.solarSensor3;",
307
+ "description": "Solar temp",
308
+ "filter": "@bind=typeof data.solarSensor3 !== 'undefined';"
309
+ },
310
+ {
311
+ "topic": "state/temps/solarSensor4",
312
+ "message": "@bind=data.solarSensor4;",
313
+ "description": "Solar temp",
314
+ "filter": "@bind=typeof data.solarSensor4 !== 'undefined';"
315
+ },
316
+ {
317
+ "topic": "state/temps/waterSensor1",
318
+ "message": "@bind=data.waterSensor1;",
319
+ "description": "Water temp sensor 1",
320
+ "filter": "@bind=typeof data.waterSensor1 !== 'undefined';"
321
+ },
322
+ {
323
+ "topic": "state/temps/waterSensor2",
324
+ "message": "@bind=data.waterSensor2;",
325
+ "description": "Water temp sensor 2",
326
+ "filter": "@bind=typeof data.waterSensor2 !== 'undefined';"
327
+ },
328
+ {
329
+ "topic": "state/temps/waterSensor3",
330
+ "message": "@bind=data.waterSensor3;",
331
+ "description": "Water temp sensor 3",
332
+ "filter": "@bind=typeof data.waterSensor3 !== 'undefined';"
333
+ },
334
+ {
335
+ "topic": "state/temps/waterSensor4",
336
+ "message": "@bind=data.waterSensor4;",
337
+ "description": "Water temp sensor 4",
338
+ "filter": "@bind=typeof data.waterSensor4 !== 'undefined';"
339
+ },
298
340
  {
299
341
  "topic": "state/temps/units",
300
342
  "message": "@bind=data.units;"
@@ -407,6 +449,11 @@
407
449
  "message": "@bind=data.type;",
408
450
  "description": "Send type."
409
451
  },
452
+ {
453
+ "topic": "state/chlorinators/@bind=data.id;/model",
454
+ "message": "@bind=data.model;",
455
+ "description": "Send Model"
456
+ },
410
457
  {
411
458
  "topic": "state/chlorinators/@bind=data.id;/targetOutput",
412
459
  "message": "@bind=data.targetOutput;",
@@ -602,6 +649,17 @@
602
649
  }
603
650
  ]
604
651
  },
652
+ {
653
+ "name": "chemicalDose",
654
+ "description": "Event when a chemical is being dosed",
655
+ "topics": [
656
+ {
657
+ "topic": "state/chemControllers/@bind=data.id;/@bind=data.chem;",
658
+ "message": "@bind=data;",
659
+ "enabled": true
660
+ }
661
+ ]
662
+ },
605
663
  {
606
664
  "name": "filter",
607
665
  "description": "Populate the filter topic",
@@ -126,6 +126,7 @@ export class InterfaceEvent {
126
126
  public options: any = {};
127
127
  public body: any = {};
128
128
  public vars: any = {};
129
+ public processor?: string[]
129
130
  }
130
131
  export class InterfaceContext {
131
132
  public name: string;
@@ -20,14 +20,15 @@ import * as http from "http";
20
20
  import * as https from "https";
21
21
  import extend=require("extend");
22
22
  import { logger } from "../../logger/Logger";
23
- import { sys } from "../../controller/Equipment";
24
- import { state } from "../../controller/State";
23
+ import { PoolSystem, sys } from "../../controller/Equipment";
24
+ import { State, state } from "../../controller/State";
25
25
  import { InterfaceContext, InterfaceEvent, BaseInterfaceBindings } from "./baseInterface";
26
26
 
27
27
  export class HttpInterfaceBindings extends BaseInterfaceBindings {
28
28
  constructor(cfg) {
29
29
  super(cfg);
30
30
  }
31
+ declare sockets: HttpInterfaceSocketEvent[];
31
32
  public bindEvent(evt: string, ...data: any) {
32
33
  // Find the binding by first looking for the specific event name.
33
34
  // If that doesn't exist then look for the "*" (all events).
@@ -121,4 +122,24 @@ export class HttpInterfaceBindings extends BaseInterfaceBindings {
121
122
  }
122
123
  }
123
124
  }
125
+ class HttpInterfaceSocketEvent {
126
+ event: string;
127
+ description: string;
128
+ processor: (sock: HttpInterfaceSocketEvent, sys: PoolSystem, state: State, value: any) => void;
129
+ constructor(sock: any) {
130
+ this.event = sock.event;
131
+ if (typeof sock.processor !== 'undefined') {
132
+ let fnBody = Array.isArray(sock.processor) ? sock.processor.join('\n') : sock.processor;
133
+ try {
134
+ this.processor = new Function('sock', 'sys', 'state', 'value', fnBody) as (sock: HttpInterfaceSocketEvent, sys: PoolSystem, state: State, value: any) => void;
135
+ } catch (err) { logger.error(`Error compiling socket event processor: ${err} -- ${fnBody}`); }
136
+ }
137
+ }
138
+ }
139
+ export interface IHTTPInterfaceSocketEvent {
140
+ event: string,
141
+ description: string,
142
+ processor?: string
143
+ }
144
+
124
145
 
@@ -16,7 +16,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
16
16
  */
17
17
 
18
18
  import extend = require("extend");
19
- import { ClientOptions, InfluxDB, Point, WriteApi, WritePrecisionType } from '@influxdata/influxdb-client';
19
+ import { ClientOptions, DEFAULT_WriteOptions, InfluxDB, Point, WriteApi, WriteOptions, WritePrecisionType } from '@influxdata/influxdb-client';
20
20
  import { utils, Timestamp } from '../../controller/Constants';
21
21
  import { logger } from "../../logger/Logger";
22
22
  import { BaseInterfaceBindings, InterfaceContext, InterfaceEvent } from "./baseInterface";
@@ -33,7 +33,8 @@ export class InfluxInterfaceBindings extends BaseInterfaceBindings {
33
33
  let url = 'http://';
34
34
  if (typeof baseOpts.protocol !== 'undefined' && baseOpts.protocol) url = baseOpts.protocol;
35
35
  if (!url.endsWith('://')) url += '://';
36
- url = `${url}${baseOpts.host}:${baseOpts.port}`;
36
+ url = `${url}${baseOpts.host}`;
37
+ if(typeof baseOpts.port !== 'undefined' && baseOpts.port !== null && !isNaN(baseOpts.port)) url = `${url}:${baseOpts.port}`;
37
38
  let influxDB: InfluxDB;
38
39
  let bucket;
39
40
  let org;
@@ -62,9 +63,6 @@ export class InfluxInterfaceBindings extends BaseInterfaceBindings {
62
63
  }
63
64
  influxDB = new InfluxDB(clientOptions);
64
65
  }
65
- this.writeApi = influxDB.getWriteApi(org, bucket, 'ms');
66
-
67
-
68
66
  // set global tags from context
69
67
  let baseTags = {}
70
68
  baseOpts.tags.forEach(tag => {
@@ -74,7 +72,40 @@ export class InfluxInterfaceBindings extends BaseInterfaceBindings {
74
72
  if (typeof sname !== 'undefined' && typeof svalue !== 'undefined' && !sname.includes('@bind') && !svalue.includes('@bind'))
75
73
  baseTags[sname] = svalue;
76
74
  })
77
- this.writeApi.useDefaultTags(baseTags);
75
+ //this.writeApi.useDefaultTags(baseTags);
76
+ const writeOptions:WriteOptions = {
77
+ /* the maximum points/line to send in a single batch to InfluxDB server */
78
+ batchSize: baseOpts.batchSize || 100,
79
+ /* default tags to add to every point */
80
+ defaultTags: baseTags,
81
+ /* maximum time in millis to keep points in an unflushed batch, 0 means don't periodically flush */
82
+ flushInterval: DEFAULT_WriteOptions.flushInterval,
83
+ /* maximum size of the retry buffer - it contains items that could not be sent for the first time */
84
+ maxBufferLines: DEFAULT_WriteOptions.maxBufferLines,
85
+ /* the count of retries, the delays between retries follow an exponential backoff strategy if there is no Retry-After HTTP header */
86
+ maxRetries: DEFAULT_WriteOptions.maxRetries,
87
+ /* maximum delay between retries in milliseconds */
88
+ maxRetryDelay: DEFAULT_WriteOptions.maxRetryDelay,
89
+ /* minimum delay between retries in milliseconds */
90
+ minRetryDelay: DEFAULT_WriteOptions.minRetryDelay, // minimum delay between retries
91
+ /* a random value of up to retryJitter is added when scheduling next retry */
92
+ retryJitter: DEFAULT_WriteOptions.retryJitter,
93
+ // ... or you can customize what to do on write failures when using a writeFailed fn, see the API docs for details
94
+ writeFailed: function(error, lines, failedAttempts){
95
+ /** return promise or void */
96
+ logger.error(`InfluxDB batch write failed writing ${lines.length} lines with ${failedAttempts} failed attempts. ${error.message}`);
97
+ },
98
+ writeSuccess: function(lines){
99
+ logger.silly(`InfluxDB successfully wrote ${lines.length} lines.`)
100
+ },
101
+ maxRetryTime: DEFAULT_WriteOptions.maxRetryTime,
102
+ exponentialBase: DEFAULT_WriteOptions.exponentialBase,
103
+ randomRetry: DEFAULT_WriteOptions.randomRetry
104
+ }
105
+ this.writeApi = influxDB.getWriteApi(org, bucket, 'ms', writeOptions);
106
+
107
+
108
+
78
109
  }
79
110
  public bindEvent(evt: string, ...data: any) {
80
111
 
@@ -115,6 +146,10 @@ export class InfluxInterfaceBindings extends BaseInterfaceBindings {
115
146
  }
116
147
  else {
117
148
  logger.error(`InfluxDB tag binding failure on ${evt}:${_tag.name}/${_tag.value} --> ${svalue || 'undefined'} ${JSON.stringify(data[0])}`);
149
+ if (typeof sname === 'undefined') logger.error(`InfluxDB tag name is undefined`);
150
+ if (typeof svalue === 'undefined') logger.error(`InfluxDB value is undefined`);
151
+ if (svalue.includes('@bind')) logger.error(`InfluxDB value not bound`);
152
+ if (svalue === null) logger.error(`InfluxDB value is null`);
118
153
  }
119
154
  })
120
155
  _point.fields.forEach(_field => {
@@ -188,14 +223,14 @@ export class InfluxInterfaceBindings extends BaseInterfaceBindings {
188
223
  let sec = ts.getSeconds() - 1;
189
224
  ts.setSeconds(sec);
190
225
  point2.timestamp(ts);
191
- logger.silly(`Writing influx ${e.name} inverse data point ${point2.toString()})`)
226
+ logger.silly(`Batching influx ${e.name} inverse data point ${point2.toString()})`)
192
227
  this.writeApi.writePoint(point2);
193
228
  }
194
229
  if (typeof point.toLineProtocol() !== 'undefined') {
195
- logger.silly(`Writing influx ${e.name} data point ${point.toString()}`)
230
+ logger.silly(`Batching influx ${e.name} data point ${point.toString()}`)
196
231
  this.writeApi.writePoint(point);
197
- this.writeApi.flush()
198
- .catch(error => { logger.error(error); });
232
+ // this.writeApi.flush()
233
+ // .catch(error => { logger.error(`Error flushing Influx data point ${point.toString()} ${error}`); });
199
234
  //logger.info(`INFLUX: ${point.toLineProtocol()}`)
200
235
  }
201
236
  else {
@@ -20,8 +20,8 @@ import * as http from "http";
20
20
  import * as https from "https";
21
21
  import extend = require("extend");
22
22
  import { logger } from "../../logger/Logger";
23
- import { sys } from "../../controller/Equipment";
24
- import { state } from "../../controller/State";
23
+ import { PoolSystem, sys } from "../../controller/Equipment";
24
+ import { State, state } from "../../controller/State";
25
25
  import { InterfaceEvent, BaseInterfaceBindings } from "./baseInterface";
26
26
  import { sys as sysAlias } from "../../controller/Equipment";
27
27
  import { state as stateAlias } from "../../controller/State";
@@ -35,8 +35,9 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
35
35
  this.subscribed = false;
36
36
  }
37
37
  private client: MqttClient;
38
- private topics: string[] = [];
38
+ private topics: MqttTopicSubscription[] = [];
39
39
  declare events: MqttInterfaceEvent[];
40
+ declare subscriptions: MqttTopicSubscription[];
40
41
  private subscribed: boolean; // subscribed to events or not
41
42
  private sentInitialMessages = false;
42
43
  private init = () => {
@@ -55,7 +56,6 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
55
56
  url
56
57
  }
57
58
  this.client = connect(url, opts);
58
-
59
59
  this.client.on('connect', () => {
60
60
  try {
61
61
  logger.info(`MQTT connected to ${url}`);
@@ -82,7 +82,7 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
82
82
  while (this.topics.length > 0) {
83
83
  let topic = this.topics.pop();
84
84
  if (typeof topic !== 'undefined') {
85
- this.client.unsubscribe(topic, (err, packet) => {
85
+ this.client.unsubscribe(topic.topicPath, (err, packet) => {
86
86
  if (err) logger.error(`Error unsubscribing from MQTT topic ${topic}`);
87
87
  else {
88
88
  logger.debug(`Unsubscribed from MQTT topic ${topic}`);
@@ -92,47 +92,47 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
92
92
  }
93
93
  } catch (err) { logger.error(`Error unsubcribing to MQTT topic: ${err.message}`); }
94
94
  }
95
- private subscribe() {
96
- try {
97
- if (this.topics.length > 0) this.unsubscribe();
98
- let root = this.rootTopic();
99
- this.topics.push(`${root}/state/+/setState`,
100
- `${root}/state/+/setstate`,
101
- `${root}/state/+/toggleState`,
102
- `${root}/state/+/togglestate`,
103
- `${root}/state/body/setPoint`,
104
- `${root}/state/body/setpoint`,
105
- `${root}/state/body/heatMode`,
106
- `${root}/state/body/heatmode`,
107
- `${root}/state/+/setTheme`,
108
- `${root}/state/+/settheme`,
109
- `${root}/state/temps`,
110
- `${root}/config/tempSensors`,
111
- `${root}/config/chemController`,
112
- `${root}/state/chemController`,
113
- `${root}/config/chlorinator`,
114
- `${root}/state/chlorinator`);
115
- for (let i = 0; i < this.topics.length; i++) {
116
- let topic = this.topics[i];
117
- // await new Promise<void>((resolve, reject) => {
118
- this.client.subscribe(topic, (err, granted) => {
119
- if (!err) {
120
- logger.debug(`MQTT subscribed to ${JSON.stringify(granted)}`);
121
- // resolve();
122
- }
123
- else {
124
- logger.error(`MQTT Subscribe: ${err}`);
125
- // reject(err);
126
- }
127
- });
128
- // });
129
-
95
+ protected subscribe() {
96
+ if (this.topics.length > 0) this.unsubscribe();
97
+ let root = this.rootTopic();
98
+ if (typeof this.subscriptions !== 'undefined') {
99
+ for (let i = 0; i < this.subscriptions.length; i++) {
100
+ let sub = this.subscriptions[i];
101
+ if(sub.enabled !== false) this.topics.push(new MqttTopicSubscription(root, sub));
102
+ }
103
+ }
104
+ else {
105
+ let arrTopics = [
106
+ `state/+/setState`,
107
+ `state/+/setstate`,
108
+ `state/+/toggleState`,
109
+ `state/+/togglestate`,
110
+ `state/body/setPoint`,
111
+ `state/body/setpoint`,
112
+ `state/body/heatMode`,
113
+ `state/body/heatmode`,
114
+ `state/+/setTheme`,
115
+ `state/+/settheme`,
116
+ `state/temps`,
117
+ `config/tempSensors`,
118
+ `config/chemController`,
119
+ `state/chemController`,
120
+ `config/chlorinator`,
121
+ `state/chlorinator`];
122
+ for (let i = 0; i < arrTopics.length; i++) {
123
+ this.topics.push(new MqttTopicSubscription(root, { topic: arrTopics[i] }));
130
124
  }
131
- this.client.on('message', async (topic, msg) => { try { await this.messageHandler(topic, msg) } catch (err) { logger.error(`Error processing MQTT request ${err}.`) }; })
132
- this.subscribed = true;
133
- } catch (err) { logger.error(`Error subcribing to MQTT topics`); }
125
+ }
126
+ for (let i = 0; i < this.topics.length; i++) {
127
+ let topic = this.topics[i];
128
+ this.client.subscribe(topic.topicPath, (err, granted) => {
129
+ if (!err) logger.debug(`MQTT subscribed to ${JSON.stringify(granted)}`);
130
+ else logger.error(`MQTT Subscribe: ${err}`);
131
+ });
132
+ }
133
+ this.client.on('message', async (topic, msg) => { try { await this.messageHandler(topic, msg) } catch (err) { logger.error(`Error processing MQTT request ${err}.`) }; })
134
+ this.subscribed = true;
134
135
  }
135
-
136
136
  // this will take in the MQTT Formatter options and format each token that is bound
137
137
  // otherwise, it's the same as the base buildTokens fn.
138
138
  // This could be combined into one fn but for now it's specific to MQTT formatting of topics
@@ -178,7 +178,6 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
178
178
  }
179
179
  return toks;
180
180
  }
181
-
182
181
  private rootTopic = () => {
183
182
  let toks = {};
184
183
  let baseOpts = extend(true, { headers: {} }, this.cfg.options, this.context.options);
@@ -187,7 +186,6 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
187
186
  topic = this.replaceTokens(baseOpts.rootTopic, toks);
188
187
  return topic;
189
188
  }
190
-
191
189
  public bindEvent(evt: string, ...data: any) {
192
190
  try {
193
191
  if (!this.sentInitialMessages && evt === 'controller' && data[0].status.val === 1) {
@@ -239,10 +237,28 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
239
237
  topic = `${rootTopic}/${this.replaceTokens(t.topic, topicToks)}`;
240
238
  // Filter out any topics where there may be undefined in it. We don't want any of this if that is the case.
241
239
  if (topic.endsWith('/undefined') || topic.indexOf('/undefined/') !== -1 || topic.startsWith('null/') || topic.indexOf('/null') !== -1) return;
242
-
243
-
244
- this.buildTokens(t.message, evt, topicToks, e, data[0]);
245
- message = this.tokensReplacer(t.message, evt, topicToks, e, data[0]);
240
+ if (typeof t.processor !== 'undefined') {
241
+ if (t.ignoreProcessor) message = "err";
242
+ else {
243
+ if (typeof t._fnProcessor !== 'function') {
244
+ let fnBody = Array.isArray(t.processor) ? t.processor.join('\n') : t.processor;
245
+ try {
246
+ // Try to compile it.
247
+ t._fnProcessor = new Function('ctx', 'pub', 'sys', 'state', 'data', fnBody) as (ctx: any, pub: MQTTPublishTopic, sys: PoolSystem, state: State, data: any) => any;
248
+ } catch (err) { logger.error(`Error compiling subscription processor: ${err} -- ${fnBody}`); t.ignoreProcessor = true; }
249
+ }
250
+ if (typeof t._fnProcessor === 'function') {
251
+ let ctx = { util: utils }
252
+ try {
253
+ message = t._fnProcessor(ctx, t, sys, state, data[0]).toString();
254
+ } catch (err) { logger.error(`Error publishing MQTT data for topic ${t.topic}: ${err.message}`); message = "err"; }
255
+ }
256
+ }
257
+ }
258
+ else {
259
+ this.buildTokens(t.message, evt, topicToks, e, data[0]);
260
+ message = this.tokensReplacer(t.message, evt, topicToks, e, data[0]);
261
+ }
246
262
 
247
263
  let publishOptions: IClientPublishOptions = { retain: typeof baseOpts.retain !== 'undefined' ? baseOpts.retain : true, qos: typeof baseOpts.qos !== 'undefined' ? baseOpts.qos : 2 };
248
264
  let changesOnly = typeof baseOpts.changesOnly !== 'undefined' ? baseOpts.changesOnly : true;
@@ -286,6 +302,16 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
286
302
  try {
287
303
  let msg = message.toString();
288
304
  if (msg[0] === '{') msg = JSON.parse(msg);
305
+
306
+ let sub: MqttTopicSubscription = this.topics.find(elem => topic === elem.topicPath);
307
+ if (typeof sub !== 'undefined') {
308
+ logger.debug(`Topic not found ${topic}`)
309
+ // Alright so now lets process our results.
310
+ if (typeof sub.processor === 'function') {
311
+ sub.executeProcessor(msg);
312
+ return;
313
+ }
314
+ }
289
315
  const topics = topic.split('/');
290
316
  logger.debug(`MQTT: Inbound ${topic}: ${message.toString()}`);
291
317
  if (topic.startsWith(this.rootTopic() + '/') && typeof msg === 'object') {
@@ -452,12 +478,10 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
452
478
  }
453
479
  }
454
480
  }
455
-
456
481
  class MqttInterfaceEvent extends InterfaceEvent {
457
- public topics: IMQTT[]
482
+ public topics: MQTTPublishTopic[]
458
483
  }
459
-
460
- export interface IMQTT {
484
+ export class MQTTPublishTopic {
461
485
  topic: string;
462
486
  message: string;
463
487
  description: string;
@@ -468,8 +492,44 @@ export interface IMQTT {
468
492
  filter?: string;
469
493
  lastSent: MQTTMessage[];
470
494
  options: any;
495
+ processor?: string[];
496
+ ignoreProcessor: boolean = false;
497
+ _fnProcessor: (ctx: any, pub: MQTTPublishTopic, sys: PoolSystem, state: State, data: any) => any
471
498
  }
472
499
  class MQTTMessage {
473
500
  topic: string;
474
501
  message: string;
475
502
  }
503
+
504
+ class MqttSubscriptions {
505
+ public subscriptions: IMQTTSubscription[]
506
+ }
507
+ class MqttTopicSubscription {
508
+ root: string;
509
+ topic: string;
510
+ enabled: boolean;
511
+ processor: (ctx: any, sub: MqttTopicSubscription, sys: PoolSystem, state: State, value: any) => void;
512
+ constructor(root: string, sub: any) {
513
+ this.root = sub.root || root;
514
+ this.topic = sub.topic;
515
+ if (typeof sub.processor !== 'undefined') {
516
+ let fnBody = Array.isArray(sub.processor) ? sub.processor.join('\n') : sub.processor;
517
+ try {
518
+ this.processor = new Function('ctx', 'sub', 'sys', 'state', 'value', fnBody) as (ctx: any, sub: MqttTopicSubscription, sys: PoolSystem, state: State, value: any) => void;
519
+ } catch (err) { logger.error(`Error compiling subscription processor: ${err} -- ${fnBody}`); }
520
+ }
521
+ }
522
+ public get topicPath(): string { return `${this.root}/${this.topic}` };
523
+ public executeProcessor(value: any) {
524
+ let ctx = { util:utils }
525
+ this.processor(ctx, this, sys, state, value);
526
+ state.emitEquipmentChanges();
527
+ }
528
+
529
+ }
530
+ export interface IMQTTSubscription {
531
+ topic: string,
532
+ description: string,
533
+ processor?: string,
534
+ enabled?: boolean
535
+ }