diff --git a/coap/coap-request.html b/coap/coap-request.html index f4650d7..2ad656b 100644 --- a/coap/coap-request.html +++ b/coap/coap-request.html @@ -7,7 +7,7 @@ value: "GET", validate: function (v) { return ( - ["", "GET", "PUT", "POST", "DELETE"].indexOf(v) !== -1 + ["use", "GET", "PUT", "POST", "DELETE"].indexOf(v) !== -1 ); }, }, @@ -16,8 +16,8 @@ multicastTimeout: { value: 20000, required: false }, url: { value: "" }, "content-format": { value: "text/plain" }, - "raw-buffer": { value: false, required: true }, name: { value: "" }, + paytoqs: {value: "ignore"}, }, inputs: 1, outputs: 1, @@ -29,10 +29,48 @@ labelStyle: function () { return this.name ? "node_label_italic" : ""; }, + oneditprepare: function () { + function updateMulticastOptions() { + if ($("#node-input-multicast").is(":checked")) { + $("#node-input-multicast-row").show(); + } else { + $("#node-input-multicast-row").hide(); + } + } + if (this.multicast) { + $("#node-input-multicast").prop("checked", true); + } else { + $("#node-input-multicast").prop("checked", false); + } + updateMulticastOptions(); + $("#node-input-multicast").on("click", function() { + updateMulticastOptions(); + }); + $("#node-input-method").on("change", function() { + if ($(this).val() === "GET") { + $(".node-input-paytoqs-row").show(); + } else { + $(".node-input-paytoqs-row").hide(); + } + }); + if (this.paytoqs === true || this.paytoqs === "query") { + $("#node-input-paytoqs").val("query"); + } else { + $("#node-input-paytoqs").val("ignore"); + } + $("#node-input-method").on("change", function() { + if ($(this).val() === "GET") { + $(".form-row-coap-request-observe").show(); + } else { + $(".form-row-coap-request-observe").hide(); + } + }).change(); + } }); diff --git a/coap/coap-request.js b/coap/coap-request.js index 71693b4..44ca424 100644 --- a/coap/coap-request.js +++ b/coap/coap-request.js @@ -9,11 +9,40 @@ module.exports = function (RED) { RED.nodes.createNode(this, config); var node = this; + var paytoqs = config.paytoqs || "ignore"; + + function _stringifyParams(params) { + var paramList = []; + for (var [key, value] of Object.entries(params)) { + var dataType = typeof value; + if (["string", "number"].includes(dataType)) { + paramList.push(`${key}=${value}`); + } + } + + return paramList.join("&"); + } + + function _appendQueryParams(reqOpts, payload) { + if (typeof payload === "object") { + var newParams = _stringifyParams(payload); + if (newParams && reqOpts.query !== "") { + newParams = "&" + newParams; + } + + reqOpts.query = reqOpts.query + newParams; + } else { + throw new Error("Hallo"); + } + } + function _constructPayload(msg, contentFormat) { var payload = null; - if (contentFormat === "text/plain") { - payload = msg.payload; + if (!msg.payload) { + return null; + } else if (contentFormat === "text/plain") { + payload = msg.payload.toString(); } else if (contentFormat === "application/json") { payload = JSON.stringify(msg.payload); } else if (contentFormat === "application/cbor") { @@ -49,18 +78,26 @@ module.exports = function (RED) { port: url.port, query: url.search.substring(1), }; - reqOpts.method = ( - config.method || - msg.method || - "GET" - ).toUpperCase(); - reqOpts.headers = {}; - reqOpts.headers["Content-Format"] = config["content-format"]; + reqOpts.method = config.method.toUpperCase() || "GET"; + if (config.method === "use" && msg.method != null) { + reqOpts.method = msg.method.toUpperCase(); + } + + reqOpts.headers = msg.headers; + if (reqOpts.headers == null) { + reqOpts.headers = {}; + } + if (reqOpts.headers["Content-Format"] == null) { + reqOpts.headers["Content-Format"] = "application/json"; + } reqOpts.multicast = config.multicast; reqOpts.multicastTimeout = config.multicastTimeout; function _onResponse(res) { function _send(payload) { + if (!reqOpts.observe) { + node.status({}); + } node.send( Object.assign({}, msg, { payload: payload, @@ -71,31 +108,38 @@ module.exports = function (RED) { } function _onResponseData(data) { - if (config["raw-buffer"]) { + var contentFormat = res.headers["Content-Format"]; + var configContentFormat = config["content-format"]; + + if (config["raw-buffer"] === true || configContentFormat === "raw-buffer") { _send(data); - } else if (res.headers["Content-Format"] === "text/plain") { + } else if (contentFormat === "text/plain" || configContentFormat === "text/plain") { _send(data.toString()); - } else if ( - res.headers["Content-Format"] === "application/json" - ) { + } else if (contentFormat.startsWith("application/") && contentFormat.includes("json")) { try { _send(JSON.parse(data.toString())); } catch (error) { + node.status({ + fill: "red", + shape: "ring", + text: error.message, + }); node.error(error.message); } - } else if ( - res.headers["Content-Format"] === "application/cbor" - ) { - cbor.decodeAll(data, function (err, data) { - if (err) { + } else if (contentFormat.startsWith("application/") && contentFormat.includes("cbor")) { + cbor.decodeAll(data, function (error, data) { + if (error) { + node.error(error.message); + node.status({ + fill: "red", + shape: "ring", + text: error.message, + }); return false; } _send(data[0]); }); - } else if ( - res.headers["Content-Format"] === - "application/link-format" - ) { + } else if (contentFormat === "application/link-format") { _send(linkFormat.parse(data.toString())); } else { _send(data.toString()); @@ -104,12 +148,28 @@ module.exports = function (RED) { res.on("data", _onResponseData); - if (reqOpts.observe) { + if (reqOpts.observe === true) { + node.status({ + fill: "blue", + shape: "dot", + text: "coapRequest.status.observing", + }); node.stream = res; } } - var payload = _constructPayload(msg, config["content-format"]); + var payload; + + if (reqOpts.method !== "GET") { + payload = _constructPayload(msg, reqOpts.headers["Content-Format"]); + } else if (paytoqs === "query") { + try { + _appendQueryParams(reqOpts, msg.payload); + } catch (error) { + node.error("Coap request: Invalid payload format!"); + return; + } + } if (config.observe === true) { reqOpts.observe = true; @@ -117,27 +177,41 @@ module.exports = function (RED) { delete reqOpts.observe; } - //TODO: should revisit this block + // TODO: should revisit this block if (node.stream) { node.stream.close(); } var req = coap.request(reqOpts); req.on("response", _onResponse); - req.on("error", function (err) { + req.on("error", function (error) { + node.status({ + fill: "red", + shape: "ring", + text: error.message, + }); node.log("client error"); - node.log(err); + node.log(error.message); }); - if (payload) { + if (payload != null) { req.write(payload); } req.end(); } this.on("input", function (msg) { + node.status({ + fill: "blue", + shape: "dot", + text: "coapRequest.status.requesting", + }); _makeRequest(msg); }); + + this.on("close", function () { + node.status({}); + }); } RED.nodes.registerType("coap request", CoapRequestNode); }; diff --git a/coap/locales/de/coap-request.json b/coap/locales/de/coap-request.json index f9df1c5..2929920 100644 --- a/coap/locales/de/coap-request.json +++ b/coap/locales/de/coap-request.json @@ -16,7 +16,7 @@ "label": "Multicast erzwingen" }, "inputMulticastTimeout": { - "label": "Multicast-Timeout (in ms)" + "label": "Timeout (in ms)" }, "inputObserve": { "label": "Observe?" @@ -27,6 +27,17 @@ "inputContentFormat": { "label": "Content-Format" }, + "return": { + "label": "Rückgabe", + "utf8": "Eine UTF-8-Zeichenfolge", + "binary": "Einen binären Buffer", + "json": "Ein geparstes JSON- oder CBOR-Objekt" + }, + "status": { + "observing": "beobachten", + "requesting": "anfordern", + "no-response": "Keine Antwort vom Server" + }, "tip": "Tipp: Lassen Sie den URL oder die Methode leer, wenn Sie diese über msg-Eigenschaften definieren wollen." } } diff --git a/coap/locales/en-US/coap-request.json b/coap/locales/en-US/coap-request.json index f622dbb..2fcb03e 100644 --- a/coap/locales/en-US/coap-request.json +++ b/coap/locales/en-US/coap-request.json @@ -16,7 +16,7 @@ "label": "Force multicast" }, "inputMulticastTimeout": { - "label": "Multicast timeout (in ms)" + "label": "Timeout (in ms)" }, "inputObserve": { "label": "Observe?" @@ -27,6 +27,26 @@ "inputContentFormat": { "label": "Content format" }, + "return": { + "label": "Return", + "utf8": "a UTF-8 string", + "binary": "a binary buffer", + "json": "a parsed JSON or CBOR object" + }, + "setby": { + "label": "- set by msg.method -" + }, + "paytoqs": { + "label": "Payload", + "ignore": "Ignore", + "query": "Append to query-string parameters", + "body": "Send as request body" + }, + "status": { + "observing": "observing", + "requesting": "requesting", + "no-response": "no response from server" + }, "tip": "Tip: Leave url or method blank if you want to set them via msg properties." } } diff --git a/test/coap-request_spec.js b/test/coap-request_spec.js index d194026..f42b074 100644 --- a/test/coap-request_spec.js +++ b/test/coap-request_spec.js @@ -141,7 +141,7 @@ describe("CoapRequestNode", function () { id: "n3", type: "coap request", "content-format": "text/plain", - method: "", + method: "use", name: "coapRequest", observe: false, url: "coap://localhost:" + port + "/test-resource", @@ -675,8 +675,18 @@ describe("CoapRequestNode", function () { id: "n1", type: "inject", name: "Fire once", - payload: test.message, - payloadType: "string", + props: [ + { + "p": "payload", + "vt": "string", + "v": test.message + }, + { + "p": "headers", + "v": {"Content-Format": test.format}, + "vt": "object" + } + ], repeat: "", crontab: "", once: true, @@ -685,7 +695,6 @@ describe("CoapRequestNode", function () { { id: "n2", type: "coap request", - "content-format": test.format, method: "POST", name: "coapRequestPost", observe: false,