UNPKG

14.3 kBJavaScriptView Raw
1/**
2 *
3 * ioBroker node-red Adapter
4 *
5 * (c) 2014 bluefox<bluefox@ccu.io>
6 *
7 * Apache 2.0 License
8 *
9 */
10/* jshint -W097 */// jshint strict:false
11/*jslint node: true */
12"use strict";
13
14var utils = require(__dirname + '/lib/utils'); // Get common adapter utils
15var adapter = utils.adapter({
16 name: 'node-red',
17 systemConfig: true, // get the system configuration as systemConfig parameter of adapter
18 unload: unloadRed
19});
20
21var fs = require('fs');
22var path = require('path');
23var spawn = require('child_process').spawn;
24var Notify = require('fs.notify');
25var attempts = {};
26var additional = [];
27
28var userdataDir = __dirname + '/userdata/';
29
30adapter.on('message', function (obj) {
31 if (obj) processMessage(obj);
32 processMessages();
33});
34
35adapter.on('ready', function () {
36 installLibraries(main);
37});
38
39function installNpm(npmLib, callback) {
40 var path = __dirname;
41 if (typeof npmLib == 'function') {
42 callback = npmLib;
43 npmLib = undefined;
44 }
45
46 var cmd = 'npm install ' + npmLib + ' --production --prefix "' + path + '"';
47 adapter.log.info(cmd + ' (System call)');
48 // Install node modules as system call
49
50 // System call used for update of js-controller itself,
51 // because during installation npm packet will be deleted too, but some files must be loaded even during the install process.
52 var exec = require('child_process').exec;
53 var child = exec(cmd);
54 child.stdout.on('data', function(buf) {
55 adapter.log.info(buf.toString('utf8'));
56 });
57 child.stderr.on('data', function(buf) {
58 adapter.log.error(buf.toString('utf8'));
59 });
60
61 child.on('exit', function (code, signal) {
62 if (code) {
63 adapter.log.error('Cannot install ' + npmLib + ': ' + code);
64 }
65 // command succeeded
66 if (callback) callback(npmLib);
67 });
68}
69
70function installLibraries(callback) {
71 var allInstalled = true;
72 if (adapter.common && adapter.common.npmLibs) {
73 for (var lib = 0; lib < adapter.common.npmLibs.length; lib++) {
74 if (adapter.common.npmLibs[lib] && adapter.common.npmLibs[lib].trim()) {
75 adapter.common.npmLibs[lib] = adapter.common.npmLibs[lib].trim();
76 if (!fs.existsSync(__dirname + '/node_modules/' + adapter.common.npmLibs[lib] + '/package.json')) {
77
78 if (!attempts[adapter.common.npmLibs[lib]]) {
79 attempts[adapter.common.npmLibs[lib]] = 1;
80 } else {
81 attempts[adapter.common.npmLibs[lib]]++;
82 }
83 if (attempts[adapter.common.npmLibs[lib]] > 3) {
84 adapter.log.error('Cannot install npm packet: ' + adapter.common.npmLibs[lib]);
85 continue;
86 }
87
88 installNpm(adapter.common.npmLibs[lib], function () {
89 installLibraries(callback);
90 });
91 allInstalled = false;
92 break;
93 } else {
94 if (additional.indexOf(adapter.common.npmLibs[lib]) == -1) additional.push(adapter.common.npmLibs[lib]);
95 }
96 }
97 }
98 }
99 if (allInstalled) callback();
100}
101
102// is called if a subscribed state changes
103//adapter.on('stateChange', function (id, state) {
104//});
105function unloadRed (callback) {
106 // Stop node-red
107 stopping = true;
108 if (redProcess) {
109 adapter.log.info("kill node-red task");
110 redProcess.kill();
111 redProcess = null;
112 }
113 if (notificationsCreds) notificationsCreds.close();
114 if (notificationsFlows) notificationsFlows.close();
115
116 if (callback) callback();
117}
118
119function processMessage(obj) {
120 if (!obj || !obj.command) return;
121 switch (obj.command) {
122 case 'update': {
123 writeStateList(function(error) {
124 if (obj.callback) adapter.sendTo(obj.from, obj.command, error, obj.callback);
125 });
126 }
127 case 'stopInstance': {
128 unloadRed();
129 }
130 }
131}
132
133function processMessages() {
134 adapter.getMessage(function (err, obj) {
135 if (obj) {
136 processMessage(obj.command, obj.message);
137 processMessages();
138 }
139 });
140}
141
142function getNodeRedPath() {
143 var nodeRed = __dirname + '/node_modules/node-red';
144 if (!fs.existsSync(nodeRed)) {
145 nodeRed = path.normalize(__dirname + '/../node-red');
146 if (!fs.existsSync(nodeRed)) {
147 adapter.log.error('Cannot find node-red packet!');
148 throw new Error('Cannot find node-red packet!');
149 }
150 }
151
152 return nodeRed;
153}
154
155var redProcess;
156var stopping;
157var notificationsFlows;
158var notificationsCreds;
159var saveTimer;
160var nodePath = getNodeRedPath();
161
162function startNodeRed() {
163 var args = ['--max-old-space-size=128', nodePath + '/red.js', '-v', '--settings', userdataDir + 'settings.js'];
164 adapter.log.info('Starting node-red: ' + args.join(' '));
165
166 redProcess = spawn('node', args);
167 redProcess.stdout.on('data', function (data) {
168 if (!data) return;
169 data = data.toString();
170 if (data[data.length - 2] == '\r' && data[data.length - 1] == '\n') data = data.substring(0, data.length - 2);
171 if (data[data.length - 2] == '\n' && data[data.length - 1] == '\r') data = data.substring(0, data.length - 2);
172 if (data[data.length - 1] == '\r') data = data.substring(0, data.length - 1);
173
174 if (data.indexOf('[err') != -1) {
175 adapter.log.error(data);
176 } else if (data.indexOf('[warn]') != -1) {
177 adapter.log.warn(data);
178 } else {
179 adapter.log.debug(data);
180 }
181 });
182 redProcess.stderr.on('data', function (data) {
183 if (!data) return;
184 if (data[0]) {
185 var text = "";
186 for (var i = 0; i < data.length; i++) {
187 text += String.fromCharCode(data[i]);
188 }
189 data = text;
190 }
191 if (data.indexOf && data.indexOf('[warn]') == -1) {
192 adapter.log.warn(data);
193 } else {
194 adapter.log.error(JSON.stringify(data));
195 }
196 });
197
198 redProcess.on('exit', function (exitCode) {
199 adapter.log.info('node-red exited with ' + exitCode);
200 redProcess = null;
201 if (!stopping) {
202 setTimeout(startNodeRed, 5000);
203 }
204 });
205}
206
207function setOption(line, option, value) {
208 var toFind = "'%%" + option + "%%'";
209 var pos = line.indexOf(toFind);
210 if (pos != -1) {
211 return line.substring(0, pos) + ((value !== undefined) ? value : (adapter.config[option] === null || adapter.config[option] === undefined) ? '' : adapter.config[option]) + line.substring(pos + toFind.length);
212 }
213 return line;
214}
215
216function writeSettings() {
217 var config = JSON.stringify(adapter.systemConfig);
218 var text = fs.readFileSync(__dirname + '/settings.js').toString();
219 var lines = text.split('\n');
220 var npms = '\r\n';
221 var dir = __dirname.replace(/\\/g, '/') + '/node_modules/';
222 var nodesDir = '"' + __dirname.replace(/\\/g, '/') + '/nodes/"';
223 for (var a = 0; a < additional.length; a++) {
224 if (additional[a].match(/^node-red-/)) continue;
225 npms += ' "' + additional[a] + '": require("' + dir + additional[a] + '")';
226 if (a != additional.length - 1) {
227 npms += ', \r\n';
228 }
229 }
230
231 // update from 1.0.1 (new convert-option)
232 if (adapter.config.valueConvert === null ||
233 adapter.config.valueConvert === undefined ||
234 adapter.config.valueConvert === '' ||
235 adapter.config.valueConvert === 'true' ||
236 adapter.config.valueConvert === '1' ||
237 adapter.config.valueConvert === 1) {
238 adapter.config.valueConvert = true;
239 }
240 if (adapter.config.valueConvert === 0 ||
241 adapter.config.valueConvert === '0' ||
242 adapter.config.valueConvert === 'false') {
243 adapter.config.valueConvert = false;
244 }
245 for (var i = 0; i < lines.length; i++) {
246 lines[i] = setOption(lines[i], 'port');
247 lines[i] = setOption(lines[i], 'instance', adapter.instance);
248 lines[i] = setOption(lines[i], 'config', config);
249 lines[i] = setOption(lines[i], 'functionGlobalContext', npms);
250 lines[i] = setOption(lines[i], 'nodesdir', nodesDir);
251 lines[i] = setOption(lines[i], 'httpRoot');
252 lines[i] = setOption(lines[i], 'valueConvert');
253 }
254 fs.writeFileSync(userdataDir + 'settings.js', lines.join('\n'));
255}
256
257function writeStateList(callback) {
258 adapter.getForeignObjects('*', function (err, obj) {
259 // remove native information
260 for (var i in obj) {
261 if (obj[i].native) delete obj[i].native;
262 }
263
264 fs.writeFileSync(nodePath + '/public/iobroker.json', JSON.stringify(obj));
265 if (callback) callback(err);
266 });
267/* adapter.getForeignObjects('*', 'state', 'rooms', function (err, obj) {
268 var states = {};
269 for (var state in obj) {
270 states[state] = {name: obj[state].common.name, role: obj[state].common.role, rooms: obj[state].enums};
271 }
272 fs.writeFileSync(__dirname + '/node_modules/node-red/public/iobroker.json', JSON.stringify(states));
273 if (callback) callback(err);
274 });*/
275}
276
277function saveObjects() {
278 if (saveTimer) {
279 clearTimeout(saveTimer);
280 saveTimer = null;
281 }
282 var cred = undefined;
283 var flows = undefined;
284
285 try {
286 if (fs.existsSync(userdataDir + 'flows_cred.json')) {
287 cred = JSON.parse(fs.readFileSync(userdataDir + 'flows_cred.json'));
288 }
289 } catch(e) {
290 adapter.log.error('Cannot save ' + userdataDir + 'flows_cred.json');
291 }
292 try {
293 if (fs.existsSync(userdataDir + 'flows.json')) {
294 flows = JSON.parse(fs.readFileSync(userdataDir + 'flows.json'));
295 }
296 } catch(e) {
297 adapter.log.error('Cannot save ' + userdataDir + 'flows.json');
298 }
299 //upload it to config
300 adapter.setObject('flows', {
301 common: {
302 name: 'Flows for node-red'
303 },
304 native: {
305 cred: cred,
306 flows: flows
307 },
308 type: 'config'
309 }, function () {
310 adapter.log.info('Save ' + userdataDir + 'flows.json');
311 });
312}
313
314function syncPublic(path) {
315 if (!path) path = '/public';
316
317 var dir = fs.readdirSync(__dirname + path);
318
319 if (!fs.existsSync(nodePath + path)) {
320 fs.mkdirSync(nodePath + path);
321 }
322
323 for (var i = 0; i < dir.length; i++) {
324 var stat = fs.statSync(__dirname + path + '/' + dir[i]);
325 if (stat.isDirectory()) {
326 syncPublic(path + '/' + dir[i]);
327 } else {
328 if (!fs.existsSync(nodePath + path + '/' + dir[i])) {
329 fs.createReadStream(__dirname + path + '/' + dir[i]).pipe(fs.createWriteStream(nodePath + path + '/' + dir[i]));
330 }
331 }
332 }
333}
334
335function installNotifierFlows(isFirst) {
336 if (!notificationsFlows) {
337 if (fs.existsSync(userdataDir + 'flows.json')) {
338 if (!isFirst) saveObjects();
339 // monitor project file
340 notificationsFlows = new Notify([userdataDir + 'flows.json']);
341 notificationsFlows.on('change', function () {
342 if (saveTimer) clearTimeout(saveTimer);
343 saveTimer = setTimeout(saveObjects, 500);
344 });
345 } else {
346 // Try to install notifier every 10 seconds till the file will be created
347 setTimeout(function () {
348 installNotifierFlows();
349 }, 10000);
350 }
351 }
352}
353
354function installNotifierCreds(isFirst) {
355 if (!notificationsCreds) {
356 if (fs.existsSync(userdataDir + 'flows_cred.json')) {
357 if (!isFirst) saveObjects();
358 // monitor project file
359 notificationsCreds = new Notify([userdataDir + 'flows_cred.json']);
360 notificationsCreds.on('change', function () {
361 if (saveTimer) clearTimeout(saveTimer);
362 saveTimer = setTimeout(saveObjects, 500);
363 });
364 } else {
365 // Try to install notifier every 10 seconds till the file will be created
366 setTimeout(function () {
367 installNotifierCreds();
368 }, 10000);
369 }
370 }
371}
372
373function main() {
374 // Find userdata directory
375
376 // normally /opt/iobroker/node_modules/iobroker.js-controller
377 // but can be /example/ioBroker.js-controller
378 var controllerDir = utils.controllerDir;
379 var parts = controllerDir.split('/');
380 if (parts.length > 1 && parts[parts.length - 2] == 'node_modules') {
381 parts.splice(parts.length - 2, 2);
382 userdataDir = parts.join('/');
383 userdataDir += '/iobroker-data/node-red/';
384 }
385
386 // create userdata directory
387 if (!fs.existsSync(userdataDir)) {
388 fs.mkdirSync(userdataDir);
389 }
390
391 syncPublic();
392
393 // Read configuration
394 adapter.getObject('flows', function (err, obj) {
395 if (obj && obj.native && obj.native.cred) {
396 var c = JSON.stringify(obj.native.cred);
397 // If really not empty
398 if (c != '{}' && c != '[]') {
399 fs.writeFileSync(userdataDir + 'flows_cred.json', JSON.stringify(obj.native.cred));
400 }
401 }
402 if (obj && obj.native && obj.native.flows) {
403 var f = JSON.stringify(obj.native.flows);
404 // If really not empty
405 if (f != '{}' && f != '[]') {
406 fs.writeFileSync(userdataDir + 'flows.json', JSON.stringify(obj.native.flows));
407 }
408 }
409
410 installNotifierFlows(true);
411 installNotifierCreds(true);
412
413 // Create settings for node-red
414 writeSettings();
415 writeStateList(function () {
416 startNodeRed();
417 });
418 });
419}
420