Create web page dynamically, depending on clients HTTP request

Hi, total Node-RED newbie here.

I've got a PIR sensor, which upon detecting motion is waking a ESP8266 from deep sleep. The ESP is connecting to Wifi, then sending an HTTP request to the (Node-RED) web server, then going back to sleep.

At the server I'd like to dynamically create/update some web page content. If a request from the ESP comes in, the page should basically just say "Alert", and if there hasn't been a new request for a certain time then the page should return to displaying "OK".
To distinguish the ESP requests from those generated by users I would append some "secret" data to the ESP request, e.g. GET /pir?iamesp

I've set up a minimal flow with an HTTP_in, a Template with some HTML and an HTTP_out.
But I have no idea how to parse the request data and then to create HTML "on the fly".

My google fu is failing me, all I get there are examples on how to send an HTTP request from Node-RED to another host, i.e. the other way around.

Thanks for any help!

If you really want to create your own custom interface, you can do so either by using Node-RED's http-in/-out nodes or something like uibuilder. If you want something mostly ready-made - as long as you are happy with the inevitable constraints, you should use Dashboard.

Thanks for the reply.
I really need only the most basic web interface, but I do need to have some javascript in there to dynamically change the favicon according to the Alert/OK state.
I would then have this page permanently open in a desktop browser as a pinned tab, so that I could check the state with a quick glance at the favicon.

Like I said, I did create a web interface with http-in/-out, I'm now looking for a way to update the content, depending on the data submitted via the HTTP request.

uibuilder will let you build your own custom web page and will help with dynamic updates. Amongst other things, it creates a websocket interface for you that lets you send messages to/from the front-end just like any other flow in Node-RED. This is the easiest way to do it.

You can try this flow:

[{"id":"768f2f024f5ef580","type":"http in","z":"31c1e2adb95d7cd8","name":"","url":"/pir","method":"get","upload":false,"swaggerDoc":"","x":240,"y":500,"wires":[["7965f2c9dbbd9851"]]},{"id":"6049d296d3eb6644","type":"http response","z":"31c1e2adb95d7cd8","name":"","statusCode":"","headers":{},"x":810,"y":480,"wires":[]},{"id":"adaee07d9bd7127c","type":"template","z":"31c1e2adb95d7cd8","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!DOCTYPE html>\n<html>\n    <head>\n        <style>\n        body{font-family:sans-serif;padding:20px}\n        #pir{padding:4px;color:white;padding:8px}\n        .alert{background-color:#f30}\n        .ok{background-color:green}\n        </style>\n    </head>\n<body>\n<div id='pir'></div>\n<script>\nconst pir = document.querySelector(\"#pir\")\n\nconst loop = setInterval(()=>{\n    fetch('?check=status')\n    .then((response) => response.text())\n    .then((data) => {\n    pir.innerText = data\n    pir.classList.remove(\"alert\",\"ok\")\n    pir.classList.add(data)\n    document.title = `PIR: ${data}`\n    });\n},1000)\n\n</script>\n</body>\n</html>","output":"str","x":580,"y":480,"wires":[["6049d296d3eb6644"]]},{"id":"7965f2c9dbbd9851","type":"switch","z":"31c1e2adb95d7cd8","name":"","property":"payload","propertyType":"msg","rules":[{"t":"empty"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":390,"y":500,"wires":[["adaee07d9bd7127c"],["08f88c60a105925b"]]},{"id":"08f88c60a105925b","type":"change","z":"31c1e2adb95d7cd8","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"pir","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":600,"y":520,"wires":[["6049d296d3eb6644"]]},{"id":"8fb43a30bf5640d7","type":"inject","z":"31c1e2adb95d7cd8","name":"pir status : alert","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"alert","payloadType":"str","x":280,"y":600,"wires":[["1ba019d9628e257c"]]},{"id":"1ba019d9628e257c","type":"change","z":"31c1e2adb95d7cd8","name":"","rules":[{"t":"set","p":"pir","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":470,"y":620,"wires":[[]]},{"id":"7232224da029e4b4","type":"inject","z":"31c1e2adb95d7cd8","name":"pir status : ok","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"ok","payloadType":"str","x":290,"y":640,"wires":[["1ba019d9628e257c"]]}]

It performs a call every second, updates the title and text on page dynamically using the standard fetch API.
Basically you have 1 endpoint which is reused for checking the status. Update the PIR state and write it to a flow variable.

page is located at /pir


This is quick and dirty. Better solution is to use a websocket and then instead of using fetch, initiate a single websocket client connection that can handle the data instantly.

Thanks @bakman2, that does work when clicking the inject nodes.
But apparently I have yet to understand some very basic Node-RED concepts.
How do I 'replace' the inject nodes with the actual GET request, or how would the request from the ESP have to look like to trigger the 'alert' state in your example?

Then it is even easier:

Or remove the switch+request node all together and edit fetch function in the template node and point it to the url you are looking for.

Thanks @TotallyInformation, at first I didn't realise that uibuilder is a seperate module (me = Newbie :)).
I did the first-timers walkthrough from the docs, but like I said to bakman2, it looks like I haven't grasped the basic concept of Node-RED.
I've played around with the "Quote of the Day" and "send and receive multiple data from node-red to webpage and vice versa" examples from uibuilder: nodes and flows (collection) - Node-RED but I have been unable to adjust them for my project.

Sorry for being so thick, but if the ESP is calling http://nodered:1880/pir?iamesp , what would the properties of the http-request node have to look like to toggle the state to 'alert'?

Have you thought of using MQTT to communicate between the ESP and NR?

Ok i was not following completely, ESP calls node-red, not the other way around.

ESP would call ?setStatus and updates a flow variable.

Retry:

[{"id":"768f2f024f5ef580","type":"http in","z":"31c1e2adb95d7cd8","name":"","url":"/pir","method":"get","upload":false,"swaggerDoc":"","x":140,"y":400,"wires":[["7965f2c9dbbd9851"]]},{"id":"6049d296d3eb6644","type":"http response","z":"31c1e2adb95d7cd8","name":"","statusCode":"","headers":{},"x":1070,"y":360,"wires":[]},{"id":"adaee07d9bd7127c","type":"template","z":"31c1e2adb95d7cd8","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!DOCTYPE html>\n<html>\n    <head>\n        <style>\n        body{font-family:sans-serif;padding:20px}\n        #pir{padding:4px;color:white;padding:8px}\n        .alert{background-color:#f30}\n        .ok{background-color:green}\n        </style>\n    </head>\n<body>\n<div id='pir'></div>\n<script>\nconst pir = document.querySelector(\"#pir\")\n\nconst loop = setInterval(()=>{\n    fetch('?getStatus')\n    .then((response) => response.text())\n    .then((data) => {\n    pir.innerText = data\n    pir.classList.remove(\"alert\",\"ok\")\n    pir.classList.add(data)\n    document.title = `PIR: ${data}`\n    });\n},1000)\n\n</script>\n</body>\n</html>","output":"str","x":480,"y":360,"wires":[["6049d296d3eb6644"]]},{"id":"7965f2c9dbbd9851","type":"switch","z":"31c1e2adb95d7cd8","name":"","property":"payload","propertyType":"msg","rules":[{"t":"empty"},{"t":"hask","v":"getStatus","vt":"str"},{"t":"hask","v":"setStatus","vt":"str"}],"checkall":"true","repair":false,"outputs":3,"x":290,"y":400,"wires":[["adaee07d9bd7127c"],["08f88c60a105925b"],["93d3eee4790d04d4"]]},{"id":"08f88c60a105925b","type":"change","z":"31c1e2adb95d7cd8","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"pir","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":880,"y":400,"wires":[["6049d296d3eb6644"]]},{"id":"de4e92de70bd943c","type":"change","z":"31c1e2adb95d7cd8","name":"","rules":[{"t":"set","p":"pir","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":670,"y":440,"wires":[["08f88c60a105925b"]]},{"id":"93d3eee4790d04d4","type":"switch","z":"31c1e2adb95d7cd8","name":"pir var exists ?","property":"pir","propertyType":"flow","rules":[{"t":"null"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":500,"y":460,"wires":[["de4e92de70bd943c"],["de1674ac9d6bdf91"]]},{"id":"de1674ac9d6bdf91","type":"change","z":"31c1e2adb95d7cd8","name":"flow pir ok","rules":[{"t":"set","p":"pir","pt":"flow","to":"ok","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":670,"y":480,"wires":[["08f88c60a105925b"]]}]

Does the ESP call node-red before it goes back to sleep to end the alert? Then it would require some additional modifications. But first try to digest to see what the flow is doing.

as @zenofmud states; mqtt is a better option instead of http.

Just working on an example - this is incomplete:

OK, so a very simple example to get you going:

[{"id":"0c2e209c97820977","type":"uibuilder","z":"c51f51da7ee02d68","name":"","topic":"","url":"esp-test","fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"templateFolder":"blank","extTemplate":"","showfolder":false,"reload":true,"sourceFolder":"src","deployedVersion":"6.1.0","showMsgUib":false,"x":850,"y":220,"wires":[["2ce8632982fab514"],["79bbf9a8ad263a1f","aad0c61ffd40f302"]]},{"id":"2ce8632982fab514","type":"debug","z":"c51f51da7ee02d68","name":"debug 39","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":985,"y":200,"wires":[],"l":false},{"id":"33730e1ee0c8bd64","type":"http in","z":"c51f51da7ee02d68","name":"","url":"/esp-upd","method":"get","upload":false,"swaggerDoc":"","x":200,"y":160,"wires":[["2471ddd15975e4d1","2be5186355f2c2c0"]]},{"id":"2471ddd15975e4d1","type":"http response","z":"c51f51da7ee02d68","name":"","statusCode":"200","headers":{},"x":380,"y":160,"wires":[]},{"id":"4a266abd843e4796","type":"http request","z":"c51f51da7ee02d68","name":"","method":"GET","ret":"txt","paytoqs":"ignore","url":"http://red.localhost:1880/nr/esp-upd?id=esp001","tls":"","persist":false,"proxy":"","insecureHTTPParser":false,"authType":"","senderr":false,"headers":[],"x":390,"y":40,"wires":[[]]},{"id":"a9bac14328a9ac69","type":"inject","z":"c51f51da7ee02d68","name":"Simulate ESP call","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":210,"y":40,"wires":[["4a266abd843e4796"]]},{"id":"c8838e7fa63011fa","type":"comment","z":"c51f51da7ee02d68","name":"API endpoint for ESP call","info":"","x":230,"y":120,"wires":[]},{"id":"79bbf9a8ad263a1f","type":"debug","z":"c51f51da7ee02d68","name":"debug 40","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1025,"y":240,"wires":[],"l":false},{"id":"2be5186355f2c2c0","type":"change","z":"c51f51da7ee02d68","name":"Filter .req & .res + set topic","rules":[{"t":"delete","p":"req","pt":"msg"},{"t":"delete","p":"res","pt":"msg"},{"t":"set","p":"topic","pt":"msg","to":"msg-from-esp","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":360,"y":220,"wires":[["b18de733e3d7e4d5"]]},{"id":"b18de733e3d7e4d5","type":"uib-cache","z":"c51f51da7ee02d68","cacheall":false,"cacheKey":"topic","newcache":true,"num":1,"storeName":"default","name":"","storeContext":"context","varName":"uib_cache","x":630,"y":220,"wires":[["0c2e209c97820977"]]},{"id":"0cb298630b1e94a6","type":"link in","z":"c51f51da7ee02d68","name":"link in 2","links":["aad0c61ffd40f302"],"x":465,"y":260,"wires":[["b18de733e3d7e4d5"]]},{"id":"aad0c61ffd40f302","type":"link out","z":"c51f51da7ee02d68","name":"link out 9","mode":"link","links":["0cb298630b1e94a6"],"x":985,"y":260,"wires":[]}]

index.html:

<!doctype html>
<html lang="en"><head>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Blank template - Node-RED uibuilder</title>
    <meta name="description" content="Node-RED uibuilder - Blank template">
    <link rel="icon" href="./images/node-blue.ico">

    <!-- Your own CSS (defaults to loading uibuilders css)-->
    <link type="text/css" rel="stylesheet" href="./index.css" media="all">

    <!-- #region Supporting Scripts. These MUST be in the right order. Note no leading / - socket.io no longer needed  -->
    <script defer src="../uibuilder/uibuilder.iife.min.js"></script>
    <script defer src="./index.js">/* OPTIONAL: Put your custom code in that */</script>
    <!-- #endregion -->

</head><body class="uib">
    
    <h1>uibuilder Blank Template - v6.1.0</h1>

    <div id="more"><!-- '#more' is used as a parent for dynamic HTML content in examples --></div>

    <div style="margin-bottom: 2em;">
        ESP 001 Status: <span id="status001"></span>
    </div>


    <!-- Two different ways to send data back to Node-RED via buttons.
         fnSendToNR uses standard `uibuilder.send`.
         eventSend includes `data-*` attributes, keyb modifiers, etc. Works with any event. -->
    <button onclick="fnSendToNR('A message from the sharp end!')">Send a msg back to Node-RED</button>
    <button onclick="uibuilder.eventSend(event)" data-type="eventSend" data-foo="Bah">eventSend</button>

    <pre id="msg" class="syntax-highlight">Waiting for a message from Node-RED</pre>

</body></html>

index.js

// @ts-nocheck

/** The simplest use of uibuilder client library
 * Note that uibuilder.start() should no longer be needed.
 * See the Tech docs if the client doesn't start on its own.
 */
'use strict'

// logLevel 2+ shows more built-in logging. 0=error,1=warn,2=info,3=log,4=debug,5=trace.
// uibuilder.set('logLevel', 2)
// uibuilder.log('info', 'a prefix', 'some info', {any:'data',life:42})

// Helper function to send a message back to Node-RED using the standard send function - see the HTML file for use
window.fnSendToNR = function fnSendToNR(payload) {
    uibuilder.send({
        'topic': 'msg-from-uibuilder-front-end',
        'payload': payload,
    })
}

// Listen for incoming messages from Node-RED
uibuilder.onChange('msg', function(msg) {
    // Dump the msg as text to the "msg" html element
    // either the HTML way or via uibuilder's $ helper function
    // const eMsg = document.getElementById('msg')
    const eMsg = $('#msg')
    if (eMsg) eMsg.innerHTML = uibuilder.syntaxHighlight(msg)

    $('#status001').innerText = `I was alive at ${(new Date).toLocaleTimeString()}!`
})

Note that most of the front-end code is boiler-plate from the "blank" example (actually from the next release of uibuilder but it should work just fine with the live 6.0.0 release).

What it does is to use http-in/-out to create the API endpoint (uibuilder can do that too but this is simpler for this simple case). When called, that endpoint passes some data on to uibuilder where you can either process it in node-red before sending or process it in the front-end - or both. It also uses the uib-cache node to retain the last value sent so that a newly connected browser tab gets that value.

You could easily extend the change node to set the topic to the ESP's id so that the last msg from each ESP device was retained.

Note that it took a lot longer to copy/paste and write this text than it did to create a new uibuilder node with the flow and the front-end code!! :grin:

Thank you very much, I think I can work with that.
However, after replacing index.html and index.js with the code you posted seperately I could no longer see the message in /esp-test, I had to revert.

No, I was planning to have a timer within the javascript part, that would reset the state to 'OK'.

Thanks for the updated flow, although it did not set the Alert state when calling /pir?SetStatus. Maybe a logic error in node 'flow pir ok'?
I changed it like this, which gives kinda the result I was hoping for:

[{"id":"768f2f024f5ef580","type":"http in","z":"3d20c26bc60da1a9","name":"","url":"/pir","method":"get","upload":false,"swaggerDoc":"","x":420,"y":160,"wires":[["7965f2c9dbbd9851"]]},{"id":"6049d296d3eb6644","type":"http response","z":"3d20c26bc60da1a9","name":"","statusCode":"","headers":{},"x":1370,"y":100,"wires":[]},{"id":"adaee07d9bd7127c","type":"template","z":"3d20c26bc60da1a9","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!DOCTYPE html>\n<html>\n    <head>\n        <style>\n        body{font-family:sans-serif;padding:20px}\n        #pir{padding:4px;color:white;padding:8px}\n        .alert{background-color:#f30}\n        .ok{background-color:green}\n        </style>\n    </head>\n<body>\n<div id='pir'></div>\n<script>\nconst pir = document.querySelector(\"#pir\")\n\nconst loop = setInterval(()=>{\n    fetch('?getStatus')\n    .then((response) => response.text())\n    .then((data) => {\n    pir.innerText = data\n    pir.classList.remove(\"alert\",\"ok\")\n    pir.classList.add(data)\n    document.title = `PIR: ${data}`\n    });\n},1000)\n\n</script>\n</body>\n</html>","output":"str","x":780,"y":100,"wires":[["6049d296d3eb6644"]]},{"id":"7965f2c9dbbd9851","type":"switch","z":"3d20c26bc60da1a9","name":"","property":"payload","propertyType":"msg","rules":[{"t":"empty"},{"t":"hask","v":"getStatus","vt":"str"},{"t":"hask","v":"setAlert","vt":"str"},{"t":"hask","v":"setOK","vt":"str"}],"checkall":"true","repair":false,"outputs":4,"x":550,"y":160,"wires":[["adaee07d9bd7127c"],["08f88c60a105925b"],["93d3eee4790d04d4"],["270d5d218d4225d2"]]},{"id":"08f88c60a105925b","type":"change","z":"3d20c26bc60da1a9","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"pir","tot":"flow"}],"action":"","property":"","from":"","to":"","reg":false,"x":1200,"y":140,"wires":[["6049d296d3eb6644"]]},{"id":"de4e92de70bd943c","type":"change","z":"3d20c26bc60da1a9","name":"","rules":[{"t":"set","p":"pir","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":950,"y":180,"wires":[["08f88c60a105925b"]]},{"id":"93d3eee4790d04d4","type":"switch","z":"3d20c26bc60da1a9","name":"pir var exists ?","property":"pir","propertyType":"flow","rules":[{"t":"null"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":740,"y":200,"wires":[["de4e92de70bd943c"],["de1674ac9d6bdf91"]]},{"id":"de1674ac9d6bdf91","type":"change","z":"3d20c26bc60da1a9","name":"flow pir ALERT","rules":[{"t":"set","p":"pir","pt":"flow","to":"alert","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":960,"y":220,"wires":[["08f88c60a105925b"]]},{"id":"c53f0b2d7b07db24","type":"change","z":"3d20c26bc60da1a9","name":"","rules":[{"t":"set","p":"pir","pt":"flow","to":"payload","tot":"msg"}],"action":"","property":"","from":"","to":"","reg":false,"x":950,"y":280,"wires":[["08f88c60a105925b"]]},{"id":"270d5d218d4225d2","type":"switch","z":"3d20c26bc60da1a9","name":"pir var exists ?","property":"pir","propertyType":"flow","rules":[{"t":"null"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":740,"y":300,"wires":[["c53f0b2d7b07db24"],["241a5b5248717547"]]},{"id":"241a5b5248717547","type":"change","z":"3d20c26bc60da1a9","name":"flow pir OK","rules":[{"t":"set","p":"pir","pt":"flow","to":"ok","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":950,"y":320,"wires":[["08f88c60a105925b"]]}]

I'm using the setOK part as a stand-in for said timer function but it might also be useful for doing a manual reset.

As for the MQTT suggestion, I had thought it to be overkill for such a simple task, plus I have no experience with MQTT either. :face_with_spiral_eyes:

You should have seen an error in the browsers dev console - what error were you getting?

When saving the updated index.html in the NR editor the /esp-test page automatically changes to
uibuilder2

It stays exactly that way when calling /esp-upd?id=anything
Saving the updated index.js has no noticable effect.

I'm using Firefox 102.6.0esr and could only find a 'browser console', which gives this message when calling /esp-upd?id=anything

When calling /esp-test it gives an error about the CanvasBlocker extension, but it does that for every web page

Hope this helps!

Helps me but maybe not you. You need to try an in-private (or whatever Firefox calls it) browser session with no extensions loaded because I'm certain that is what is causing those issues.

This topic was automatically closed 60 days after the last reply. New replies are no longer allowed.