nodejs-poolcontroller 7.7.0 → 8.0.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.
- package/.eslintrc.json +26 -35
- package/Changelog +22 -0
- package/README.md +7 -3
- package/anslq25/MessagesMock.ts +218 -0
- package/anslq25/boards/MockBoardFactory.ts +50 -0
- package/anslq25/boards/MockEasyTouchBoard.ts +696 -0
- package/anslq25/boards/MockSystemBoard.ts +217 -0
- package/anslq25/chemistry/MockChlorinator.ts +75 -0
- package/anslq25/pumps/MockPump.ts +84 -0
- package/app.ts +10 -14
- package/config/Config.ts +13 -9
- package/config/VersionCheck.ts +6 -2
- package/controller/Constants.ts +58 -25
- package/controller/Equipment.ts +225 -41
- package/controller/Errors.ts +2 -1
- package/controller/Lockouts.ts +34 -2
- package/controller/State.ts +491 -48
- package/controller/boards/AquaLinkBoard.ts +6 -3
- package/controller/boards/BoardFactory.ts +5 -1
- package/controller/boards/EasyTouchBoard.ts +1971 -1751
- package/controller/boards/IntelliCenterBoard.ts +1311 -1688
- package/controller/boards/IntelliComBoard.ts +7 -1
- package/controller/boards/IntelliTouchBoard.ts +153 -42
- package/controller/boards/NixieBoard.ts +209 -66
- package/controller/boards/SunTouchBoard.ts +393 -0
- package/controller/boards/SystemBoard.ts +1862 -1543
- package/controller/comms/Comms.ts +539 -138
- package/controller/comms/ScreenLogic.ts +1663 -0
- package/controller/comms/messages/Messages.ts +242 -60
- package/controller/comms/messages/config/ChlorinatorMessage.ts +4 -3
- package/controller/comms/messages/config/CircuitGroupMessage.ts +5 -2
- package/controller/comms/messages/config/CircuitMessage.ts +81 -13
- package/controller/comms/messages/config/ConfigMessage.ts +3 -1
- package/controller/comms/messages/config/CoverMessage.ts +2 -1
- package/controller/comms/messages/config/CustomNameMessage.ts +2 -1
- package/controller/comms/messages/config/EquipmentMessage.ts +5 -1
- package/controller/comms/messages/config/ExternalMessage.ts +33 -3
- package/controller/comms/messages/config/FeatureMessage.ts +2 -1
- package/controller/comms/messages/config/GeneralMessage.ts +2 -1
- package/controller/comms/messages/config/HeaterMessage.ts +3 -1
- package/controller/comms/messages/config/IntellichemMessage.ts +2 -1
- package/controller/comms/messages/config/OptionsMessage.ts +12 -6
- package/controller/comms/messages/config/PumpMessage.ts +9 -12
- package/controller/comms/messages/config/RemoteMessage.ts +80 -13
- package/controller/comms/messages/config/ScheduleMessage.ts +43 -3
- package/controller/comms/messages/config/SecurityMessage.ts +2 -1
- package/controller/comms/messages/config/ValveMessage.ts +43 -26
- package/controller/comms/messages/status/ChlorinatorStateMessage.ts +8 -7
- package/controller/comms/messages/status/EquipmentStateMessage.ts +93 -20
- package/controller/comms/messages/status/HeaterStateMessage.ts +24 -5
- package/controller/comms/messages/status/IntelliChemStateMessage.ts +7 -4
- package/controller/comms/messages/status/IntelliValveStateMessage.ts +2 -1
- package/controller/comms/messages/status/PumpStateMessage.ts +72 -4
- package/controller/comms/messages/status/VersionMessage.ts +2 -1
- package/controller/nixie/Nixie.ts +15 -4
- package/controller/nixie/NixieEquipment.ts +1 -0
- package/controller/nixie/chemistry/ChemController.ts +300 -129
- package/controller/nixie/chemistry/ChemDoser.ts +806 -0
- package/controller/nixie/chemistry/Chlorinator.ts +133 -129
- package/controller/nixie/circuits/Circuit.ts +171 -30
- package/controller/nixie/heaters/Heater.ts +337 -173
- package/controller/nixie/pumps/Pump.ts +264 -236
- package/controller/nixie/schedules/Schedule.ts +9 -3
- package/defaultConfig.json +46 -5
- package/logger/Logger.ts +38 -9
- package/package.json +13 -9
- package/web/Server.ts +235 -122
- package/web/bindings/aqualinkD.json +114 -59
- package/web/bindings/homeassistant.json +437 -0
- package/web/bindings/influxDB.json +15 -0
- package/web/bindings/mqtt.json +28 -9
- package/web/bindings/mqttAlt.json +15 -0
- package/web/interfaces/baseInterface.ts +58 -7
- package/web/interfaces/httpInterface.ts +5 -2
- package/web/interfaces/influxInterface.ts +9 -2
- package/web/interfaces/mqttInterface.ts +234 -74
- package/web/interfaces/ruleInterface.ts +87 -0
- package/web/services/config/Config.ts +140 -33
- package/web/services/config/ConfigSocket.ts +2 -1
- package/web/services/state/State.ts +144 -3
- package/web/services/state/StateSocket.ts +65 -14
- package/web/services/utilities/Utilities.ts +189 -1
|
@@ -22,11 +22,11 @@ import extend = require("extend");
|
|
|
22
22
|
import { logger } from "../../logger/Logger";
|
|
23
23
|
import { PoolSystem, sys } from "../../controller/Equipment";
|
|
24
24
|
import { State, state } from "../../controller/State";
|
|
25
|
-
import { InterfaceEvent, BaseInterfaceBindings } from "./baseInterface";
|
|
25
|
+
import { InterfaceEvent, BaseInterfaceBindings, InterfaceContext, IInterfaceEvent } from "./baseInterface";
|
|
26
26
|
import { sys as sysAlias } from "../../controller/Equipment";
|
|
27
27
|
import { state as stateAlias } from "../../controller/State";
|
|
28
28
|
import { webApp as webAppAlias } from '../Server';
|
|
29
|
-
import { utils } from "../../controller/Constants";
|
|
29
|
+
import { Timestamp, Utils, utils } from "../../controller/Constants";
|
|
30
30
|
import { ServiceParameterError } from '../../controller/Errors';
|
|
31
31
|
|
|
32
32
|
export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
@@ -34,66 +34,103 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
34
34
|
super(cfg);
|
|
35
35
|
this.subscribed = false;
|
|
36
36
|
}
|
|
37
|
-
|
|
37
|
+
public client: MqttClient;
|
|
38
38
|
private topics: MqttTopicSubscription[] = [];
|
|
39
39
|
declare events: MqttInterfaceEvent[];
|
|
40
40
|
declare subscriptions: MqttTopicSubscription[];
|
|
41
41
|
private subscribed: boolean; // subscribed to events or not
|
|
42
42
|
private sentInitialMessages = false;
|
|
43
|
-
private init = () => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
43
|
+
private init = () => { (async () => { await this.initAsync(); })(); }
|
|
44
|
+
public async initAsync() {
|
|
45
|
+
try {
|
|
46
|
+
if (this.client) await this.stopAsync();
|
|
47
|
+
logger.info(`Initializing MQTT client ${this.cfg.name}`);
|
|
48
|
+
let baseOpts = extend(true, { headers: {} }, this.cfg.options, this.context.options);
|
|
49
|
+
if ((typeof baseOpts.hostname === 'undefined' || !baseOpts.hostname) && (typeof baseOpts.host === 'undefined' || !baseOpts.host || baseOpts.host === '*')) {
|
|
50
|
+
logger.warn(`Interface: ${this.cfg.name} has not resolved to a valid host.`);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const url = `${baseOpts.protocol || 'mqtt://'}${baseOpts.host}:${baseOpts.port || 1883}`;
|
|
54
|
+
let toks = {};
|
|
55
|
+
const opts = {
|
|
56
|
+
clientId: this.tokensReplacer(baseOpts.clientId, undefined, toks, { vars: {} } as any, {}),
|
|
57
|
+
username: baseOpts.username,
|
|
58
|
+
password: baseOpts.password,
|
|
59
|
+
rejectUnauthorized: !baseOpts.selfSignedCertificate,
|
|
60
|
+
url
|
|
61
|
+
}
|
|
62
|
+
this.setWillOptions(opts);
|
|
63
|
+
this.client = connect(url, opts);
|
|
64
|
+
this.client.on('connect', async () => {
|
|
65
|
+
try {
|
|
66
|
+
logger.info(`MQTT connected to ${url}`);
|
|
67
|
+
await this.subscribe();
|
|
68
|
+
// make sure status is up to date immediately
|
|
69
|
+
// especially in the case of a re-connect
|
|
70
|
+
this.bindEvent("controller", state.controllerState);
|
|
71
|
+
} catch (err) { logger.error(err); }
|
|
72
|
+
});
|
|
73
|
+
this.client.on('reconnect', () => {
|
|
74
|
+
try {
|
|
75
|
+
logger.info(`Re-connecting to MQTT broker ${this.cfg.name}`);
|
|
76
|
+
} catch (err) { logger.error(err); }
|
|
65
77
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
78
|
+
});
|
|
79
|
+
this.client.on('error', (error) => {
|
|
80
|
+
logger.error(`MQTT error ${error}`)
|
|
81
|
+
this.clearWillState();
|
|
82
|
+
});
|
|
83
|
+
} catch (err) { logger.error(`Error initializing MQTT client ${this.cfg.name}: ${err}`); }
|
|
69
84
|
}
|
|
70
85
|
public async stopAsync() {
|
|
71
86
|
try {
|
|
72
87
|
if (typeof this.client !== 'undefined') {
|
|
73
|
-
this.unsubscribe();
|
|
74
|
-
|
|
75
|
-
|
|
88
|
+
await this.unsubscribe();
|
|
89
|
+
await new Promise<boolean>((resolve, reject) => {
|
|
90
|
+
this.client.end(true, { reasonCode: 0, reasonString: `Shutting down MQTT Client` }, () => {
|
|
91
|
+
resolve(true);
|
|
92
|
+
logger.info(`Successfully shut down MQTT Client`);
|
|
93
|
+
});
|
|
76
94
|
});
|
|
95
|
+
if (this.client) this.client.removeAllListeners();
|
|
96
|
+
this.client = null;
|
|
77
97
|
}
|
|
78
98
|
} catch (err) { logger.error(`Error stopping MQTT Client: ${err.message}`); }
|
|
79
99
|
}
|
|
100
|
+
public async reload(data) {
|
|
101
|
+
try {
|
|
102
|
+
await this.unsubscribe();
|
|
103
|
+
this.context = Object.assign<InterfaceContext, any>(new InterfaceContext(), data.context);
|
|
104
|
+
this.events = Object.assign<MqttInterfaceEvent[], any>([], data.events);
|
|
105
|
+
this.subscriptions = Object.assign<MqttTopicSubscription[], any>([], data.subscriptions);
|
|
106
|
+
await this.subscribe();
|
|
107
|
+
} catch (err) { logger.error(`Error reloading MQTT bindings`); }
|
|
108
|
+
}
|
|
80
109
|
private async unsubscribe() {
|
|
81
110
|
try {
|
|
111
|
+
this.client.off('message', this.messageHandler);
|
|
82
112
|
while (this.topics.length > 0) {
|
|
83
113
|
let topic = this.topics.pop();
|
|
84
114
|
if (typeof topic !== 'undefined') {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
115
|
+
await new Promise<boolean>((resolve, reject) => {
|
|
116
|
+
this.client.unsubscribe(topic.topicPath, (err, packet) => {
|
|
117
|
+
if (err) {
|
|
118
|
+
logger.error(`Error unsubscribing from MQTT topic ${topic.topicPath}: ${err}`);
|
|
119
|
+
resolve(false);
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
logger.debug(`Unsubscribed from MQTT topic ${topic.topicPath}`);
|
|
123
|
+
resolve(true);
|
|
124
|
+
}
|
|
125
|
+
});
|
|
90
126
|
});
|
|
91
127
|
}
|
|
92
128
|
}
|
|
129
|
+
this.subscribed = false;
|
|
93
130
|
} catch (err) { logger.error(`Error unsubcribing to MQTT topic: ${err.message}`); }
|
|
94
131
|
}
|
|
95
|
-
protected subscribe() {
|
|
96
|
-
if (this.topics.length > 0) this.unsubscribe();
|
|
132
|
+
protected async subscribe() {
|
|
133
|
+
if (this.topics.length > 0) await this.unsubscribe();
|
|
97
134
|
let root = this.rootTopic();
|
|
98
135
|
if (typeof this.subscriptions !== 'undefined') {
|
|
99
136
|
for (let i = 0; i < this.subscriptions.length; i++) {
|
|
@@ -101,7 +138,7 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
101
138
|
if(sub.enabled !== false) this.topics.push(new MqttTopicSubscription(root, sub));
|
|
102
139
|
}
|
|
103
140
|
}
|
|
104
|
-
else {
|
|
141
|
+
else if (typeof root !== 'undefined') {
|
|
105
142
|
let arrTopics = [
|
|
106
143
|
`state/+/setState`,
|
|
107
144
|
`state/+/setstate`,
|
|
@@ -109,6 +146,8 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
109
146
|
`state/+/togglestate`,
|
|
110
147
|
`state/body/setPoint`,
|
|
111
148
|
`state/body/setpoint`,
|
|
149
|
+
`state/body/heatSetpoint`,
|
|
150
|
+
`state/body/coolSetpoint`,
|
|
112
151
|
`state/body/heatMode`,
|
|
113
152
|
`state/body/heatmode`,
|
|
114
153
|
`state/+/setTheme`,
|
|
@@ -126,11 +165,11 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
126
165
|
for (let i = 0; i < this.topics.length; i++) {
|
|
127
166
|
let topic = this.topics[i];
|
|
128
167
|
this.client.subscribe(topic.topicPath, (err, granted) => {
|
|
129
|
-
if (!err) logger.
|
|
168
|
+
if (!err) logger.verbose(`MQTT subscribed to ${JSON.stringify(granted)}`);
|
|
130
169
|
else logger.error(`MQTT Subscribe: ${err}`);
|
|
131
170
|
});
|
|
132
171
|
}
|
|
133
|
-
this.client.on('message',
|
|
172
|
+
this.client.on('message', this.messageHandler);
|
|
134
173
|
this.subscribed = true;
|
|
135
174
|
}
|
|
136
175
|
// this will take in the MQTT Formatter options and format each token that is bound
|
|
@@ -141,10 +180,10 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
141
180
|
let s = input;
|
|
142
181
|
let regx = /(?<=@bind\=\s*).*?(?=\;)/g;
|
|
143
182
|
let match;
|
|
144
|
-
let vars = extend(true, {}, this.cfg.vars, this.context.vars, typeof e !== 'undefined' && e.vars);
|
|
145
183
|
let sys = sysAlias;
|
|
146
184
|
let state = stateAlias;
|
|
147
185
|
let webApp = webAppAlias;
|
|
186
|
+
let vars = this.bindVarTokens(e, eventName, data);
|
|
148
187
|
// Map all the returns to the token list. We are being very basic
|
|
149
188
|
// here an the object graph is simply based upon the first object occurrence.
|
|
150
189
|
// We simply want to eval against that object reference.
|
|
@@ -178,7 +217,42 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
178
217
|
}
|
|
179
218
|
return toks;
|
|
180
219
|
}
|
|
181
|
-
private
|
|
220
|
+
private setWillOptions = (connectOpts) => {
|
|
221
|
+
const baseOpts = extend(true, { headers: {} }, this.cfg.options, this.context.options);
|
|
222
|
+
|
|
223
|
+
if (baseOpts.willTopic !== 'undefined') {
|
|
224
|
+
const rootTopic = this.rootTopic();
|
|
225
|
+
const topic = `${rootTopic}/${baseOpts.willTopic}`;
|
|
226
|
+
const publishOptions = {
|
|
227
|
+
retain: typeof baseOpts.retain !== 'undefined' ? baseOpts.retain : true,
|
|
228
|
+
qos: typeof baseOpts.qos !== 'undefined' ? baseOpts.qos : 2
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
connectOpts.will = {
|
|
232
|
+
topic: topic,
|
|
233
|
+
payload: baseOpts.willPayload,
|
|
234
|
+
retain: publishOptions.retain,
|
|
235
|
+
qos: publishOptions.qos
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
private clearWillState() {
|
|
240
|
+
if (typeof this.client.options.will === 'undefined') return;
|
|
241
|
+
let willTopic = this.client.options.will.topic;
|
|
242
|
+
let willPayload = this.client.options.will.payload;
|
|
243
|
+
|
|
244
|
+
if (typeof this.events !== 'undefined') this.events.forEach(evt => {
|
|
245
|
+
if (typeof evt.topics !== 'undefined') evt.topics.forEach(t => {
|
|
246
|
+
if (typeof t.lastSent !== 'undefined') {
|
|
247
|
+
let lm = t.lastSent.find(elem => elem.topic === willTopic);
|
|
248
|
+
if (typeof lm !== 'undefined') {
|
|
249
|
+
lm.message = willPayload.toString();
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
public rootTopic = () => {
|
|
182
256
|
let toks = {};
|
|
183
257
|
let baseOpts = extend(true, { headers: {} }, this.cfg.options, this.context.options);
|
|
184
258
|
let topic = '';
|
|
@@ -189,6 +263,7 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
189
263
|
public bindEvent(evt: string, ...data: any) {
|
|
190
264
|
try {
|
|
191
265
|
if (!this.sentInitialMessages && evt === 'controller' && data[0].status.val === 1) {
|
|
266
|
+
// Emitting all the equipment messages
|
|
192
267
|
state.emitAllEquipmentChanges();
|
|
193
268
|
this.sentInitialMessages = true;
|
|
194
269
|
}
|
|
@@ -234,9 +309,23 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
234
309
|
// a value like @bind=data.name; would be eval'd the same
|
|
235
310
|
// across all topics
|
|
236
311
|
this.buildTokensWithFormatter(t.topic, evt, topicToks, e, data[0], topicFormatter);
|
|
237
|
-
topic =
|
|
312
|
+
topic = this.replaceTokens(t.topic, topicToks);
|
|
313
|
+
if (t.useRootTopic !== false) topic = `${rootTopic}/${topic}`;
|
|
238
314
|
// Filter out any topics where there may be undefined in it. We don't want any of this if that is the case.
|
|
239
315
|
if (topic.endsWith('/undefined') || topic.indexOf('/undefined/') !== -1 || topic.startsWith('null/') || topic.indexOf('/null') !== -1) return;
|
|
316
|
+
let publishOptions: IClientPublishOptions = { retain: typeof baseOpts.retain !== 'undefined' ? baseOpts.retain : true, qos: typeof baseOpts.qos !== 'undefined' ? baseOpts.qos : 2 };
|
|
317
|
+
let changesOnly = typeof baseOpts.changesOnly !== 'undefined' ? baseOpts.changesOnly : true;
|
|
318
|
+
if (typeof e.options !== 'undefined') {
|
|
319
|
+
if (typeof e.options.retain !== 'undefined') publishOptions.retain = e.options.retain;
|
|
320
|
+
if (typeof e.options.qos !== 'undefined') publishOptions.retain = e.options.qos;
|
|
321
|
+
if (typeof e.options.changesOnly !== 'undefined') changesOnly = e.options.changesOnly;
|
|
322
|
+
}
|
|
323
|
+
if (typeof t.options !== 'undefined') {
|
|
324
|
+
if (typeof t.options.retain !== 'undefined') publishOptions.retain = t.options.retain;
|
|
325
|
+
if (typeof t.options.qos !== 'undefined') publishOptions.qos = t.options.qos;
|
|
326
|
+
if (typeof t.options.changeOnly !== 'undefined') changesOnly = t.options.changesOnly;
|
|
327
|
+
}
|
|
328
|
+
|
|
240
329
|
if (typeof t.processor !== 'undefined') {
|
|
241
330
|
if (t.ignoreProcessor) message = "err";
|
|
242
331
|
else {
|
|
@@ -248,9 +337,11 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
248
337
|
} catch (err) { logger.error(`Error compiling subscription processor: ${err} -- ${fnBody}`); t.ignoreProcessor = true; }
|
|
249
338
|
}
|
|
250
339
|
if (typeof t._fnProcessor === 'function') {
|
|
251
|
-
let
|
|
340
|
+
let vars = this.bindVarTokens(e, evt, data);
|
|
341
|
+
let ctx = { util: utils, rootTopic: rootTopic, topic: topic, opts: opts, vars: vars }
|
|
252
342
|
try {
|
|
253
343
|
message = t._fnProcessor(ctx, t, sys, state, data[0]).toString();
|
|
344
|
+
topic = ctx.topic;
|
|
254
345
|
} catch (err) { logger.error(`Error publishing MQTT data for topic ${t.topic}: ${err.message}`); message = "err"; }
|
|
255
346
|
}
|
|
256
347
|
}
|
|
@@ -260,18 +351,6 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
260
351
|
message = this.tokensReplacer(t.message, evt, topicToks, e, data[0]);
|
|
261
352
|
}
|
|
262
353
|
|
|
263
|
-
let publishOptions: IClientPublishOptions = { retain: typeof baseOpts.retain !== 'undefined' ? baseOpts.retain : true, qos: typeof baseOpts.qos !== 'undefined' ? baseOpts.qos : 2 };
|
|
264
|
-
let changesOnly = typeof baseOpts.changesOnly !== 'undefined' ? baseOpts.changesOnly : true;
|
|
265
|
-
if (typeof e.options !== 'undefined') {
|
|
266
|
-
if (typeof e.options.retain !== 'undefined') publishOptions.retain = e.options.retain;
|
|
267
|
-
if (typeof e.options.qos !== 'undefined') publishOptions.retain = e.options.qos;
|
|
268
|
-
if (typeof e.options.changesOnly !== 'undefined') changesOnly = e.options.changesOnly;
|
|
269
|
-
}
|
|
270
|
-
if (typeof t.options !== 'undefined') {
|
|
271
|
-
if (typeof t.options.retain !== 'undefined') publishOptions.retain = t.options.retain;
|
|
272
|
-
if (typeof t.options.qos !== 'undefined') publishOptions.qos = t.options.qos;
|
|
273
|
-
if (typeof t.options.changeOnly !== 'undefined') changesOnly = t.options.changesOnly;
|
|
274
|
-
}
|
|
275
354
|
if (changesOnly) {
|
|
276
355
|
if (typeof t.lastSent === 'undefined') t.lastSent = [];
|
|
277
356
|
let lm = t.lastSent.find(elem => elem.topic === topic);
|
|
@@ -298,22 +377,28 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
298
377
|
logger.error(err);
|
|
299
378
|
}
|
|
300
379
|
}
|
|
301
|
-
|
|
380
|
+
// This needed to be refactored so we could extract it from an anonymous function. We want to be able to unbind
|
|
381
|
+
// from it
|
|
382
|
+
private messageHandler = (topic, message) => { (async () => { await this.processMessage(topic, message); })(); }
|
|
383
|
+
private processMessage = async (topic, message) => {
|
|
302
384
|
try {
|
|
385
|
+
if (!state.isInitialized){
|
|
386
|
+
logger.info(`MQTT: **TOPIC IGNORED, SYSTEM NOT READY** Inbound ${topic}: ${message.toString()}`);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
303
389
|
let msg = message.toString();
|
|
304
390
|
if (msg[0] === '{') msg = JSON.parse(msg);
|
|
305
391
|
|
|
306
392
|
let sub: MqttTopicSubscription = this.topics.find(elem => topic === elem.topicPath);
|
|
307
393
|
if (typeof sub !== 'undefined') {
|
|
308
|
-
logger.debug(`
|
|
394
|
+
logger.debug(`MQTT: Inbound ${topic} ${message.toString()}`);
|
|
309
395
|
// Alright so now lets process our results.
|
|
310
|
-
if (typeof sub.
|
|
311
|
-
sub.executeProcessor(msg);
|
|
396
|
+
if (typeof sub.fnProcessor === 'function') {
|
|
397
|
+
sub.executeProcessor(this, msg);
|
|
312
398
|
return;
|
|
313
399
|
}
|
|
314
400
|
}
|
|
315
401
|
const topics = topic.split('/');
|
|
316
|
-
logger.debug(`MQTT: Inbound ${topic}: ${message.toString()}`);
|
|
317
402
|
if (topic.startsWith(this.rootTopic() + '/') && typeof msg === 'object') {
|
|
318
403
|
// RKS: Not sure why there is no processing of state vs config here. Right now the topics are unique
|
|
319
404
|
// between them so it doesn't matter but it will become an issue.
|
|
@@ -323,12 +408,12 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
323
408
|
if (typeof id !== 'undefined' && isNaN(id)) {
|
|
324
409
|
logger.error(`Inbound MQTT ${topics} has an invalid id (${id}) in the message (${msg}).`)
|
|
325
410
|
};
|
|
326
|
-
let isOn = utils.makeBool(msg.isOn);
|
|
411
|
+
let isOn = typeof msg.isOn !== 'undefined' ? utils.makeBool(msg.isOn) : typeof msg.state !== 'undefined' ? utils.makeBool(msg.state) : undefined;
|
|
327
412
|
switch (topics[topics.length - 2].toLowerCase()) {
|
|
328
413
|
case 'circuits':
|
|
329
414
|
case 'circuit': {
|
|
330
415
|
try {
|
|
331
|
-
if
|
|
416
|
+
if(typeof isOn !== 'undefined') await sys.board.circuits.setCircuitStateAsync(id, isOn);
|
|
332
417
|
}
|
|
333
418
|
catch (err) { logger.error(err); }
|
|
334
419
|
break;
|
|
@@ -336,7 +421,7 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
336
421
|
case 'features':
|
|
337
422
|
case 'feature': {
|
|
338
423
|
try {
|
|
339
|
-
if (
|
|
424
|
+
if (typeof isOn !== 'undefined') await sys.board.features.setFeatureStateAsync(id, isOn);
|
|
340
425
|
}
|
|
341
426
|
catch (err) { logger.error(err); }
|
|
342
427
|
break;
|
|
@@ -344,7 +429,7 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
344
429
|
case 'lightgroups':
|
|
345
430
|
case 'lightgroup': {
|
|
346
431
|
try {
|
|
347
|
-
await sys.board.circuits.setLightGroupStateAsync(id, isOn);
|
|
432
|
+
if (typeof isOn !== 'undefined') await sys.board.circuits.setLightGroupStateAsync(id, isOn);
|
|
348
433
|
}
|
|
349
434
|
catch (err) { logger.error(err); }
|
|
350
435
|
break;
|
|
@@ -352,7 +437,7 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
352
437
|
case 'circuitgroups':
|
|
353
438
|
case 'circuitgroup': {
|
|
354
439
|
try {
|
|
355
|
-
await sys.board.circuits.setCircuitGroupStateAsync(id, isOn);
|
|
440
|
+
if (typeof isOn !== 'undefined') await sys.board.circuits.setCircuitGroupStateAsync(id, isOn);
|
|
356
441
|
}
|
|
357
442
|
catch (err) { logger.error(err); }
|
|
358
443
|
break;
|
|
@@ -388,6 +473,41 @@ export class MqttInterfaceBindings extends BaseInterfaceBindings {
|
|
|
388
473
|
}
|
|
389
474
|
break;
|
|
390
475
|
}
|
|
476
|
+
case 'heatsetpoint':
|
|
477
|
+
try {
|
|
478
|
+
let body = sys.bodies.findByObject(msg);
|
|
479
|
+
if (topics[topics.length - 2].toLowerCase() === 'body') {
|
|
480
|
+
if (typeof body === 'undefined') {
|
|
481
|
+
logger.error(new ServiceParameterError(`Cannot set body heatSetpoint. You must supply a valid id, circuit, name, or type for the body`, 'body', 'id', msg.id));
|
|
482
|
+
return;
|
|
483
|
+
}
|
|
484
|
+
if (typeof msg.setPoint !== 'undefined' || typeof msg.heatSetpoint !== 'undefined') {
|
|
485
|
+
let setPoint = parseInt(msg.setPoint, 10) || parseInt(msg.heatSetpoint, 10);
|
|
486
|
+
if (!isNaN(setPoint)) {
|
|
487
|
+
await sys.board.bodies.setHeatSetpointAsync(body, setPoint);
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
catch (err) { logger.error(err); }
|
|
493
|
+
break;
|
|
494
|
+
case 'coolsetpoint':
|
|
495
|
+
try {
|
|
496
|
+
let body = sys.bodies.findByObject(msg);
|
|
497
|
+
if (topics[topics.length - 2].toLowerCase() === 'body') {
|
|
498
|
+
if (typeof body === 'undefined') {
|
|
499
|
+
logger.error(new ServiceParameterError(`Cannot set body coolSetpoint. You must supply a valid id, circuit, name, or type for the body`, 'body', 'id', msg.id));
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (typeof msg.setPoint !== 'undefined' || typeof msg.coolSetpoint !== 'undefined') {
|
|
503
|
+
let setPoint = parseInt(msg.coolSetpoint, 10) || parseInt(msg.coolSetpoint, 10);
|
|
504
|
+
if (!isNaN(setPoint)) {
|
|
505
|
+
await sys.board.bodies.setCoolSetpointAsync(body, setPoint);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
} catch (err) { logger.error(err); }
|
|
510
|
+
break;
|
|
391
511
|
case 'setpoint':
|
|
392
512
|
try {
|
|
393
513
|
let body = sys.bodies.findByObject(msg);
|
|
@@ -483,6 +603,7 @@ class MqttInterfaceEvent extends InterfaceEvent {
|
|
|
483
603
|
}
|
|
484
604
|
export class MQTTPublishTopic {
|
|
485
605
|
topic: string;
|
|
606
|
+
useRootTopic: boolean;
|
|
486
607
|
message: string;
|
|
487
608
|
description: string;
|
|
488
609
|
formatter: any[];
|
|
@@ -504,28 +625,67 @@ class MQTTMessage {
|
|
|
504
625
|
class MqttSubscriptions {
|
|
505
626
|
public subscriptions: IMQTTSubscription[]
|
|
506
627
|
}
|
|
507
|
-
class MqttTopicSubscription {
|
|
628
|
+
class MqttTopicSubscription implements IInterfaceEvent {
|
|
508
629
|
root: string;
|
|
509
630
|
topic: string;
|
|
510
631
|
enabled: boolean;
|
|
511
|
-
|
|
632
|
+
fnProcessor: (ctx: any, sub: MqttTopicSubscription, sys: PoolSystem, state: State, value: any) => void;
|
|
633
|
+
options: any = {};
|
|
512
634
|
constructor(root: string, sub: any) {
|
|
513
635
|
this.root = sub.root || root;
|
|
514
636
|
this.topic = sub.topic;
|
|
515
637
|
if (typeof sub.processor !== 'undefined') {
|
|
516
638
|
let fnBody = Array.isArray(sub.processor) ? sub.processor.join('\n') : sub.processor;
|
|
517
639
|
try {
|
|
518
|
-
this.
|
|
640
|
+
this.fnProcessor = new Function('ctx', 'sub', 'sys', 'state', 'value', fnBody) as (ctx: any, sub: MqttTopicSubscription, sys: PoolSystem, state: State, value: any) => void;
|
|
519
641
|
} catch (err) { logger.error(`Error compiling subscription processor: ${err} -- ${fnBody}`); }
|
|
520
642
|
}
|
|
521
643
|
}
|
|
522
644
|
public get topicPath(): string { return `${this.root}/${this.topic}` };
|
|
523
|
-
public executeProcessor(value: any) {
|
|
524
|
-
let
|
|
525
|
-
|
|
645
|
+
public executeProcessor(bindings: MqttInterfaceBindings, value: any) {
|
|
646
|
+
let baseOpts = extend(true, { headers: {} }, bindings.cfg.options, bindings.context.options);
|
|
647
|
+
let opts = extend(true, baseOpts, this.options);
|
|
648
|
+
let vars = bindings.bindVarTokens(this, this.topic, value);
|
|
649
|
+
|
|
650
|
+
let ctx = {
|
|
651
|
+
util: utils,
|
|
652
|
+
client: bindings.client,
|
|
653
|
+
vars: vars || {},
|
|
654
|
+
publish: (topic: string, message: any, options?: any) => {
|
|
655
|
+
try {
|
|
656
|
+
let msg: string;
|
|
657
|
+
if (typeof message === 'undefined') msg = '';
|
|
658
|
+
else if (typeof message === 'string') msg = message;
|
|
659
|
+
else if (typeof message === 'boolean') msg = message ? 'true' : 'false';
|
|
660
|
+
else if (message instanceof Timestamp) (message as Timestamp).format();
|
|
661
|
+
else if (typeof message.getTime === 'function') msg = Timestamp.toISOLocal(message);
|
|
662
|
+
else {
|
|
663
|
+
msg = Utils.stringifyJSON(message);
|
|
664
|
+
}
|
|
665
|
+
let baseOpts = extend(true, { headers: {} }, bindings.cfg.options, bindings.context.options);
|
|
666
|
+
let pubOpts: IClientPublishOptions = { retain: typeof baseOpts.retain !== 'undefined' ? baseOpts.retain : true, qos: typeof baseOpts.qos !== 'undefined' ? baseOpts.qos : 2 };
|
|
667
|
+
if (typeof options !== 'undefined') {
|
|
668
|
+
if (typeof options.retain !== 'undefined') pubOpts.retain = options.retain;
|
|
669
|
+
if (typeof options.qos !== 'undefined') pubOpts.qos = options.qos;
|
|
670
|
+
if (typeof options.headers !== 'undefined') pubOpts.properties = extend(true, {}, baseOpts.properties, options.properties);
|
|
671
|
+
}
|
|
672
|
+
let top = `${this.root}`;
|
|
673
|
+
if (!top.endsWith('/') && !topic.startsWith('/')) top += '/';
|
|
674
|
+
top += topic;
|
|
675
|
+
logger.silly(`Publishing ${top}-${msg}`);
|
|
676
|
+
// Now we should be able to send this to the broker.
|
|
677
|
+
bindings.client.publish(top, msg, pubOpts, (err) => {
|
|
678
|
+
if (err) {
|
|
679
|
+
logger.error(`Error publishing topic ${top}-${msg} : ${err}`);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
} catch (err) { logger.error(`Error publishing ${topic} to server ${bindings.cfg.name} from ${this.topic}`); }
|
|
683
|
+
}
|
|
684
|
+
};
|
|
685
|
+
|
|
686
|
+
this.fnProcessor(ctx, this, sys, state, value);
|
|
526
687
|
state.emitEquipmentChanges();
|
|
527
688
|
}
|
|
528
|
-
|
|
529
689
|
}
|
|
530
690
|
export interface IMQTTSubscription {
|
|
531
691
|
topic: string,
|
|
@@ -0,0 +1,87 @@
|
|
|
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 { webApp } from "../../web/Server";
|
|
19
|
+
import extend=require("extend");
|
|
20
|
+
import { logger } from "../../logger/Logger";
|
|
21
|
+
import { PoolSystem, sys } from "../../controller/Equipment";
|
|
22
|
+
import { State, state } from "../../controller/State";
|
|
23
|
+
import { InterfaceContext, InterfaceEvent, BaseInterfaceBindings } from "./baseInterface";
|
|
24
|
+
|
|
25
|
+
export class RuleInterfaceBindings extends BaseInterfaceBindings {
|
|
26
|
+
constructor(cfg) { super(cfg);}
|
|
27
|
+
declare events: RuleInterfaceEvent[];
|
|
28
|
+
public bindProcessor(evt: RuleInterfaceEvent) {
|
|
29
|
+
if (evt.processorBound) return;
|
|
30
|
+
if (typeof evt.fnProcessor === 'undefined') {
|
|
31
|
+
let fnBody = Array.isArray(evt.processor) ? evt.processor.join('\n') : evt.processor;
|
|
32
|
+
if (typeof fnBody !== 'undefined' && fnBody !== '') {
|
|
33
|
+
//let AsyncFunction = Object.getPrototypeOf(async => () => { }).constructor;
|
|
34
|
+
let AsyncFunction = Object.getPrototypeOf(async function () { }).constructor;
|
|
35
|
+
try {
|
|
36
|
+
evt.fnProcessor = new AsyncFunction('rule', 'options', 'vars', 'logger', 'webApp', 'sys', 'state', 'data', fnBody) as (rule: RuleInterfaceEvent, vars: any, sys: PoolSystem, state: State, data: any) => void;
|
|
37
|
+
} catch (err) { logger.error(`Error compiling rule event processor: ${err} -- ${fnBody}`); }
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
evt.processorBound = true;
|
|
41
|
+
}
|
|
42
|
+
public executeProcessor(eventName: string, evt: RuleInterfaceEvent, ...data: any) {
|
|
43
|
+
this.bindProcessor(evt);
|
|
44
|
+
let vars = this.bindVarTokens(evt, eventName, data);
|
|
45
|
+
let opts = extend(true, this.cfg.options, this.context.options, evt.options);
|
|
46
|
+
if (typeof evt.fnProcessor !== undefined) evt.fnProcessor(evt, opts, vars, logger, webApp, sys, state, data);
|
|
47
|
+
}
|
|
48
|
+
public bindEvent(evt: string, ...data: any) {
|
|
49
|
+
// Find the binding by first looking for the specific event name.
|
|
50
|
+
// If that doesn't exist then look for the "*" (all events).
|
|
51
|
+
if (typeof this.events !== 'undefined') {
|
|
52
|
+
let evts = this.events.filter(elem => elem.name === evt);
|
|
53
|
+
// If we don't have an explicitly defined event then see if there is a default.
|
|
54
|
+
if (evts.length === 0) {
|
|
55
|
+
let e = this.events.find(elem => elem.name === '*');
|
|
56
|
+
evts = e ? [e] : [];
|
|
57
|
+
}
|
|
58
|
+
if (evts.length > 0) {
|
|
59
|
+
let toks = {};
|
|
60
|
+
for (let i = 0; i < evts.length; i++) {
|
|
61
|
+
let e = evts[i];
|
|
62
|
+
if (typeof e.enabled !== 'undefined' && !e.enabled) continue;
|
|
63
|
+
// Figure out whether we need to check the filter.
|
|
64
|
+
if (typeof e.filter !== 'undefined') {
|
|
65
|
+
this.buildTokens(e.filter, evt, toks, e, data[0]);
|
|
66
|
+
if (eval(this.replaceTokens(e.filter, toks)) === false) continue;
|
|
67
|
+
}
|
|
68
|
+
// Look for the processor.
|
|
69
|
+
this.executeProcessor(evt, e, ...data);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
class RuleInterfaceEvent extends InterfaceEvent {
|
|
76
|
+
event: string;
|
|
77
|
+
description: string;
|
|
78
|
+
fnProcessor: (rule: RuleInterfaceEvent, options:any, vars: any, logger: any, webApp: any, sys: PoolSystem, state: State, data: any) => void;
|
|
79
|
+
processorBound: boolean = false;
|
|
80
|
+
}
|
|
81
|
+
export interface IRuleInterfaceEvent {
|
|
82
|
+
event: string,
|
|
83
|
+
description: string,
|
|
84
|
+
processor?: string
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|