UNPKG

12.9 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
15
16var adapter = utils.adapter({
17 name: 'node-red',
18 systemConfig: true, // get the system configuration as systemConfig parameter of adapter
19 unload: unloadRed
20});
21
22var fs = require('fs');
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 fs = fs || require('fs');
77
78 if (!fs.existsSync(__dirname + '/node_modules/' + adapter.common.npmLibs[lib] + '/package.json')) {
79
80 if (!attempts[adapter.common.npmLibs[lib]]) {
81 attempts[adapter.common.npmLibs[lib]] = 1;
82 } else {
83 attempts[adapter.common.npmLibs[lib]]++;
84 }
85 if (attempts[adapter.common.npmLibs[lib]] > 3) {
86 adapter.log.error('Cannot install npm packet: ' + adapter.common.npmLibs[lib]);
87 continue;
88 }
89
90 installNpm(adapter.common.npmLibs[lib], function () {
91 installLibraries(callback);
92 });
93 allInstalled = false;
94 } else {
95 if (additional.indexOf(adapter.common.npmLibs[lib]) == -1) additional.push(adapter.common.npmLibs[lib]);
96 }
97 }
98 }
99 }
100 if (allInstalled) callback();
101}
102
103// is called if a subscribed state changes
104//adapter.on('stateChange', function (id, state) {
105//});
106function unloadRed (callback) {
107 // Stop node-red
108 stopping = true;
109 if (redProcess) {
110 adapter.log.info("kill node-red task");
111 redProcess.kill();
112 redProcess = null;
113 }
114 if (notificationsCreds) notificationsCreds.close();
115 if (notificationsFlows) notificationsFlows.close();
116
117 if (callback) callback();
118}
119
120function processMessage(obj) {
121 if (!obj || !obj.command) return;
122 switch (obj.command) {
123 case 'update': {
124 writeStateList(function(error) {
125 if (obj.callback) adapter.sendTo(obj.from, obj.command, error, obj.callback);
126 });
127 }
128 case 'stopInstance': {
129 unloadRed();
130 }
131 }
132}
133
134function processMessages() {
135 adapter.getMessage(function (err, obj) {
136 if (obj) {
137 processMessage(obj.command, obj.message);
138 processMessages();
139 }
140 });
141}
142var redProcess;
143var stopping;
144var notificationsFlows;
145var notificationsCreds;
146var saveTimer;
147
148function startNodeRed() {
149 var args = [__dirname + '/node_modules/node-red/red.js', '-v', '--settings', userdataDir + 'settings.js'];
150 adapter.log.info('Starting node-red: ' + args.join(' '));
151
152 redProcess = spawn('node', args);
153 redProcess.stdout.on('data', function (data) {
154 if (!data) return;
155 data = data.toString();
156 if (data[data.length - 2] == '\r' && data[data.length - 1] == '\n') data = data.substring(0, data.length - 2);
157 if (data[data.length - 2] == '\n' && data[data.length - 1] == '\r') data = data.substring(0, data.length - 2);
158 if (data[data.length - 1] == '\r') data = data.substring(0, data.length - 1);
159
160 if (data.indexOf('[err') != -1) {
161 adapter.log.error(data);
162 } else if (data.indexOf('[warn]') != -1) {
163 adapter.log.warn(data);
164 } else {
165 adapter.log.debug(data);
166 }
167 });
168 redProcess.stderr.on('data', function (data) {
169 if (!data) return;
170 if (data[0]) {
171 var text = "";
172 for (var i = 0; i < data.length; i++) {
173 text += String.fromCharCode(data[i]);
174 }
175 data = text;
176 }
177 if (data.indexOf && data.indexOf('[warn]') == -1) {
178 adapter.log.warn(data);
179 } else {
180 adapter.log.error(JSON.stringify(data));
181 }
182 });
183
184 redProcess.on('exit', function (exitCode) {
185 adapter.log.info('node-red exited with ' + exitCode);
186 redProcess = null;
187 if (!stopping) {
188 setTimeout(startNodeRed, 5000);
189 }
190 });
191}
192
193function setOption(line, option, value) {
194 var toFind = "'%%" + option + "%%'";
195 var pos = line.indexOf(toFind);
196 if (pos != -1) {
197 return line.substring(0, pos) + ((value !== undefined) ? value: adapter.config[option]) + line.substring(pos + toFind.length);
198 }
199 return line;
200}
201
202function writeSettings() {
203 var config = JSON.stringify(adapter.systemConfig);
204 var text = fs.readFileSync(__dirname + '/settings.js').toString();
205 var lines = text.split('\n');
206 var npms = '\r\n';
207 var dir = __dirname.replace(/\\/g, '/') + '/node_modules/';
208 for (var a = 0; a < additional.length; a++) {
209 npms += ' "' + additional[a] + '": require("' + dir + additional[a] + '")';
210 if (a != additional.length - 1) {
211 npms += ', \r\n';
212 }
213 }
214
215
216 for (var i = 0; i < lines.length; i++) {
217 lines[i] = setOption(lines[i], 'port');
218 lines[i] = setOption(lines[i], 'instance', adapter.instance);
219 lines[i] = setOption(lines[i], 'config', config);
220 lines[i] = setOption(lines[i], 'functionGlobalContext', npms);
221 }
222 fs.writeFileSync(userdataDir + 'settings.js', lines.join('\n'));
223}
224
225function writeStateList(callback) {
226 adapter.getForeignObjects('*', function (err, obj) {
227 // remove native information
228 for (var i in obj) {
229 if (obj[i].native) delete obj[i].native;
230 }
231
232 fs.writeFileSync(__dirname + '/node_modules/node-red/public/iobroker.json', JSON.stringify(obj));
233 if (callback) callback(err);
234 });
235/* adapter.getForeignObjects('*', 'state', 'rooms', function (err, obj) {
236 var states = {};
237 for (var state in obj) {
238 states[state] = {name: obj[state].common.name, role: obj[state].common.role, rooms: obj[state].enums};
239 }
240 fs.writeFileSync(__dirname + '/node_modules/node-red/public/iobroker.json', JSON.stringify(states));
241 if (callback) callback(err);
242 });*/
243}
244
245function saveObjects() {
246 if (saveTimer) {
247 clearTimeout(saveTimer);
248 saveTimer = null;
249 }
250 var cred = undefined;
251 var flows = undefined;
252
253 try {
254 if (fs.existsSync(userdataDir + 'flows_cred.json')) {
255 cred = JSON.parse(fs.readFileSync(userdataDir + 'flows_cred.json'));
256 }
257 } catch(e) {
258 adapter.log.error('Cannot save ' + userdataDir + 'flows_cred.json');
259 }
260 try {
261 if (fs.existsSync(userdataDir + 'flows.json')) {
262 flows = JSON.parse(fs.readFileSync(userdataDir + 'flows.json'));
263 }
264 } catch(e) {
265 adapter.log.error('Cannot save ' + userdataDir + 'flows.json');
266 }
267 //upload it to config
268 adapter.setObject('flows', {
269 common: {
270 name: 'Flows for node-red'
271 },
272 native: {
273 cred: cred,
274 flows: flows
275 },
276 type: 'config'
277 }, function () {
278 adapter.log.info('Save ' + userdataDir + 'flows.json');
279 });
280}
281
282function syncPublic(path) {
283 if (!path) path = '/public';
284
285 var dir = fs.readdirSync(__dirname + path);
286
287 if (!fs.existsSync(__dirname + '/node_modules/node-red' + path)) {
288 fs.mkdirSync(__dirname + '/node_modules/node-red' + path);
289 }
290
291 for (var i = 0; i < dir.length; i++) {
292 var stat = fs.statSync(__dirname + path + '/' + dir[i]);
293 if (stat.isDirectory()) {
294 syncPublic(path + '/' + dir[i]);
295 } else {
296 if (!fs.existsSync(__dirname + '/node_modules/node-red' + path + '/' + dir[i])) {
297 fs.createReadStream(__dirname + path + '/' + dir[i]).pipe(fs.createWriteStream(__dirname + '/node_modules/node-red' + path + '/' + dir[i]));
298 }
299 }
300 }
301}
302
303function installNotifierFlows(isFirst) {
304 if (!notificationsFlows) {
305 if (fs.existsSync(userdataDir + 'flows.json')) {
306 if (!isFirst) saveObjects();
307 // monitor project file
308 notificationsFlows = new Notify([userdataDir + 'flows.json']);
309 notificationsFlows.on('change', function () {
310 if (saveTimer) clearTimeout(saveTimer);
311 saveTimer = setTimeout(saveObjects, 500);
312 });
313 } else {
314 // Try to install notifier every 10 seconds till the file will be created
315 setTimeout(function () {
316 installNotifierFlows();
317 }, 10000);
318 }
319 }
320}
321
322function installNotifierCreds(isFirst) {
323 if (!notificationsCreds) {
324 if (fs.existsSync(userdataDir + 'flows_cred.json')) {
325 if (!isFirst) saveObjects();
326 // monitor project file
327 notificationsCreds = new Notify([userdataDir + 'flows_cred.json']);
328 notificationsCreds.on('change', function () {
329 if (saveTimer) clearTimeout(saveTimer);
330 saveTimer = setTimeout(saveObjects, 500);
331 });
332 } else {
333 // Try to install notifier every 10 seconds till the file will be created
334 setTimeout(function () {
335 installNotifierCreds();
336 }, 10000);
337 }
338 }
339}
340
341function main() {
342 // Find userdata directory
343
344 // normally /opt/iobroker/node_modules/iobroker.js-controller
345 // but can be /example/ioBroker.js-controller
346 var controllerDir = utils.controllerDir;
347 var parts = controllerDir.split('/');
348 if (parts.length > 1 && parts[parts.length - 2] == 'node_modules') {
349 parts.splice(parts.length - 2, 2);
350 userdataDir = parts.join('/');
351 userdataDir += '/iobroker-data/node-red/';
352 }
353
354 // create userdata directory
355 if (!fs.existsSync(userdataDir)) {
356 fs.mkdirSync(userdataDir);
357 }
358
359 syncPublic();
360
361 // Read configuration
362 adapter.getObject('flows', function (err, obj) {
363 if (obj && obj.native && obj.native.cred) {
364 var c = JSON.stringify(obj.native.cred);
365 // If really not empty
366 if (c != '{}' && c != '[]') {
367 fs.writeFileSync(userdataDir + 'flows_cred.json', JSON.stringify(obj.native.cred));
368 }
369 }
370 if (obj && obj.native && obj.native.flows) {
371 var f = JSON.stringify(obj.native.flows);
372 // If really not empty
373 if (f != '{}' && f != '[]') {
374 fs.writeFileSync(userdataDir + 'flows.json', JSON.stringify(obj.native.flows));
375 }
376 }
377
378 installNotifierFlows(true);
379 installNotifierCreds(true);
380
381 // Create settings for node-red
382 writeSettings();
383 writeStateList(function () {
384 startNodeRed();
385 });
386 });
387}
388