-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathtulip_machine_attribute.js
289 lines (256 loc) · 9 KB
/
tulip_machine_attribute.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
module.exports = function (RED) {
'use strict';
const {
doHttpRequest,
getHttpAgent,
getHttpLib,
getMachineAttributeEndpoint,
} = require('./utils');
/**
* Validates a list of attributes to write to the Machine API. Is valid if it is a list of
* { attributeId: string, machineId: string, value: any }
* @param {Object[]} payload - list of { machineId, attributeId, value } to validate
* @returns {Object} result - the validation result
* @returns {boolean} result.isValid - whether the input list is valid
* @returns {string|null} result.errorMessage - error message if not valid, otherwise null
*/
const validateBatchedPayload = function (payload) {
try {
// Wrap in try-catch in case there is an unexpected validation error
if (!Array.isArray(payload)) {
return {
isValid: false,
errorMessage: `invalid payload: expected array, got: ${typeof payload}`,
};
} else {
for (const attribute of payload) {
const attributeId = attribute.attributeId;
const machineId = attribute.machineId;
const value = attribute.value;
// validate attributeId exists and is string
if (typeof attributeId !== 'string') {
return {
isValid: false,
errorMessage:
`attributeId must be a string, got: ` +
`<${attributeId}> of type <${typeof attributeId}>`,
};
}
// validate machineId exists and is string
if (typeof machineId !== 'string') {
return {
isValid: false,
errorMessage: `machineId must be a string, got: ${machineId}`,
};
}
// validate value is not null|undefined
if (value === undefined || value === null) {
return {
isValid: false,
errorMessage: `value cannot be null or undefined, got: ${value}`,
};
}
}
}
} catch (err) {
return {
isValid: false,
errorMessage: `invalid payload: ${JSON.stringify(err, null, 2)}`,
};
}
return {
isValid: true,
errorMessage: null,
};
};
// Tulip API node
function MachineAttrNode(config) {
RED.nodes.createNode(this, config);
const apiAuthNode = RED.nodes.getNode(config.apiAuth);
// Set node properties
this.name = config.name;
this.deviceInfo = JSON.parse(config.deviceInfo);
this.payloadSource = config.payloadSource;
this.payloadType = config.payloadType;
// Legacy nodes only allowed writing one attribute, so if property is missing default to true
this.singleAttribute = 'singleAttribute' in config ? config.singleAttribute : true;
// Legacy nodes did not allow retaining msg props, so if property is missing default to false
this.retainMsgProps = 'retainMsgProps' in config ? config.retainMsgProps : false;
this.apiCredentials = apiAuthNode.credentials;
const factoryUrl = getFactoryUrl(
apiAuthNode.protocol,
apiAuthNode.hostname,
apiAuthNode.port,
);
this.endpoint = getMachineAttributeEndpoint(factoryUrl);
this.httpLib = getHttpLib(apiAuthNode.protocol);
const node = this;
// Use http or https depending on the factory protocol
const agent = getHttpAgent(node.httpLib, config.keepAlive, config.keepAliveMsecs);
// Configure HTTP POST request options w/auth
const defaultRequestOpts = {
method: 'POST',
auth: `${node.apiCredentials.apiKey}:${node.apiCredentials.apiSecret}`,
agent,
};
// Handle node inputs
this.on('input', function (msg, send, done) {
try {
const headers = getHeaders(msg, node);
// Decide whether to pass the input msg params to the output msg
const sendMsg = node.retainMsgProps
? (newMsg) => {
send({
...msg,
...newMsg,
});
}
: send;
if (node.singleAttribute) {
// Get the payload from user-defined input
const payload = getPayloadFromSource(msg, node);
// Use either config or override with msg value
const machineId = getDeviceInfo(msg, node.deviceInfo, 'machineId');
const attributeId = getDeviceInfo(msg, node.deviceInfo, 'attributeId');
// Everything is ok, send the payload
sendPayloadForAttribute(payload, headers, machineId, attributeId, sendMsg, done);
} else {
const { isValid, errorMessage } = validateBatchedPayload(msg.payload);
if (!isValid) {
done(new Error(errorMessage));
return;
}
sendPayloadAsAttributes(msg.payload, headers, sendMsg, done);
}
} catch (e) {
done(e);
}
});
// Sends payload to Tulip Machine API for attribute given by { machineId, attributeId }
function sendPayloadForAttribute(payload, headers, machineId, attributeId, send, done) {
const bodyObj = {
attributes: [
{
machineId,
attributeId,
value: payload,
},
],
};
const body = JSON.stringify(bodyObj);
const options = defaultRequestOpts;
options.headers = headers;
// Create, send, handle, and close HTTP request
doHttpRequest(node.httpLib, node.endpoint, options, body, node.error.bind(node), send, done);
}
// Sends payload to Tulip Machine API as the request `attributes` property
function sendPayloadAsAttributes(payload, headers, send, done) {
const bodyObj = {
attributes: payload,
};
const body = JSON.stringify(bodyObj);
const options = defaultRequestOpts;
options.headers = headers;
// Create, send, handle, and close HTTP request
doHttpRequest(node.httpLib, node.endpoint, options, body, node.error.bind(node), send, done);
}
function getFactoryUrl(protocol, hostname, port) {
// start with valid protocol & host
const baseUrl = `${protocol}://${hostname}`;
const url = new URL(baseUrl);
// build the url
url.port = port;
return url;
}
/**
* Gets the headers for the Tulip API request. Uses the user-defined msg.headers,
* but overrides 'Content-Type' to 'application/json'.
*/
function getHeaders(msg) {
// Default header for API request
const headers = msg.headers || {};
// Content-Type set by this node; overrides user value
if (headers['Content-Type']) {
node.warn(
`Overriding header 'Content-Type'='${headers['Content-Type']}'; must be 'application/json'`
);
}
headers['Content-Type'] = 'application/json';
return headers;
}
/**
* If the machineId or attributeId is present in the msg, returns the value.
* Otherwise, returns the value from the statically configured Device Info field.
*/
function getDeviceInfo(msg, deviceInfo, param) {
const paramVal = msg[param];
const configVal = deviceInfo[param];
if (paramVal == undefined) {
// Use config value
return configVal;
} else {
// Use msg value
if (typeof paramVal != 'string') {
// msg parameter is invalid
const paramType = typeof paramVal;
throw new Error(
`msg.${param} must be string,` +
`${paramVal} is of type ${paramType}`
);
} else {
// msg has valid parameter
return paramVal;
}
}
}
}
/**
* Gets the payload from the configured payload source.
* Throws an error if the payload is undefined.
*/
function getPayloadFromSource(msg, node) {
const payload = getTypedData(
msg,
node,
node.payloadType,
node.payloadSource
);
if (payload == undefined) {
throw new Error('payload not defined');
}
return payload;
}
/*
* Gets the data from a TypedInput configuration field. If JSONata, evaluates the expression,
* otherwise evaluates the node property. Throws an error if the JSONata is invalid or if
* the specified property does not exist.
*/
function getTypedData(msg, node, dataType, dataVal) {
if (dataType === 'jsonata') {
// Evaluate JSONata expression
try {
const expr = RED.util.prepareJSONataExpression(dataVal, node);
return RED.util.evaluateJSONataExpression(expr, msg);
} catch (err) {
// Add detailed error message
node.error(`Error evaluating JSONata expression: ${dataVal}`);
throw err;
}
} else {
// Evaluate node property
try {
return RED.util.evaluateNodeProperty(dataVal, dataType, node, msg);
} catch (err) {
// Add detailed error message
node.error(
new Error(
`Error evaluating node property ${dataVal} of type ${dataType}`
)
);
throw err;
}
}
}
// Register the node type
RED.nodes.registerType('tulip-machine-attribute', MachineAttrNode);
};