UNPKG

30.4 kBJavaScriptView Raw
1/**
2 * Copyright 2014-2020 bluefox <dogafox@gmail.com>.
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 **/
16
17module.exports = function(RED) {
18 'use strict';
19 // patch event emitter
20 require('events').EventEmitter.prototype._maxListeners = 1000;
21
22 const utils = require('@iobroker/adapter-core');
23 const settings = require(process.env.NODE_RED_HOME + '/lib/red').settings;
24
25 const instance = settings.get('iobrokerInstance') || 0;
26 let config = settings.get('iobrokerConfig');
27 const valueConvert = settings.get('valueConvert');
28 const allowCreationOfForeignObjects = settings.get('allowCreationOfForeignObjects');
29 if (typeof config === 'string') {
30 config = JSON.parse(config);
31 }
32 let adapter;
33 const existingNodes = [];
34
35 try {
36 adapter = utils.Adapter({name: 'node-red', instance, config});
37 } catch(e) {
38 console.log(e);
39 }
40 const nodeSets = [];
41 const checkStates = [];
42 const isValidID = new RegExp('^[_A-Za-z0-9ÄÖÜäöüа-яА-Я][-_A-Za-z0-9ÄÖÜäöüа-яА-Я]*\\.\\d+\\.');
43 let ready = false;
44 const log = adapter && adapter.log && adapter.log.warn ? adapter.log.warn : console.log;
45
46 adapter.on('ready', () => {
47 function checkQueuedStates(callback) {
48 if (!checkStates.length) {
49 return callback && callback();
50 }
51 const check = checkStates.shift();
52 checkState(check.node, check.id, check.common, check.val, () => {
53 check.callback && check.callback();
54 setImmediate(() => checkQueuedStates(callback));
55 });
56 }
57
58 ready = true;
59 checkQueuedStates(() => {
60 existingNodes.forEach(node => {
61 if (node instanceof IOBrokerInNode) {
62 adapter.on('stateChange', node.stateChange);
63 }
64 node.subscribePattern && adapter.subscribeForeignStates(node.subscribePattern);
65 node.status({fill: 'green', shape: 'dot', text: 'connected'});
66 });
67
68 let count = 0;
69
70 while (nodeSets.length) {
71 const nodeSetData = nodeSets.pop();
72 nodeSetData.node.emit('input', nodeSetData.msg);
73 count++;
74 }
75 count && log(count + ' queued state values set in ioBroker');
76 });
77 });
78
79 function isForeignState(id) {
80 return isValidID.test(id) && !id.startsWith(adapter.namespace + '.');
81 }
82
83 // name is like system.state, pattern is like "*.state" or "*" or "*system*"
84 function getRegex(pattern) {
85 if (!pattern || pattern === '*') {
86 return null;
87 }
88 if (!pattern.includes('*')) {
89 return null;
90 }
91 if (pattern[pattern.length - 1] !== '*') {
92 pattern = pattern + '$';
93 }
94 if (pattern[0] !== '*') {
95 pattern = '^' + pattern;
96 }
97 pattern = pattern.replace(/\*/g, '[a-zA-Z0-9.\s]');
98 pattern = pattern.replace(/\./g, '\\.');
99 return new RegExp(pattern);
100 }
101
102 // check if object exists and sets its value if provided
103 function checkState(node, id, common, val, callback) {
104 if (node.idChecked) {
105 return callback && callback();
106 }
107 if (!ready) {
108 checkStates.push({node, id, common, val, callback});
109 return;
110 }
111 if (node.topic) {
112 node.idChecked = true;
113 }
114
115 if (val === null || val === '__create__') {
116 val = undefined;
117 }
118
119 adapter.getObject(id, (err, obj) => {
120 if (!obj) {
121 adapter.getForeignObject(id, (err, obj) => {
122 // If not exists
123 if (!obj) {
124 if (common) {
125 log('State "' + id + '" was created in the ioBroker as ' + adapter._fixId(id));
126 // Create object
127 const data = {
128 common,
129 native: {},
130 type: 'state'
131 };
132
133 if (isForeignState(id)) {
134 if (allowCreationOfForeignObjects) {
135 adapter.setForeignObject(id, data, _ => adapter.setForeignState(id, val, () => callback && callback(true)));
136 } else {
137 adapter.log.info('Creation of foreign objects is not enabled. You can enable it in the configuration');
138 callback && callback(false);
139 }
140 } else {
141 adapter.setObject(id, data, _ => adapter.setState(id, val, () => callback && callback(true)));
142 }
143 } else {
144 adapter.log.info('Automatic objects creation is not enabled. You can enable it in the node configuration');
145 callback && callback(false);
146 }
147 } else {
148 node._id = obj._id;
149 if (val !== undefined) {
150 adapter.setForeignState(obj._id, val, () => callback && callback(true));
151 } else {
152 callback && callback(true);
153 }
154 }
155 });
156 } else {
157 if (val !== undefined) {
158 adapter.setForeignState(obj._id, val, () => callback && callback(true));
159 } else {
160 callback && callback(true);
161 }
162 }
163 });
164 }
165
166 function assembleCommon(node, msg, id) {
167 msg = msg || {};
168 const common = {
169 read: true,
170 write: node.objectPreDefinedReadonly,
171 desc: 'Created by Node-Red',
172 role: node.objectPreDefinedRole || msg.stateRole || 'state',
173 name: node.objectPreDefinedName || msg.stateName || id,
174 type: node.objectPreDefinedType || msg.stateType || typeof msg.payload || 'string'
175 };
176 if (msg.stateReadonly !== undefined) {
177 common.write = !(msg.stateReadonly === false || msg.stateReadonly === 'false');
178 }
179
180 if (node.objectPreDefinedUnit || msg.stateUnit) {
181 common.unit = node.objectPreDefinedUnit || msg.stateUnit;
182 }
183 if (node.objectPreDefinedMax || node.objectPreDefinedMax === 0 || msg.stateMax || msg.stateMax === 0) {
184 if (node.objectPreDefinedMax || node.objectPreDefinedMax === 0) {
185 common.max = node.objectPreDefinedMax;
186 } else {
187 common.max = msg.stateMax;
188 }
189 }
190 if (node.objectPreDefinedMin || node.objectPreDefinedMin === 0 || msg.stateMin || msg.stateMin === 0) {
191 if (node.objectPreDefinedMin || node.objectPreDefinedMin === 0) {
192 common.max = node.objectPreDefinedMin;
193 } else {
194 common.max = msg.stateMin;
195 }
196 }
197 return common;
198 }
199
200 function defineCommon(node, n) {
201 node.autoCreate = n.autoCreate === 'true' || n.autoCreate === true;
202
203 if (node.autoCreate) {
204 node.objectPreDefinedRole = n.role;
205 node.objectPreDefinedType = n.payloadType;
206 node.objectPreDefinedName = n.stateName || '';
207 node.objectPreDefinedReadonly = !(n.readonly === 'false' || n.readonly === false);
208 node.objectPreDefinedUnit = n.stateUnit;
209 node.objectPreDefinedMin = n.stateMin;
210 node.objectPreDefinedMax = n.stateMax;
211 }
212 }
213
214 function onClose(node) {
215 const pos = existingNodes.indexOf(node);
216 if (pos !== -1) {
217 existingNodes.splice(pos, 1);
218 }
219 node.subscribePattern && adapter.unsubscribeForeignStates(node.subscribePattern);
220 }
221
222 function IOBrokerInNode(n) {
223 const node = this;
224 RED.nodes.createNode(node, n);
225 node.topic = (n.topic || '*').replace(/\//g, '.');
226
227 defineCommon(node, n);
228
229 // If no adapter prefix, add own adapter prefix
230 if (node.topic && !isValidID.test(node.topic) && !node.topic.startsWith(adapter.namespace)) {
231 node.topic = adapter.namespace + '.' + node.topic;
232 }
233 node.subscribePattern = node.topic;
234
235 node.regexTopic = getRegex(node.topic);
236 node.payloadType = n.payloadType;
237 node.onlyack = n.onlyack === true || n.onlyack === 'true' || false;
238 node.func = n.func || 'all';
239 node.gap = n.gap || '0';
240 node.gap = n.gap || '0';
241 node.fireOnStart = n.fireOnStart === true || n.fireOnStart === 'true' || false;
242
243 if (node.gap.substr(-1) === '%') {
244 node.pc = true;
245 node.gap = parseFloat(node.gap);
246 }
247 node.g = node.gap;
248
249 node.previous = {};
250
251 // Create ID if not exits
252 if (node.topic && !node.topic.includes('*')) {
253 checkState(node, node.topic);
254 }
255
256 if (ready) {
257 node.status({fill: 'green', shape: 'dot', text: 'connected'});
258 } else {
259 node.status({fill: 'red', shape: 'ring', text: 'disconnected'}, true);
260 }
261
262 node.stateChange = function (topic, state) {
263 if (node.regexTopic) {
264 if (!node.regexTopic.test(topic)) {
265 return;
266 }
267 } else if (node.topic !== '*' && node.topic !== topic) {
268 return;
269 }
270
271 if (node.onlyack && state && !state.ack) {
272 return;
273 }
274
275 const t = topic.replace(/\./g, '/') || '_no_topic';
276 //node.log ("Function: " + node.func);
277
278 if (node.func === 'rbe') {
279 if (state && state.val === node.previous[t]) {
280 return;
281 }
282 } else if (state && node.func === 'deadband') {
283 const n = parseFloat(state.val.toString());
284 if (!isNaN(n)) {
285 //node.log('Old Value: ' + node.previous[t] + ' New Value: ' + n);
286 if (node.pc) {
287 node.gap = (node.previous[t] * node.g / 100) || 0;
288 }
289 if (!node.previous.hasOwnProperty(t)) {
290 node.previous[t] = n - node.gap;
291 }
292 if (!Math.abs(n - node.previous[t]) >= node.gap) {
293 return;
294 }
295 } else {
296 node.warn('no number found in value');
297 return;
298 }
299 }
300 node.previous[t] = state ? state.val : null;
301
302 node.send({
303 topic: t,
304 payload: node.payloadType === 'object' ? state : (!state || state.val === null || state.val === undefined ? '' : (valueConvert ? state.val.toString() : state.val)),
305 acknowledged: state ? state.ack : false,
306 timestamp: state ? state.ts : Date.now(),
307 lastchange: state ? state.lc : Date.now(),
308 from: state ? state.from : ''
309 });
310
311 if (!state) {
312 node.status({
313 fill: 'red',
314 shape: 'ring',
315 text: 'not exists'
316 });
317 } else {
318 node.status({
319 fill: 'green',
320 shape: 'dot',
321 text: node.payloadType === 'object' ? JSON.stringify(state) : (!state || state.val === null || state.val === undefined ? '' : state.val.toString())
322 });
323 }
324 };
325
326 if (ready) {
327 adapter.on('stateChange', node.stateChange);
328 node.subscribePattern && adapter.subscribeForeignStates(node.subscribePattern);
329
330 if (node.fireOnStart && !node.topic.includes('*')) {
331 adapter.getForeignState(node.topic, (err, state) =>
332 node.stateChange(node.topic, state));
333 }
334 }
335
336 node.on('close', () => {
337 adapter.removeListener('stateChange', node.stateChange);
338 onClose(node);
339 });
340 existingNodes.push(node);
341 }
342 RED.nodes.registerType('ioBroker in', IOBrokerInNode);
343
344 function IOBrokerOutNode(n) {
345 const node = this;
346 RED.nodes.createNode(node, n);
347 node.topic = n.topic;
348
349 node.ack = n.ack === 'true' || n.ack === true;
350
351 defineCommon(node, n);
352
353 if (ready) {
354 node.status({fill: 'green', shape: 'dot', text: 'connected'});
355 } else {
356 node.status({fill: 'red', shape: 'ring', text: 'disconnected'}, true);
357 }
358
359 function setState(id, val, ack) {
360 if (node.idChecked) {
361 if (val !== undefined && val !== '__create__') {
362 // If not this adapter state
363 if (isForeignState(id)) {
364 adapter.setForeignState(id, {val, ack});
365 } else {
366 adapter.setState(id, {val, ack});
367 }
368 }
369 } else {
370 checkState(node, id, null, {val, ack});
371 }
372 }
373
374 node.on('input', msg => {
375 let id = node.topic;
376 if (!id) {
377 id = msg.topic;
378 }
379 // if not starts with adapter.instance.
380 if (id && !isValidID.test(id) && !id.startsWith(adapter.namespace)) {
381 id = adapter.namespace + '.' + id;
382 }
383
384 if (!ready) {
385 //log('Message for "' + id + '" queued because ioBroker connection not initialized');
386 nodeSets.push({node, msg});
387 } else if (id) {
388 id = id.replace(/\//g, '.');
389 // Create variable if not exists
390 if (node.autoCreate && !node.idChecked) {
391 if (!id.includes('*') && isValidID.test(id)) {
392 return checkState(node, id, assembleCommon(node, msg, id), {val: msg.payload, ack: node.ack}, isOk => {
393 if (isOk) {
394 node.status({
395 fill: 'green',
396 shape: 'dot',
397 text: msg.payload === null || msg.payload === undefined ? '' : msg.payload.toString()
398 });
399 } else {
400 node.status({
401 fill: 'red',
402 shape: 'ring',
403 text: 'Cannot set state'
404 });
405 }
406 });
407 }
408 }
409 // If not this adapter state
410 if (isForeignState(id)) {
411 // Check if state exists
412 adapter.getForeignObject(id, (err, obj) => {
413 if (!err && obj) {
414 adapter.setForeignState(id, {val: msg.payload, ack: node.ack}, (err, _id) => {
415 if (err) {
416 node.status({
417 fill: 'red',
418 shape: 'ring',
419 text: 'Error on setForeignState. See Log'
420 });
421 log('Error on setState for ' + id + ': ' + err);
422 } else {
423 node.status({
424 fill: 'green',
425 shape: 'dot',
426 text: _id + ': ' + (msg.payload === null || msg.payload === undefined ? '' : msg.payload.toString())
427 });
428 }
429 });
430 } else {
431 log('State "' + id + '" does not exist in the ioBroker');
432 node.status({
433 fill: 'red',
434 shape: 'ring',
435 text: 'State "' + id + '" does not exist in the ioBroker'
436 });
437 }
438 });
439 } else {
440 if (id.includes('*')) {
441 log('Invalid topic name "' + id + '" for ioBroker');
442 node.status({
443 fill: 'red',
444 shape: 'ring',
445 text: 'Invalid topic name "' + id + '" for ioBroker'
446 });
447 } else {
448 setState(id, msg.payload, node.ack, (err, _id) => {
449 if (err) {
450 node.status({
451 fill: 'red',
452 shape: 'ring',
453 text: 'Error on setState. See Log'
454 });
455 log('Error on setState for ' + id + ': ' + err);
456 } else {
457 node.status({
458 fill: 'green',
459 shape: 'dot',
460 text: _id + ': ' + (msg.payload === null || msg.payload === undefined ? '' : msg.payload.toString())
461 });
462 }
463 });
464 }
465 }
466 } else {
467 node.warn('No key or topic set');
468 node.status({
469 fill: 'red',
470 shape: 'ring',
471 text: 'No key or topic set'
472 });
473 }
474 });
475
476 node.on('close', () => onClose(node));
477 existingNodes.push(node);
478 }
479 RED.nodes.registerType('ioBroker out', IOBrokerOutNode);
480
481 function IOBrokerGetNode(n) {
482 const node = this;
483 RED.nodes.createNode(node, n);
484 node.topic = typeof n.topic === 'string' && n.topic.length > 0 ? n.topic.replace(/\//g, '.') : null;
485
486 defineCommon(node, n);
487
488 // If no adapter prefix, add own adapter prefix
489 if (node.topic && !isValidID.test(node.topic) && !node.topic.startsWith(adapter.namespace)) {
490 node.topic = adapter.namespace + '.' + node.topic;
491 }
492
493 node.payloadType = n.payloadType;
494 node.attrname = n.attrname;
495
496 // Create ID if not exits
497 if (node.topic && !node.topic.includes('*')) {
498 checkState(node, node.topic);
499 }
500
501 if (ready) {
502 node.status({fill: 'green', shape: 'dot', text: 'connected'});
503 } else {
504 node.status({fill: 'red', shape: 'ring', text: 'disconnected'}, true);
505 }
506
507 node.getStateValue = function (msg, id) {
508 return function (err, state) {
509 if (!err && state) {
510 msg[node.attrname] = (node.payloadType === 'object') ? state : ((state.val === null || state.val === undefined) ? '' : (valueConvert ? state.val.toString() : state.val));
511 msg.acknowledged = state.ack;
512 msg.timestamp = state.ts;
513 msg.lastchange = state.lc;
514 msg.topic = node.topic || msg.topic;
515 node.status({
516 fill: 'green',
517 shape: 'dot',
518 text: (node.payloadType === 'object') ? JSON.stringify(state) : ((state.val === null || state.val === undefined) ? '' : state.val.toString())
519 });
520 node.send(msg);
521 } else {
522 log('State "' + id + '" does not exist in the ioBroker');
523 }
524 };
525 };
526
527 node.on('input', msg => {
528 let id = node.topic || msg.topic;
529 if (!ready) {
530 nodeSets.push({node, msg});
531 //log('Message for "' + id + '" queued because ioBroker connection not initialized');
532 return;
533 }
534 if (id) {
535 if (id.includes('*')) {
536 log('Invalid topic name "' + id + '" for ioBroker');
537 } else {
538 id = id.replace(/\//g, '.');
539 // If not this adapter state
540 if (isForeignState(id)) {
541 adapter.getForeignState(id, node.getStateValue(msg, id));
542 } else {
543 adapter.getState(id, node.getStateValue(msg, id));
544 }
545 }
546 } else {
547 node.warn('No key or topic set');
548 }
549 });
550
551 node.on('close', () => onClose(node));
552 existingNodes.push(node);
553 }
554 RED.nodes.registerType('ioBroker get', IOBrokerGetNode);
555
556 function IOBrokerGetObjectNode(n) {
557 const node = this;
558 RED.nodes.createNode(node, n);
559 node.topic = typeof n.topic === 'string' && n.topic.length > 0 ? n.topic.replace(/\//g, '.') : null;
560
561 defineCommon(node, n);
562
563 // If no adapter prefix, add own adapter prefix
564 if (node.topic && !isValidID.test(node.topic) && !node.topic.startsWith(adapter.namespace)) {
565 node.topic = adapter.namespace + '.' + node.topic;
566 }
567 node.attrname = n.attrname;
568
569 // Create ID if not exits
570 if (node.topic && !node.topic.includes('*')) {
571 checkState(node, node.topic);
572 }
573
574 if (ready) {
575 node.status({fill: 'green', shape: 'dot', text: 'connected'});
576 } else {
577 node.status({fill: 'red', shape: 'ring', text: 'disconnected'}, true);
578 }
579
580 node.getObject = function (msg) {
581 return function (err, state) {
582 if (!err && state) {
583 msg[node.attrname] = state;
584 msg.topic = node.topic || msg.topic;
585 node.status({
586 fill: 'green',
587 shape: 'dot',
588 text: JSON.stringify(state)
589 });
590 node.send(msg);
591 } else {
592 log('Object "' + id + '" does not exist in the ioBroker');
593 }
594 };
595 };
596
597 node.on('input', msg => {
598 let id = node.topic || msg.topic;
599 if (!ready) {
600 nodeSets.push({node, msg});
601 //log('Message for "' + id + '" queued because ioBroker connection not initialized');
602 } else if (id) {
603 if (id.includes('*')) {
604 log('Invalid topic name "' + id + '" for ioBroker');
605 } else {
606 id = id.replace(/\//g, '.');
607 // If not this adapter state
608 if (isForeignState(id)) {
609 // Check if state exists
610 adapter.getForeignObject(id, node.getObject(msg));
611 } else {
612 adapter.getObject(id, node.getObject(msg));
613 }
614 }
615 } else {
616 node.warn('No key or topic set');
617 }
618 });
619
620 node.on('close', () => onClose(node));
621 existingNodes.push(node);
622 }
623 RED.nodes.registerType('ioBroker get object', IOBrokerGetObjectNode);
624
625 function IOBrokerListNode(n) {
626 const node = this;
627 RED.nodes.createNode(node, n);
628 node.topic = typeof n.topic === 'string' && n.topic.length > 0 ? n.topic.replace(/\//g, '.') : null;
629
630 // If no adapter prefix, add own adapter prefix
631 if (node.topic && !isValidID.test(node.topic) && !node.topic.startsWith(adapter.namespace)) {
632 node.topic = adapter.namespace + '.' + node.topic;
633 }
634 node.objType = n.objType;
635 node.regex = n.regex;
636 node.asArray = n.asArray === 'true' || n.asArray === true;
637 node.onlyIDs = n.onlyIDs === 'true' || n.onlyIDs === true;
638 node.withValues = n.withValues === 'true' || n.withValues === true;
639 if (node.regex) {
640 node.regex = new RegExp(node.regex.replace('\\', '\\\\'));
641 }
642
643 if (ready) {
644 node.status({fill: 'green', shape: 'dot', text: 'connected'});
645 } else {
646 node.status({fill: 'red', shape: 'ring', text: 'disconnected'}, true);
647 }
648
649 node.getObject = function (msg) {
650 return function (err, state) {
651 if (!err && state) {
652 msg[node.attrname] = state;
653 node.status({
654 fill: 'green',
655 shape: 'dot',
656 text: JSON.stringify(state)
657 });
658 node.send(msg);
659 } else {
660 log('Object "' + id + '" does not exist in the ioBroker');
661 }
662 };
663 };
664
665 node.on('input', msg => {
666 let pattern = node.topic || msg.topic;
667 if (!ready) {
668 nodeSets.push({node, msg});
669 } else if (pattern) {
670 pattern = pattern.replace(/\//g, '.');
671 adapter.getForeignObjects(pattern, (err, list) => {
672 if (!err) {
673 list = list || {};
674 if (node.objType) {
675 const newList = {};
676 Object.keys(list).forEach(id => {
677 if (list[id].type === node.objType) {
678 newList[id] = list[id];
679 }
680 });
681 list = newList;
682 }
683 if (node.regex) {
684 const newList = {};
685 Object.keys(list).forEach(id => {
686 if (node.regex.test(id)) {
687 newList[id] = list[id];
688 }
689 });
690 list = newList;
691 }
692
693 const ids = Object.keys(list);
694
695 adapter.getForeignStatesAsync(!node.withValues ? [] : ids)
696 .then(values => {
697 if (node.asArray) {
698 if (node.onlyIDs) {
699 msg.payload = ids;
700 if (node.withValues) {
701 msg.payload = msg.payload.map(id => {
702 values[id]._id = id;
703 return values[id];
704 });
705 }
706 } else {
707 let newList = [];
708 ids.forEach(id => newList.push(list[id]));
709 // Add states values if required
710 node.withValues && newList.forEach(el => Object.assign(el, values[el._id] || {}));
711 msg.payload = newList;
712 }
713 node.send(msg);
714 } else {
715 // every ID as one message
716 const _msg = JSON.parse(JSON.stringify(msg));
717 ids.forEach((id, i) => {
718 const __msg = !i ? msg : JSON.parse(JSON.stringify(_msg));
719 __msg.topic = id;
720 if (!node.onlyIDs) {
721 __msg.payload = list[id];
722 }
723 // Add states values if required
724 if (node.withValues) {
725 if (typeof __msg.payload !== 'object' || __msg.payload === null) {
726 __msg.payload = {};
727 }
728 node.withValues && Object.assign(__msg.payload, values[id]);
729 }
730 node.send(__msg);
731 });
732 }
733 });
734 } else {
735 log('Cannot get list of objects for "' + pattern + '": ' + err);
736 }
737 });
738 } else {
739 node.warn('No pattern set');
740 }
741 });
742
743 node.on('close', () => onClose(node));
744 existingNodes.push(node);
745 }
746 RED.nodes.registerType('ioBroker list', IOBrokerListNode);
747};