@@ -5,7 +5,8 @@ module.exports = function (RED) {
55 const HttpsProxyAgent = require(' https-proxy-agent' );
66 const WebSocket = require(' ws' );
77 const urltemplate = require(' url-template' );
8- const Ajv = require(' ajv' );
8+ const Ajv = require(' ajv' ).default ;
9+ const nodecoap = require(' coap' );
910
1011 function extractTemplate(href, context= {} ) {
1112 return urltemplate.parse(href).expand(context);
@@ -68,14 +69,33 @@ module.exports = function (RED) {
6869 }
6970 }
7071
72+ function createCoapReqOpts(resource, method, observe=false) {
73+ let urlobj = new URL(resource);
74+ let hostname = urlobj.hostname;
75+ if (hostname.startsWith(' [' ) && hostname.endsWith(' ]' )) {
76+ // remove square brackets from IPv6 address
77+ hostname = hostname.slice(1,-1);
78+ }
79+ let query = urlobj.search;
80+ if (query.startsWith('?')) {
81+ query = query.slice(1);
82+ }
83+ return {
84+ hostname: hostname,
85+ port: urlobj.port,
86+ method: method,
87+ pathname: urlobj.pathname,
88+ query: query,
89+ observe: observe
90+ } ;
91+ }
92+
7193 function bindingCoap(node, send, done, form, options={ } ) { // options.psk
72- const coap = require(" node-coap-client" ).CoapClient;
7394 node.trace(" bindingCoap called" );
7495 const msg = options.msg || {} ;
7596 const resource = extractTemplate(form.href, options.urivars);
7697 let payload = null;
7798 let method = null;
78-
7999 if (options.interaction === "property-read") {
80100 method = form.hasOwnProperty(" cov:methodName" ) ?
81101 coapMethodCodeToName(form[' cov:methodName' ]) : ' get' ;
@@ -89,78 +109,105 @@ module.exports = function (RED) {
89109 payload = options.reqbody;
90110 }
91111
92- node.trace(`CoAP request: resource=${ resource} , method=${ method} , payload=${ payload} `);
93- coap.request(resource, method, payload)
94- .then(response => { // code, format, payload
95- node.trace(`CoAP response: code= ${response.code.toString()} , format=${ response.format} , payload=${ response.payload} `);
96- if (response.format === 50) { // application/json; rfc7252 section 12.3, table 9
112+ const coapreqopt = createCoapReqOpts(resource, method, false);
113+
114+ node.trace(`CoAP request: reqopt=${ JSON.stringify(coapreqopt)} ,payload=${ payload} `);
115+ let outmsg = nodecoap.request(coapreqopt);
116+ if (payload) {
117+ outmsg.write(payload);
118+ }
119+ outmsg.on('response', response => {
120+ const cf = response.options.find(e= > e.name === ' Content-Format' );
121+ if (cf && cf.value === ' application/json' ) {
97122 try {
98- msg.payload = JSON.parse(response.payload);
123+ msg.payload = JSON.parse(response.payload.toString());
124+ if (options.outschema) {
125+ const ajv = new Ajv({allErrors: true , strict: false , validateFormats: false } );
126+ if (!ajv.validate(options.outschema, msg.payload)) {
127+ node.warn(`output schema validation error: ${ajv.errorsText()} `, msg);
128+ }
129+ }
99130 } catch (e) {
100131 msg.payload = response.payload;
101132 }
133+ } else if (cf && cf.value === 'text/plain') {
134+ msg.payload = response.payload.toString();
102135 } else {
103136 msg.payload = response.payload;
104137 }
105- if (options.outschema) {
106- const ajv = new Ajv({allErrors: true } );
107- if (!ajv.validate(options.outschema, msg.payload)) {
108- node.warn(`output schema validation error: ${ajv.errorsText()} `, msg);
109- }
110- }
111138 send(msg);
112- if (done) {
113- done();
114- }
115- })
116- .catch(err => {
117- node.log(`Error: ${err.toString()} `);
118- msg.payload = `${ err.toString()} : ${ resource} `;
139+ response.on('end', () => { if (done) { done(); } });
140+ });
141+ const errorHandler = (err) => {
142+ node.warn(`CoAP request error: ${err.message} `);
143+ delete msg.payload;
144+ msg.error = err;
119145 send(msg);
120- if (done) {
121- done();
122- }
123- });
146+ if (done) { done() } ;
147+ };
148+ outmsg.on('timeout', errorHandler);
149+ outmsg.on('error', errorHandler);
150+ outmsg.end();
124151 }
125152
126153 function bindingCoapObserve(node, form, options={ } ) {
127- const coap = require(" node-coap-client" ).CoapClient;
128154 node.status({fill:" yellow" ,shape:" dot" ,text:" CoAP try to observe ..." } );
129155 const resource = extractTemplate(form.href,options.urivars);
130156 const method = form.hasOwnProperty("cov:methodName") ?
131157 coapMethodCodeToName(form['cov:methodName']) : 'get';
132158 const payload = options.reqbody;
133- const callback = response => { // code, format, payload
134- const msg = {} ;
135- node.trace(`CoAP observe: code=${ response.code.toString()} , format=${ response.format} , payload=${ response.payload} `);
136- if (response.format === 50) { // application/json; rfc7252 section 12.3, table 9
137- try {
138- msg.payload = JSON.parse(response.payload);
139- } catch (e) {
140- msg.payload = response.payload;
141- }
142- } else {
143- msg.payload = response.payload;
144- }
145- if (options.outschema) {
146- const ajv = new Ajv({allErrors: true } );
147- if (!ajv.validate(options.outschema, msg.payload)) {
148- node.warn(`output schema validation error: ${ajv.errorsText()} `, msg);
159+ let observingStream;
160+
161+ const coapreqopt = createCoapReqOpts(resource, method, true);
162+ node.trace(`CoAP observe request: reqopt=${ JSON.stringify(coapreqopt)} ,payload=${ payload} `);
163+
164+ let outmsg = nodecoap.request(coapreqopt);
165+ if (payload) {
166+ outmsg.write(payload);
167+ }
168+ outmsg.on('response', response => {
169+ observingStream = response;
170+ const cf = response.options.find(e= > e.name === ' Content-Format' );
171+ response.on(' end' , () => {} );
172+ response.on('data', chunk => {
173+ const msg = {} ;
174+ if (cf && cf.value === 'application/json') {
175+ try {
176+ msg.payload = JSON.parse(chunk.toString());
177+ if (options.outschema) {
178+ const ajv = new Ajv({allErrors: true , strict: false , validateFormats: false } );
179+ if (!ajv.validate(options.outschema, msg.payload)) {
180+ node.warn(`output schema validation error: ${ajv.errorsText()} `, msg);
181+ }
182+ }
183+ } catch (e) {
184+ msg.payload = chunk;
185+ }
186+ } else if (cf && cf.value === 'text/plain') {
187+ msg.payload = chunk.toString();
188+ } else {
189+ msg.payload = chunk;
149190 }
150- }
151- node.send(msg);
152- };
153- coap.observe(resource,method,callback,payload,options)
154- .then(() => {
155- node.status({fill:" green" ,shape:" dot" ,text:" CoAP Observing" } );
156- })
157- .catch(err => {
158- node.status({fill:" red" ,shape:" dot" ,text:`CoAP Error: ${err} `});
159- node.log(`Error: ${ err.toString()} `);
191+ node.send(msg);
192+ });
160193 });
194+ const errorHandler = (err) => {
195+ node.warn(`CoAP request error: ${err.message} `);
196+ node.status({ fill:' red' ,shape:' dot' ,text:`CoAP Error: ${err} `});
197+ delete msg.payload;
198+ msg.error = err;
199+ node.send(msg);
200+ };
201+ outmsg.on('timeout', errorHandler);
202+ outmsg.on('error', errorHandler);
203+ outmsg.end();
204+ node.status({ fill:' green' ,shape:' dot' ,text:' CoAP Observing' } );
161205 node.on('close', () => {
162206 node.trace(' Close node' );
163- coap.stopObserving(resource);
207+ if (observingStream) {
208+ observingStream.close();
209+ observingStream = null;
210+ } ;
164211 node.status({ } );
165212 });
166213 }
@@ -215,7 +262,7 @@ module.exports = function (RED) {
215262 msg.payload = data;
216263 }
217264 if (options.outschema) {
218- const ajv = new Ajv({allErrors: true } );
265+ const ajv = new Ajv({allErrors: true , strict: false , validateFormats: false } );
219266 if (!ajv.validate(options.outschema, msg.payload)) {
220267 node.warn(`output schema validation error: ${ajv.errorsText()} `, msg);
221268 }
@@ -282,7 +329,7 @@ module.exports = function (RED) {
282329 }
283330 // TODO: validation of return value
284331 if (options.outschema) {
285- const ajv = new Ajv();
332+ const ajv = new Ajv({allErrors: true , strict: false , validateFormats: false } );
286333 if (!ajv.validate(options.outschema, msg.payload)) {
287334 node.warn(`output schema validation error: ${ajv.errorsText()} `, msg);
288335 }
@@ -370,7 +417,7 @@ module.exports = function (RED) {
370417 }
371418 // TODO: validation of return value
372419 if (options.outschema) {
373- const ajv = new Ajv({allErrors: true } );
420+ const ajv = new Ajv({allErrors: true , strict: false , validateFormats: false } );
374421 if (!ajv.validate(options.outschema, msg.payload)) {
375422 node.warn(`output schema validation error: ${ajv.errorsText()} `, msg);
376423 }
@@ -430,15 +477,15 @@ module.exports = function (RED) {
430477 const auth = makeauth(normTd, form, username, password, token);
431478 const urivars = prop.hasOwnProperty("uriVariables") ? msg.payload : { } ;
432479 if (prop.uriVariables) {
433- const ajv = new Ajv({allErrors: true } );
480+ const ajv = new Ajv({allErrors: true , strict: false , validateFormats: false } );
434481 if (!ajv.validate(prop.uriVariables, urivars)) {
435482 node.warn(`input schema validation error: ${ajv.errorsText()} `, msg);
436483 }
437484 }
438485 if (form.href.match(/^https?:/)) {
439486 bindingHttp(node, send, done, form, {interaction:" property-read" , auth, msg, urivars, outschema: prop} );
440487 } else if (form.href.match(/^coaps?:/)) {
441- bindingCoap(node, send, done, form, send, done, {interaction:" property-read" , auth, msg, urivars, outschema: prop} );
488+ bindingCoap(node, send, done, form, {interaction:" property-read" , auth, msg, urivars, outschema: prop} );
442489 }
443490 });
444491 } else if (node.proptype === "write") {
@@ -447,7 +494,7 @@ module.exports = function (RED) {
447494 const prop = normTd.properties[node.propname];
448495 const form = prop.forms[node.formindex];// formSelection("property-write", prop.forms);
449496 const auth = makeauth(normTd, form, username, password, token);
450- const ajv = new Ajv({ allErrors: true } );
497+ const ajv = new Ajv({ allErrors: true , strict: false , validateFormats: false } );
451498 if (!ajv.validate(prop, msg.payload)) {
452499 node.warn(`input schema validation error: ${ajv.errorsText()} `, msg);
453500 }
@@ -464,7 +511,7 @@ module.exports = function (RED) {
464511 const auth = makeauth(normTd, form, username, password, token);
465512 const urivars = prop.hasOwnProperty(" uriVariables" ) ? msg.payload : {} ;
466513 if (prop.uriVariables) {
467- const ajv = new Ajv({allErrors: true } );
514+ const ajv = new Ajv({allErrors: true , strict: false , validateFormats: false } );
468515 if (!ajv.validate(prop.uriVariables, urivars)) {
469516 node.warn(`input schema validation error: ${ajv.errorsText()} `, msg);
470517 }
@@ -485,13 +532,13 @@ module.exports = function (RED) {
485532 const auth = makeauth(normTd, form, username, password, token);
486533 const urivars = act.hasOwnProperty("uriVariables") ? msg.payload : { } ;
487534 if (act.uriVariables) {
488- const ajv = new Ajv({allErrors: true } );
535+ const ajv = new Ajv({allErrors: true , strict: false , validateFormats: false } );
489536 if (!ajv.validate(act.uriVariables, urivars)) {
490537 node.warn(`input schema validation error: ${ajv.errorsText()} `, msg);
491538 }
492539 }
493540 if (act.input) {
494- const ajv = new Ajv({allErrors: true } );
541+ const ajv = new Ajv({allErrors: true , strict: false , validateFormats: false } );
495542 if (!ajv.validate(act.input, msg.payload)) {
496543 node.warn(`input schema validation error: ${ajv.errorsText()} `, msg);
497544 }
@@ -508,7 +555,7 @@ module.exports = function (RED) {
508555 const auth = makeauth(normTd, form, username, password, token);
509556 const urivars = ev.hasOwnProperty(" uriVariables" ) ? msg.payload : {} ;
510557 if (ev.uriVariables) {
511- const ajv = new Ajv({allErrors: true } );
558+ const ajv = new Ajv({allErrors: true , strict: false , validateFormats: false } );
512559 if (!ajv.validate(ev.uriVariables, urivars)) {
513560 node.warn(`input schema validation error: ${ajv.errorsText()} `, msg);
514561 }
0 commit comments