bartbutenaers / node-red-contrib-ui-svg

A Node-RED widget node to show interactive SVG (vector graphics) in the dashboard
Apache License 2.0
93 stars 27 forks source link

Request data #51

Closed tkirchm closed 3 years ago

tkirchm commented 4 years ago

Hi @bartbutenaers

I want to visualize data from a database and not from sensors and therefore I need to know which data is shown in the image.

Example: An image shows a heating system with all sensor data. All sensors are also logged into a sqlite database and I want to scroll back to a certain time.

On msg.request = "text[id^='sen']" a msg from all text boxes who's id starts with 'sen' will be sent with inner text in msg.payload.

bartbutenaers commented 4 years ago

Hi @tkirchm,

Sorry for the delay, but the recovery of my surgery took a bit longer as expected ...

Perhaps it could be useful to add such functionality for some use cases, but I don’t really see why you need it here. But please correct me if I'm wrong!

You have suggested here a nice use case to show historical IOT data in an svg, since such a picture says more than 1000 numbers 😉

But your nodes - that are responsible for drawing the SVG - know which data they have send for visualization. Seems bad practice to me to ask the SVG node afterwards which data has been visualized. Because your nodes already know which data has been sent to the browser.

Seems to me that you want to use your browser (where the dashboard is running) to store your Node-RED flow data temporarily (flow --> dashboard --> flow) ...

It think it is better that your nodes simply keep track of the timestamp of the data, which they are currently visualising.

The following flow stores some historical data on flow memory

[{"id":"1adbf8ba.794347","type":"ui_svg_graphics","z":"42a6f86e.1dff98","group":"bf03d44f.3469e8","order":2,"width":0,"height":0,"svgString":"<svg x=\"0\" y=\"0\" height=\"100\" viewBox=\"0 0 100 100\" width=\"100\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n    <circle style=\"fill:url(#toning);stroke:#010101;stroke-width:1.6871;stroke-miterlimit:10;\" cx=\"50%\" cy=\"50%\" r=\"30\">\n    </circle>\n    <text id=\"dataset_value\" x=\"50%\" y=\"50%\" text-anchor=\"middle\" alignment-baseline=\"middle\" stroke=\"#51c5cf\" stroke-width=\"2px\" font-size=\"30px\"></text>\n</svg>","clickableShapes":[],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"outputField":"payload","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","panEnabled":false,"zoomEnabled":false,"controlIconsEnabled":false,"dblClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"name":"","x":700,"y":480,"wires":[[]]},{"id":"5954061f.fe4b98","type":"ui_date_picker","z":"42a6f86e.1dff98","name":"","label":"date","group":"bf03d44f.3469e8","order":1,"width":0,"height":0,"passthru":true,"topic":"","x":270,"y":480,"wires":[["65b863bf.7ad47c"]]},{"id":"2076294a.d8cb66","type":"inject","z":"42a6f86e.1dff98","name":"At startup","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":true,"onceDelay":0.1,"x":300,"y":400,"wires":[["72464a17.566d64"]]},{"id":"72464a17.566d64","type":"change","z":"42a6f86e.1dff98","name":"Initialize data for dates","rules":[{"t":"set","p":"dataset","pt":"flow","to":"{\"2020-3-1\":\"A\",\"2020-3-2\":\"B\",\"2020-3-3\":\"C\",\"2020-3-4\":\"D\",\"2020-3-5\":\"E\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":520,"y":400,"wires":[[]]},{"id":"65b863bf.7ad47c","type":"function","z":"42a6f86e.1dff98","name":"Get value for date","func":"// Convert timestamp to milliseconds\nvar date = new Date(msg.payload);\n\nvar dateAsString = date.getFullYear() + \"-\" + (date.getMonth() + 1) + \"-\" + date.getDate();\n\nvar valueOfDate = flow.get(\"dataset\")[dateAsString];\n\nreturn {\n    \"payload\": {\n        \"command\": \"update_text\",\n        \"selector\": \"#dataset_value\", \n        \"textContent\": valueOfDate\n    }\n }","outputs":1,"noerr":0,"x":470,"y":480,"wires":[["1adbf8ba.794347"]]},{"id":"bf03d44f.3469e8","type":"ui_group","z":"","name":"History demo","tab":"aa08ed48.18388","disp":true,"width":"6","collapse":false},{"id":"aa08ed48.18388","type":"ui_tab","z":"","name":"SVG","icon":"dashboard","disabled":false,"hidden":false}]
  1. The first part of this flow loads some test time series data into flow memory

    image

    This is the data that will be loaded (i.e. a letter for four dates):

    {
       "2020-3-1": "A",
       "2020-3-2": "B",
       "2020-3-3": "C",
       "2020-3-4": "D",
       "2020-3-5": "E"
    }
  2. The second part of this flow shows a datepicker. As soon as one of the four above dates is selected, the corresponding value (A, B, C, D or E) will be fetched from flow memory and injected into the SVG:

    image

Short demo of the result:

history_demo

So now you can visualise historical data, and you know exactly the date and the value in your flow (without needing to ask it from your SVG node).

Summary of this example flow:

  1. Ask the user for a timestamp
  2. Get the data of that timestamp from your database
  3. Ask the SVG node to visualize that data

Or is there something I have misunderstand perhaps?

Bart

tkirchm commented 4 years ago

Hi @bartbutenaers,

nice to hear that you are back.

Thanks for the example. It is possible to implement it in this way but it would not be easy. I have five different data sources and all of them know how to generate a message for the svg node. All data is sent to three svg nodes and the database. I have three svg drawings and as there was no need because performance was not a problem I didn't select messages. Therefore, it would be much easier the svg node can ask for the data.

Thomas

bartbutenaers commented 4 years ago

Hello @tkirchm,

Did a quick test (not on Github yet) by adding this code snippet:

switch (op) {
   case "get_text":
      selector = payload.selector || "#" + payload.elementId;
      elements = $scope.rootDiv.querySelectorAll(selector);
      if (!elements || !elements.length) {
         console.log("Invalid selector. No SVG elements found for selector " + selector);
         return;
      }

      var elementArray = [];
      elements.forEach(function(element){
         elementArray.push({
            id: element.id,
            text: element.textContent
         });
      });  

      $scope.send({
         payload: elementArray
      });                                             
      break;
   case "update_text":
   case "update_innerHTML":
      ...

Then I can send input messages like this:

{
    "command": "get_text",
    "selector": "text"
}

Example flow

When I use a drawing with two text elements (id "first_text" and "second_text"):

image

And this flow:

image

[{"id":"9b91d170.c293f","type":"ui_svg_graphics","z":"42a6f86e.1dff98","group":"bf03d44f.3469e8","order":2,"width":0,"height":0,"svgString":"<svg x=\"0\" y=\"0\" height=\"100\" viewBox=\"0 0 100 100\" width=\"100\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n    <circle style=\"fill:url(#toning);stroke:#010101;stroke-width:1.6871;stroke-miterlimit:10;\" cx=\"30%\" cy=\"50%\" r=\"30\">\n    </circle>\n    <text id=\"first_text\" x=\"30%\" y=\"50%\" text-anchor=\"middle\" alignment-baseline=\"middle\" stroke=\"#51c5cf\" stroke-width=\"2px\" font-size=\"30px\">A</text>\n    <circle style=\"fill:url(#toning);stroke:#010101;stroke-width:1.6871;stroke-miterlimit:10;\" cx=\"80%\" cy=\"50%\" r=\"30\">\n    </circle>\n    <text id=\"second_text\" x=\"80%\" y=\"50%\" text-anchor=\"middle\" alignment-baseline=\"middle\" stroke=\"#51c5cf\" stroke-width=\"2px\" font-size=\"30px\">B</text>\n</svg>","clickableShapes":[{"targetId":"#dataset_value","action":"click","payload":"#dataset_value","payloadType":{"length":0,"prevObject":{"0":{},"length":1}},"topic":"#dataset_value"}],"smilAnimations":[],"bindings":[],"showCoordinates":false,"autoFormatAfterEdit":false,"outputField":"payload","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","panEnabled":false,"zoomEnabled":false,"controlIconsEnabled":false,"dblClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"name":"","x":560,"y":620,"wires":[["7b16d327.b57d5c"]]},{"id":"7b16d327.b57d5c","type":"debug","z":"42a6f86e.1dff98","name":"SVG output","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","x":750,"y":620,"wires":[]},{"id":"7caf05f9.b679bc","type":"inject","z":"42a6f86e.1dff98","name":"Get text from \"#first_text\"","topic":"","payload":"{\"command\":\"get_text\",\"selector\":\"#first_text\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":290,"y":620,"wires":[["9b91d170.c293f"]]},{"id":"1d596d05.8afa33","type":"inject","z":"42a6f86e.1dff98","name":"Get text from \"#second_text\"","topic":"","payload":"{\"command\":\"get_text\",\"selector\":\"#second_text\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":300,"y":660,"wires":[["9b91d170.c293f"]]},{"id":"3ce7d79.ad96a28","type":"inject","z":"42a6f86e.1dff98","name":"Get text from \"text\"","topic":"","payload":"{\"command\":\"get_text\",\"selector\":\"text\"}","payloadType":"json","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":310,"y":700,"wires":[["9b91d170.c293f"]]},{"id":"bf03d44f.3469e8","type":"ui_group","z":"","name":"History demo","tab":"aa08ed48.18388","disp":true,"width":"6","collapse":false},{"id":"aa08ed48.18388","type":"ui_tab","z":"","name":"SVG","icon":"dashboard","disabled":false,"hidden":false}]

Then indeed I get an array with the text content of the specified elements:

svg_get_text

So the concept works!

Summary

However I seems a VERY BAD solution to me, because:

  1. When you inject a message, you will only get a result when the SVG is visible in the dashboard at that moment. If the dashboard is not open or another tabsheet is displayed at the moment, then you will get NO result (because the AngularJs client side of my widget just doesn't get your input message)!
  2. When N dashboards are open (and in each of those dashboards the SVG is visible at the moment) then you will get N output messages. Since each SVG will be identical, the N output messages will also be identical. So you will end up with N duplicate messages.

This is what I meant: your flow uses your browser session to store the data temporarily. And that data is only available when the SVG is visible.

You will need to have very strong arguments to convince me to merge this functionality on Github :-)

Bart

tkirchm commented 4 years ago

Hi, Thanks for your very fast and detailed test. I found a usecase. I have installed onewire in my home and I can only read the temperature every 10s. By doing it faster, it kills onewire. Which is enough for room temperature but very slow if you have to wait for it on a dashboard. Thomas

bartbutenaers commented 4 years ago

Hi Thomas, Could you explain a bit more how the 'request data' feature could help you with this? You read the temperature every 10 second and show it. When N dashboards open, you would get N results (N can be zero).
Thanks!

tkirchm commented 4 years ago

Hi Bart, I can only read temperatures every 10 seconds. Witch means that I have to wait max. 10 seconds to get temperatures in the picture. One way would be to resend every message so I do not have to wait. However, I would prefer to ask for the temperature when a svg is shown instead of sending thousands of messages. As I log those temperatures in a sqlite-database asking would be much nicer.

Thomas

bartbutenaers commented 4 years ago

Ah, I'm getting old. Still don't get it. I need to have a general solution that fits everybody's needs (and I want to avoid having lots of discussions afterwards with other users in case this mechanism doesn't work 100% as expected).

when a svg is shown

This is for me the big question... As I mentioned in my last response: suppose N of these floorplans are currently being displayed (where N can be zero). Indeed somebody can have multiple wall-mounted tablets in his house, so the floorplan might be visible at the same time N times. If you send a request to get the value of an element id, the you will get N responses. Where N can be 0 which means no response at all (e.g. when no dashboard is currently started or none of the dashboards is currently showing the tabsheet with the floorplan).

Summarized I can have duplicate output messages or no output message at all. How did you plan to use this mechanism reliable if you don't know what the output of the node will be?

Can you please give me an example of a single sensor with some historical data, preferable in a little quick drawn sketch (so I can see which values are in your database, and which value is being displayed).

I'm currently trying to prepare the 2.0.0 version to announce a beta release. So if you have a good explanation, please let me know so I can add your feature request in this release.

bartbutenaers commented 4 years ago

Thomas (@tkirchm), There is a discussion about this on the Node-RED, which might be interesting for you!

tkirchm commented 3 years ago

Hi Bart,

its been a while and I love the SVG-node. I use it every day.

I found a solution for my problem in the flow from colinl. The node sends a message when the view is opened in a browser. I can use this message to request data for the svg.

Can you include this? This way I can request all sensor data from my postgres server.

Thomas

bartbutenaers commented 3 years ago

Hi Thomas, Nice to hear from you again! That is indeed a quick-win. When you install the 2.1.0 version from Github (which should be published on npm very soon), you will now have an extra option:

image

The content of the output msg is explained on the readme page. For example:

image

Please let me know if this fits your needs! Bart

tkirchm commented 3 years ago

Hi Bart,

thanks. Looks good. I'll wait for the update and let you know.

Thomas

bartbutenaers commented 3 years ago

Hi Thomas (@tkirchm), Version 2.1.0 is now published on NPM. Bart

tkirchm commented 3 years ago

Hi, i just implemented loaded in my flow and want to share it.

[{"id":"6abd4246.bcda1c","type":"inject","z":"b7209503.a11f58","name":"Sensor`` A = true","topic":"sensor_a","payload":"true","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":160,"y":1820,"wires":[["2f09d88b.804a68","b96b72b1.7a21e"]]},{"id":"44a0f8ce.36ddf8","type":"inject","z":"b7209503.a11f58","name":"Sensor A = false","topic":"sensor_a","payload":"false","payloadType":"bool","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":160,"y":1860,"wires":[["2f09d88b.804a68","b96b72b1.7a21e"]]},{"id":"7094a559.0707dc","type":"ui_svg_graphics","z":"b7209503.a11f58","group":"3c394e07.6fcf22","order":1,"width":"14","height":"10","svgString":"<svg preserveAspectRatio=\"none\" x=\"0\" y=\"0\" viewBox=\"0 0 900 710\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\">\n <circle id=\"sensor_a\" cx=\"50\" cy=\"50\" r=\"20\" stroke-width=\"0\" fill=\"#FF0000\" />\n</svg> ","clickableShapes":[{"targetId":"#camera_living","action":"click","payload":"camera_living","payloadType":"str","topic":"camera_living"}],"javascriptHandlers":[],"smilAnimations":[],"bindings":[{"selector":"#banner","bindSource":"payload.title","bindType":"text","attribute":""},{"selector":"#camera_living","bindSource":"payload.position.x","bindType":"attr","attribute":"x"},{"selector":"#camera_living","bindSource":"payload.camera.colour","bindType":"attr","attribute":"fill"}],"showCoordinates":false,"autoFormatAfterEdit":false,"showBrowserErrors":true,"showBrowserEvents":false,"enableJsDebugging":false,"sendMsgWhenLoaded":true,"outputField":"","editorUrl":"http://drawsvg.org/drawsvg.html","directory":"","panning":"disabled","zooming":"disabled","panOnlyWhenZoomed":false,"doubleClickZoomEnabled":false,"mouseWheelZoomEnabled":false,"dblClickZoomPercentage":"150","name":"","x":760,"y":1820,"wires":[["1b529344.09899d"]]},{"id":"7fc4ac3.0580854","type":"comment","z":"b7209503.a11f58","name":"Link in from different Sources (MQTT, Modbus, ...)","info":"","x":200,"y":1740,"wires":[]},{"id":"d6011593.3b1358","type":"function","z":"b7209503.a11f58","name":"loaded","func":"var g = flow.keys() \nvar m = {};\n\ng.forEach(function(key,i){\n var v = flow.get(key);\n m = {\n topic: key,\n payload: v.value\n }\n node.send(m);\n});\n \n\n\nreturn;","outputs":1,"noerr":0,"x":1170,"y":1880,"wires":[["b96b72b1.7a21e"]]},{"id":"2f09d88b.804a68","type":"function","z":"b7209503.a11f58","name":"SetGlobalStore","func":"var now = new Date();\nflow.set(msg.topic,{\"time\":now, \"value\":msg.payload})","outputs":1,"noerr":0,"x":400,"y":2000,"wires":[[]]},{"id":"b96b72b1.7a21e","type":"function","z":"b7209503.a11f58","name":"SetIcon","func":"var m = {\n payload: {\n command: \"update_style\",\n selector: '#' + msg.topic,\n attributeName: \"fill\",\n attributeValue: msg.payload === true ? 'yellow':'black'\n }\n};\nreturn m;","outputs":1,"noerr":0,"x":520,"y":1820,"wires":[["7094a559.0707dc"]]},{"id":"1b529344.09899d","type":"function","z":"b7209503.a11f58","name":"CommandReload","func":"if (msg.topic == 'loaded'){\n return [null, msg];\n} else {\n return [msg, null];\n}","outputs":2,"noerr":0,"x":970,"y":1820,"wires":[[],["d6011593.3b1358"]]},{"id":"304a3fdf.02d1c","type":"comment","z":"b7209503.a11f58","name":"Output 1 for additional things like Context menu","info":"","x":1160,"y":1760,"wires":[]},{"id":"3c394e07.6fcf22","type":"ui_group","z":"","name":"Test","tab":"965f6585.8cb3b8","order":1,"disp":true,"width":"14","collapse":false},{"id":"965f6585.8cb3b8","type":"ui_tab","z":"","name":"Home","icon":"dashboard","disabled":false,"hidden":false}]

So I can collect sensor data from different sources at different rates and when loading ui it will show last value.

Thomas