Featured Image - Node-Red - DHT11 DHT22 - Custom Weather Station Dashboard

Posted on

by

in

Node-Red – DHT11/DHT22 – Custom Weather Station Dashboard

Introduction

This post will show you how I created my own custom Node-Red dashboard that will display the sensor readings of my DHT22/DHT11 sensor in real-time. The page will asynchronously update itself using Javascript to display the latest sensor reads.

If you want to see a demo of this project in video format then please see below or watch it on my YouTube channel.

Design

Below are the images of how our custom Node-Red Dashboard will look while reading a DHT22/DHT11 sensor in almost real time. To do this project requires some knowledge about HTML/CSS/Javascript and how HTTP works. It also needs some knowledge about Plotly.js in developing graphical charts to display your sensor readings.

The user interface comes in a dark and light theme and is responsive to mobile devices to display the user interface.

Node Red DHT22 DHT11 Custom Dashboard - Dark Theme
Node Red DHT22 DHT11 Custom Dashboard - Light Theme
Node Red DHT22 DHT11 Custom Dashboard - Mobile Dark Theme
Node Red DHT22 DHT11 Custom Dashboard - Mobile Light Theme

Why I created my custom dashboard?

I was looking at the Node-Red Dashboard module and I was able to create my own simple user interface. However, I had some difficulty changing the stylesheet or maybe I am just not well versed yet with Node-Red so I decided to roll on with my own custom solution.

Node-Red is an easy tool to wire up all of the components needed for you to create your own Internet of Things (IoT) solution.

Prerequisites

You should have access to a working Node-Red application. I have used Raspberry Pi with the latest Raspberry Pi OS installed in this post. At the same time, I installed the Node-Red application on the same device.

Related Content:
How to install Node-Red on Raspberry Pi
Setup Raspberry Pi Headless No Monitor

Components/Parts

The following are the components required for this post.

Disclosure: These are affiliate links and I will earn small commissions to support my site when you buy through these links.

Wiring/Schematic

Node-Red - DHT11_DHT22 - Custom Dashboard - Wiring

The image above is the wiring and schematic diagram for this project. You can replace the DHT22 with the DHT11 if that is the sensor you have.

Node-Red Flow

Node Red DHT22 DHT11 Custom Dashboard Flow

The image above is the nodes associated with our Node-Red DHT22/DHT11 Custom Dashboard flow. It consists of the following sections:

  • Web Page flow – This flow will show our custom dashboard page and consists of template nodes with the HMTL/CSS/Javascript code.
  • Asynchronous HTTP endpoint flow – This flow will respond to an HTTP request “/sensorReadings” and will read our DHT22 node and return a JSON response
  • DHT Manual Trigger – This flow will display the current temperature and humidity readings using the Inject and Debug nodes.

The complete code for this project is shown below and is available in my GitHub repository.

[
    {
        "id": "098aa5ee3ff5101c",
        "type": "tab",
        "label": "DHT22 Custom Dashboard",
        "disabled": false,
        "info": "",
        "env": []
    },
    {
        "id": "fbc7583e00774a30",
        "type": "http in",
        "z": "098aa5ee3ff5101c",
        "name": "",
        "url": "/dht22-dashboard",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 380,
        "y": 240,
        "wires": [
            [
                "a5ebc553da1af8c2"
            ]
        ]
    },
    {
        "id": "c314763f331f3aab",
        "type": "template",
        "z": "098aa5ee3ff5101c",
        "name": "HTML",
        "field": "payload",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "mustache",
        "template": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>DHT22 - Node Red - Dashboard</title>\n    <link\n      href=\"https://fonts.googleapis.com/icon?family=Material+Symbols+Sharp\"\n      rel=\"stylesheet\"\n    />\n    <script src=\"https://cdn.plot.ly/plotly-2.16.1.min.js\"></script>\n    <style>{{{payload.style}}}</style>\n  </head>\n  <body>\n    <div class=\"container\">\n      <aside>\n        <div class=\"top\">\n          <div class=\"logo\">\n            <img src=\"images/logo.png\" alt=\"\" />\n            <h2>DONSKYTECH</h2>\n          </div>\n          <div class=\"close\" id=\"close-btn\">\n            <span class=\"material-symbols-sharp\"> close </span>\n          </div>\n        </div>\n        <div class=\"sidebar\">\n          <a href=\"#\" class=\"active\">\n            <span class=\"material-symbols-sharp\"> dashboard </span>\n            <h3>Dashboard</h3>\n          </a>\n        </div>\n      </aside>\n      <main>\n        <h1>Node-Red - DHT22 - Dashboard</h1>\n        <div class=\"insights\">\n          <div class=\"temperature\">\n            <div class=\"middle\">\n              <div class=\"left\">\n                <h3>Temperature</h3>\n                <h1 id=\"temperature\">0 C</h1>\n              </div>\n              <div class=\"icon\">\n                <span class=\"material-symbols-sharp\"> device_thermostat </span>\n              </div>\n            </div>\n          </div>\n          <!-- End of temperature -->\n          <div class=\"humidity\">\n            <div class=\"middle\">\n              <div class=\"left\">\n                <h3>Humidity</h3>\n                <h1 id=\"humidity\">0 %</h1>\n              </div>\n              <div class=\"icon\">\n                <span class=\"material-symbols-sharp\">\n                  humidity_percentage\n                </span>\n              </div>\n            </div>\n          </div>\n          <!-- End of humidity -->\n        </div>\n        <!-- End of Insights -->\n        <div class=\"histories\">\n          <h2>Historical Charts</h2>\n          <div class=\"history-charts\">\n            <div id=\"temperature-history\" class=\"history-divs\"></div>\n            <div id=\"humidity-history\" class=\"history-divs\"></div>\n          </div>\n        </div>\n      </main>\n      <div class=\"right\">\n        <div class=\"top\">\n          <button id=\"menu-btn\">\n            <span class=\"material-symbols-sharp\"> menu </span>\n          </button>\n          <div class=\"theme-toggler\">\n            <span class=\"material-symbols-sharp active\"> light_mode </span>\n            <span class=\"material-symbols-sharp\"> dark_mode </span>\n          </div>\n        </div>\n        <!-- End of top -->\n        <div class=\"gauge-charts\">\n          <h2>Gauge Charts</h2>\n          <div class=\"item\">\n            <div id=\"temperature-gauge\"></div>\n          </div>\n          <div class=\"item\">\n            <div id=\"humidity-gauge\"></div>\n          </div>\n        </div>\n      </div>\n    </div>\n    <script>{{{payload.script}}}</script>\n  </body>\n</html>\n",
        "output": "str",
        "x": 930,
        "y": 380,
        "wires": [
            [
                "045c78d5c132aa86"
            ]
        ]
    },
    {
        "id": "045c78d5c132aa86",
        "type": "http response",
        "z": "098aa5ee3ff5101c",
        "name": "",
        "statusCode": "",
        "headers": {},
        "x": 1070,
        "y": 240,
        "wires": []
    },
    {
        "id": "a5ebc553da1af8c2",
        "type": "template",
        "z": "098aa5ee3ff5101c",
        "name": "Javascript",
        "field": "payload.script",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "mustache",
        "template": "// Target specific HTML items\nconst sideMenu = document.querySelector(\"aside\");\nconst menuBtn = document.querySelector(\"#menu-btn\");\nconst closeBtn = document.querySelector(\"#close-btn\");\nconst themeToggler = document.querySelector(\".theme-toggler\");\n\n// Holds the background color of all chart\nvar chartBGColor = getComputedStyle(document.body).getPropertyValue(\n  \"--chart-background\"\n);\nvar chartFontColor = getComputedStyle(document.body).getPropertyValue(\n  \"--chart-font-color\"\n);\nvar chartAxisColor = getComputedStyle(document.body).getPropertyValue(\n  \"--chart-axis-color\"\n);\n\n/*\n  Event listeners for any HTML click\n*/\nmenuBtn.addEventListener(\"click\", () => {\n  sideMenu.style.display = \"block\";\n});\n\ncloseBtn.addEventListener(\"click\", () => {\n  sideMenu.style.display = \"none\";\n});\n\nthemeToggler.addEventListener(\"click\", () => {\n  document.body.classList.toggle(\"dark-theme-variables\");\n  themeToggler.querySelector(\"span:nth-child(1)\").classList.toggle(\"active\");\n  themeToggler.querySelector(\"span:nth-child(2)\").classList.toggle(\"active\");\n\n  // Update Chart background\n  chartBGColor = getComputedStyle(document.body).getPropertyValue(\n    \"--chart-background\"\n  );\n  chartFontColor = getComputedStyle(document.body).getPropertyValue(\n    \"--chart-font-color\"\n  );\n  chartAxisColor = getComputedStyle(document.body).getPropertyValue(\n    \"--chart-axis-color\"\n  );\n  updateChartsBackground();\n});\n\n/*\n  Plotly.js graph and chart setup code\n*/\nvar temperatureHistoryDiv = document.getElementById(\"temperature-history\");\nvar humidityHistoryDiv = document.getElementById(\"humidity-history\");\n\nvar temperatureGaugeDiv = document.getElementById(\"temperature-gauge\");\nvar humidityGaugeDiv = document.getElementById(\"humidity-gauge\");\n\nconst historyCharts = [temperatureHistoryDiv, humidityHistoryDiv];\n\nconst gaugeCharts = [temperatureGaugeDiv, humidityGaugeDiv];\n\n// History Data\nvar temperatureTrace = {\n  x: [],\n  y: [],\n  name: \"Temperature\",\n  mode: \"lines+markers\",\n  type: \"line\",\n};\nvar humidityTrace = {\n  x: [],\n  y: [],\n  name: \"Humidity\",\n  mode: \"lines+markers\",\n  type: \"line\",\n};\n\nvar temperatureLayout = {\n  autosize: true,\n  title: {\n    text: \"Temperature\",\n  },\n  font: {\n    size: 12,\n    color: chartFontColor,\n    family: \"poppins, san-serif\",\n  },\n  colorway: [\"#05AD86\"],\n  margin: { t: 40, b: 40, l: 60, r: 60, pad: 10 },\n  plot_bgcolor: chartBGColor,\n  paper_bgcolor: chartBGColor,\n  xaxis: {\n    color: chartAxisColor,\n    linecolor: chartAxisColor,\n    gridwidth: \"2\",\n    autorange: true,\n  },\n  yaxis: {\n    color: chartAxisColor,\n    linecolor: chartAxisColor,\n    gridwidth: \"2\",\n    autorange: true,\n  },\n};\nvar humidityLayout = {\n  autosize: true,\n  title: {\n    text: \"Humidity\",\n  },\n  font: {\n    size: 12,\n    color: chartFontColor,\n    family: \"poppins, san-serif\",\n  },\n  colorway: [\"#05AD86\"],\n  margin: { t: 40, b: 40, l: 30, r: 30, pad: 0 },\n  plot_bgcolor: chartBGColor,\n  paper_bgcolor: chartBGColor,\n  xaxis: {\n    color: chartAxisColor,\n    linecolor: chartAxisColor,\n    gridwidth: \"2\",\n  },\n  yaxis: {\n    color: chartAxisColor,\n    linecolor: chartAxisColor,\n  },\n};\n\nvar config = { responsive: true };\n\nwindow.addEventListener(\"load\", (event) => {\n  Plotly.newPlot(\n    temperatureHistoryDiv,\n    [temperatureTrace],\n    temperatureLayout,\n    config\n  );\n  Plotly.newPlot(humidityHistoryDiv, [humidityTrace], humidityLayout, config);\n\n  // Run it initially\n  handleDeviceChange(mediaQuery);\n});\n\n// Gauge Data\nvar temperatureData = [\n  {\n    domain: { x: [0, 1], y: [0, 1] },\n    value: 0,\n    title: { text: \"Temperature\" },\n    type: \"indicator\",\n    mode: \"gauge+number+delta\",\n    delta: { reference: 30 },\n    gauge: {\n      axis: { range: [null, 50] },\n      steps: [\n        { range: [0, 20], color: \"lightgray\" },\n        { range: [20, 30], color: \"gray\" },\n      ],\n      threshold: {\n        line: { color: \"red\", width: 4 },\n        thickness: 0.75,\n        value: 30,\n      },\n    },\n  },\n];\n\nvar humidityData = [\n  {\n    domain: { x: [0, 1], y: [0, 1] },\n    value: 0,\n    title: { text: \"Humidity\" },\n    type: \"indicator\",\n    mode: \"gauge+number+delta\",\n    delta: { reference: 50 },\n    gauge: {\n      axis: { range: [null, 100] },\n      steps: [\n        { range: [0, 20], color: \"lightgray\" },\n        { range: [20, 30], color: \"gray\" },\n      ],\n      threshold: {\n        line: { color: \"red\", width: 4 },\n        thickness: 0.75,\n        value: 30,\n      },\n    },\n  },\n];\n\nvar layout = { width: 300, height: 250, margin: { t: 0, b: 0, l: 0, r: 0 } };\n\nPlotly.newPlot(temperatureGaugeDiv, temperatureData, layout);\nPlotly.newPlot(humidityGaugeDiv, humidityData, layout);\n\n// Will hold the arrays we receive from our DHT22 sensor\n// Temperature\nlet newTempXArray = [];\nlet newTempYArray = [];\n// Humidity\nlet newHumidityXArray = [];\nlet newHumidityYArray = [];\n\n// The maximum number of data points displayed on our scatter/line graph\nlet MAX_GRAPH_POINTS = 12;\nlet ctr = 0;\n\n// Callback function that will retrieve our latest sensor readings and redraw our Gauge with the latest readings\nfunction updateSensorReadings(jsonResponse) {\n  console.log(typeof jsonResponse);\n  console.log(jsonResponse);\n\n  let temperature = Number(jsonResponse.temperature).toFixed(2);\n  let humidity = Number(jsonResponse.humidity).toFixed(2);\n\n  updateBoxes(temperature, humidity);\n\n  updateGauge(temperature, humidity);\n\n  // Update Temperature Line Chart\n  updateCharts(\n    temperatureHistoryDiv,\n    newTempXArray,\n    newTempYArray,\n    temperature\n  );\n  // Update Humidity Line Chart\n  updateCharts(\n    humidityHistoryDiv,\n    newHumidityXArray,\n    newHumidityYArray,\n    humidity\n  );\n}\n\nfunction updateBoxes(temperature, humidity) {\n  let temperatureDiv = document.getElementById(\"temperature\");\n  let humidityDiv = document.getElementById(\"humidity\");\n\n  temperatureDiv.innerHTML = temperature + \" C\";\n  humidityDiv.innerHTML = humidity + \" %\";\n}\n\nfunction updateGauge(temperature, humidity) {\n  var temperature_update = {\n    value: temperature,\n  };\n  var humidity_update = {\n    value: humidity,\n  };\n\n  Plotly.update(temperatureGaugeDiv, temperature_update);\n  Plotly.update(humidityGaugeDiv, humidity_update);\n}\n\nfunction updateCharts(lineChartDiv, xArray, yArray, sensorRead) {\n  if (xArray.length >= MAX_GRAPH_POINTS) {\n    xArray.shift();\n  }\n  if (yArray.length >= MAX_GRAPH_POINTS) {\n    yArray.shift();\n  }\n  xArray.push(ctr++);\n  yArray.push(sensorRead);\n\n  var data_update = {\n    x: [xArray],\n    y: [yArray],\n  };\n\n  Plotly.update(lineChartDiv, data_update);\n}\n\nfunction updateChartsBackground() {\n  // updates the background color of historical charts\n  var updateHistory = {\n    plot_bgcolor: chartBGColor,\n    paper_bgcolor: chartBGColor,\n    font: {\n      color: chartFontColor,\n    },\n    xaxis: {\n      color: chartAxisColor,\n      linecolor: chartAxisColor,\n    },\n    yaxis: {\n      color: chartAxisColor,\n      linecolor: chartAxisColor,\n    },\n  };\n  historyCharts.forEach((chart) => Plotly.relayout(chart, updateHistory));\n\n  // updates the background color of gauge charts\n  var gaugeHistory = {\n    plot_bgcolor: chartBGColor,\n    paper_bgcolor: chartBGColor,\n    font: {\n      color: chartFontColor,\n    },\n    xaxis: {\n      color: chartAxisColor,\n      linecolor: chartAxisColor,\n    },\n    yaxis: {\n      color: chartAxisColor,\n      linecolor: chartAxisColor,\n    },\n  };\n  gaugeCharts.forEach((chart) => Plotly.relayout(chart, gaugeHistory));\n}\n\nconst mediaQuery = window.matchMedia(\"(max-width: 600px)\");\n\nmediaQuery.addEventListener(\"change\", function (e) {\n  handleDeviceChange(e);\n});\n\nfunction handleDeviceChange(e) {\n  if (e.matches) {\n    console.log(\"Inside Mobile\");\n    var updateHistory = {\n      width: 323,\n      height: 250,\n      \"xaxis.autorange\": true,\n      \"yaxis.autorange\": true,\n    };\n    historyCharts.forEach((chart) => Plotly.relayout(chart, updateHistory));\n  } else {\n    var updateHistory = {\n      width: 550,\n      height: 260,\n      \"xaxis.autorange\": true,\n      \"yaxis.autorange\": true,\n    };\n    historyCharts.forEach((chart) => Plotly.relayout(chart, updateHistory));\n  }\n}\n\nfunction retrieveSensorReadings() {\n  fetch(`/sensorReadings`)\n    .then((response) => response.json())\n    .then((jsonResponse) => {\n      updateSensorReadings(jsonResponse);\n    });\n}\n\n// Continuos loop that runs evry 3 seconds to update our web page with the latest sensor readings\n(function loop() {\n  setTimeout(() => {\n    retrieveSensorReadings();\n    loop();\n  }, 3000);\n})();\n",
        "output": "str",
        "x": 600,
        "y": 380,
        "wires": [
            [
                "485bb7dcb66de76f"
            ]
        ]
    },
    {
        "id": "b5136629e78f3932",
        "type": "http in",
        "z": "098aa5ee3ff5101c",
        "name": "",
        "url": "/sensorReadings",
        "method": "get",
        "upload": false,
        "swaggerDoc": "",
        "x": 420,
        "y": 580,
        "wires": [
            [
                "94dfa9d81e3397dd"
            ]
        ]
    },
    {
        "id": "26a53cd14d224d8d",
        "type": "function",
        "z": "098aa5ee3ff5101c",
        "name": "Get Temperature Readings",
        "func": "msg.payload = { temperature: msg.payload, humidity: msg.humidity};\nreturn msg;",
        "outputs": 1,
        "noerr": 0,
        "initialize": "",
        "finalize": "",
        "libs": [],
        "x": 840,
        "y": 580,
        "wires": [
            [
                "f46a7d5c48f657d9"
            ]
        ]
    },
    {
        "id": "6692713377943b0b",
        "type": "http response",
        "z": "098aa5ee3ff5101c",
        "name": "",
        "statusCode": "200",
        "headers": {},
        "x": 1240,
        "y": 580,
        "wires": []
    },
    {
        "id": "f46a7d5c48f657d9",
        "type": "change",
        "z": "098aa5ee3ff5101c",
        "name": "Set Headers",
        "rules": [
            {
                "t": "set",
                "p": "headers",
                "pt": "msg",
                "to": "{}",
                "tot": "json"
            },
            {
                "t": "set",
                "p": "headers.content-type",
                "pt": "msg",
                "to": "application/json",
                "tot": "str"
            }
        ],
        "action": "",
        "property": "",
        "from": "",
        "to": "",
        "reg": false,
        "x": 1050,
        "y": 480,
        "wires": [
            [
                "6692713377943b0b"
            ]
        ]
    },
    {
        "id": "485bb7dcb66de76f",
        "type": "template",
        "z": "098aa5ee3ff5101c",
        "name": "CSS",
        "field": "payload.style",
        "fieldType": "msg",
        "format": "handlebars",
        "syntax": "mustache",
        "template": "@import url(\"https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700;800&display=swap\");\n\n:root {\n  --color-primary: #7380ec;\n  --color-danger: #ff7782;\n  --color-success: #41f1b6;\n  --color-warning: #ffbb55;\n  --color-white: #fff;\n  --color-info-dark: #7d8da1;\n  --color-info-light: #dce1eb;\n  --color-dark: #363949;\n  --color-light: rgba(132, 139, 200, 0.18);\n  --color-primary-variant: #111e88;\n  --color-dark-variant: #677483;\n  --color-background: #f6f6f9;\n\n  --color-insight-1: rgb(99, 209, 35);\n  --color-insight-2: rgb(233, 245, 59);\n  --color-insight-3: rgb(204, 52, 67);\n  --color-insight-4: rgb(56, 183, 238);\n\n  --card-border-radius: 2rem;\n  --border-radius-1: 0.4rem;\n  --border-radius-2: 0.8rem;\n  --border-radius-3: 1.2rem;\n\n  --card-padding: 1.8rem;\n  --padding-1: 1.2rem;\n\n  --box-shadow: 0 2rem 3rem var(--color-light);\n\n  /* Plotly Chart Color */\n  --chart-background: #fff;\n  --chart-font-color: #444;\n  --chart-axis-color: #444;\n}\n/* Dark Theme Variables */\n.dark-theme-variables {\n  --color-background: #090d3e;\n  --color-white: #0b0f4a;\n  --color-primary: #fff;\n  --color-dark: #edeffd;\n  --color-dark-variant: #fff;\n  --color-light: rgba(0, 0, 0, 0.4);\n  --box-shadow: 0 2rem 3rem var(--color-light);\n\n  --chart-background: #0d1256;\n  --chart-font-color: #fff;\n  --chart-axis-color: #fff;\n}\n\n* {\n  margin: 0;\n  padding: 0;\n  outline: 0;\n  appearance: none;\n  text-decoration: none;\n  list-style: none;\n  box-sizing: border-box;\n}\n\nhtml {\n  font-size: 14px;\n}\nbody {\n  width: 100vw;\n  height: 100vh;\n  font-family: poppins, san-serif;\n  font-size: 0.88rem;\n  background: var(--color-background);\n  user-select: none;\n  overflow-x: hidden;\n  color: var(--color-dark-variant);\n}\n.container {\n  display: grid;\n  width: 96%;\n  margin: 0 auto;\n  gap: 1.8rem;\n  grid-template-columns: 14rem auto 30rem;\n}\na {\n  color: var(--color-dark);\n}\nimg {\n  display: block;\n  width: 100%;\n}\nh1 {\n  font-weight: 800;\n  font-size: 1.8rem;\n}\nh2 {\n  font-size: 1.4rem;\n}\nh3 {\n  font-size: 0.87rem;\n}\nh4 {\n  font-size: 0.8rem;\n}\nh5 {\n  font-size: 0.77rem;\n}\nsmall {\n  font-size: 0.75rem;\n}\n.profile-photo {\n  width: 2.8rem;\n  height: 2.8rem;\n  border-radius: 50%;\n  overflow: hidden;\n}\n.text-muted {\n  color: var(--color-info-light);\n}\np {\n  color: var(--color-dark-variant);\n}\nb {\n  color: var(--color-dark-variant);\n}\n.primary {\n  color: var(--color-primary);\n}\n.danger {\n  color: var(--color-danger);\n}\n.success {\n  color: var(--color-success);\n}\n.warning {\n  color: var(--color-warning);\n}\n\n/***** Sidebar Image*****/\naside {\n  height: 100vh;\n}\naside .top {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n  margin-top: 1.4rem;\n}\naside .logo {\n  display: flex;\n  gap: 0.8rem;\n}\naside .logo img {\n  width: 2rem;\n  height: 2re;\n}\naside .close {\n  display: none;\n}\n/***** Sidebar Links*****/\naside .sidebar {\n  display: flex;\n  flex-direction: column;\n  height: 86vh;\n  position: relative;\n  top: 3rem;\n}\naside h3 {\n  font-weight: 500;\n}\n\naside .sidebar a {\n  display: flex;\n  color: var(--color-info-dark);\n  margin-left: 2rem;\n  gap: 1rem;\n  align-items: center;\n  position: relative;\n  height: 3.7rem;\n  transition: all 300ms ease;\n}\n\naside .sidebar a span {\n  font-size: 1.6rem;\n  transition: all 300ms ease;\n}\n\n/* aside .sidebar a:last-child {\n  position: absolute;\n  bottom: 2rem;\n  width: 100%;\n} */\n\naside .sidebar a.active {\n  background: var(--color-light);\n  color: var(--color-primary);\n  margin-left: 0;\n}\naside .sidebar a.active:before {\n  content: \"\";\n  width: 6px;\n  height: 100%;\n  background: var(--color-primary);\n}\naside .sidebar a.active span {\n  color: var(--color-primary);\n  margin-left: calc(1rem - 6px);\n}\naside .sidebar a:hover {\n  color: var(--color-primary);\n}\n\naside .sidebar a:hover span {\n  margin-left: 1rem;\n}\n\n/************* main ******************/\nmain {\n  margin-top: 1.4rem;\n}\n\nmain .insights {\n  display: grid;\n  grid-template-columns: repeat(2, 1fr);\n  gap: 1.6rem;\n}\nmain .insights > div {\n  background: var(--color-white);\n  padding: var(--card-padding);\n  border-radius: var(--card-border-radius);\n  margin-top: 1rem;\n  box-shadow: var(--box-shadow);\n  transition: all 300ms ease;\n}\n\nmain .insights > div:hover {\n  box-shadow: none;\n}\n\nmain .insights > div span {\n  background: var(--color-primary);\n  padding: 0.5rem;\n  border-radius: 50%;\n  color: var(--color-white);\n  font-size: 2rem;\n}\n\nmain .insights > div.temperature span {\n  background: var(--color-insight-1);\n}\n\nmain .insights > div.humidity span {\n  background: var(--color-insight-2);\n}\n\nmain .insights > div.pressure span {\n  background: var(--color-insight-3);\n}\nmain .insights > div.altitude span {\n  background: var(--color-insight-4);\n}\n\nmain .insights > div .middle {\n  display: flex;\n  align-items: center;\n  justify-content: space-between;\n}\n\nmain .insights h3 {\n  margin: 1rem 0 0.6rem;\n  font-size: 1rem;\n}\n\n/************* End of Insights ******************/\nmain .histories {\n  margin-top: 2rem;\n}\n\nmain .history-charts {\n  display: grid;\n  grid-template-columns: repeat(2, 1fr);\n  gap: 2.5rem;\n  background: var(--color-white);\n  border-radius: var(--border-radius-1);\n  padding: var(--card-padding);\n  text-align: center;\n  box-shadow: var(--box-shadow);\n}\nmain .history-charts:hover {\n  box-shadow: none;\n}\n\nmain .history-charts .history-divs {\n  text-align: center;\n}\n\nmain .histories h2 {\n  margin-bottom: 0.8rem;\n}\n\n/* ********RIGHT ********** */\n.right {\n  margin-top: 1.4rem;\n}\n.right .top {\n  display: flex;\n  justify-content: end;\n  gap: 2rem;\n}\n.right .top button {\n  display: none;\n}\n\n.right .theme-toggler {\n  background: var(--color-light);\n  display: flex;\n  justify-content: space-between;\n  align-items: center;\n  height: 1.6rem;\n  width: 4.2rem;\n  cursor: pointer;\n  border-radius: var(--border-radius-1);\n}\n\n.right .theme-toggler span {\n  font-size: 1.2rem;\n  width: 50%;\n  height: 100%;\n  display: flex;\n  align-items: center;\n  justify-content: center;\n}\n\n.right .theme-toggler span.active {\n  background: var(--color-primary);\n  color: white;\n  border-radius: var(--border-radius-1);\n}\n\n/* GAUGE CHARTS */\n.right .gauge-charts {\n  margin-top: 2rem;\n}\n.right .gauge-charts .item {\n  background: var(--color-white);\n  display: flex;\n  align-items: center;\n  justify-content: center;\n  gap: 1rem;\n  margin-bottom: 0.7rem;\n  padding: 1.4rem var(--card-padding);\n  border-radius: var(--border-radius-3);\n  box-shadow: var(--box-shadow);\n  transition: all 300ms ease;\n}\n.right .gauge-charts .item:hover {\n  box-shadow: none;\n}\n\n.right .gauge-charts .item .right {\n  display: flex;\n  justify-content: space-between;\n  align-items: start;\n  margin: 0;\n  width: 100%;\n}\n\n.right .gauge-charts .item .icon {\n  padding: 0.6rem;\n  color: var(--color-white);\n  border-radius: 50%;\n  background: var(--color-primary);\n  display: flex;\n}\n\n.right .gauge-charts .item.offline .icon {\n  background: var(--color-danger);\n}\n\n/* MEDIA QUERIES */\n@media screen and (max-width: 1200px) {\n  .container {\n    width: 94%;\n    grid-template-columns: 7rem auto 23rem;\n  }\n  aside .logo h2 {\n    display: none;\n  }\n\n  aside .sidebar h3 {\n    display: none;\n  }\n  aside .sidebar a {\n    width: 5.6rem;\n  }\n  aside .sidebar a:last-child {\n    position: relative;\n    margin-top: 1.8rem;\n  }\n  main .insights {\n    grid-template-columns: 1fr;\n  }\n  main .histories {\n    width: 94%;\n    position: absolute;\n    left: 50%;\n    transform: translateX(-50%);\n    margin: 2rem 0 0 8.8rem;\n  }\n  main .histories .history-charts {\n    grid-template-columns: 1fr;\n    width: 54vw;\n  }\n}\n\n@media only screen and (max-width: 992px) {\n  .container {\n    width: 94%;\n    grid-template-columns: 12rem auto 23rem;\n  }\n  main .insights {\n    grid-template-columns: repeat(2, 1fr);\n    gap: 1.6rem;\n  }\n  main .histories .history-charts {\n    grid-template-columns: 1fr;\n    align-items: center;\n    justify-content: center;\n  }\n}\n\n@media screen and (max-width: 768px) {\n  .container {\n    width: 100%;\n    grid-template-columns: 1fr;\n    /* height: 100vh; */\n  }\n  aside {\n    position: fixed;\n    left: -100%;\n    background: var(--color-white);\n    width: 18rem;\n    z-index: 3;\n    box-shadow: 1rem 3rem 4rem var(--color-light);\n    height: 100vh;\n    padding-right: var(--card-padding);\n    display: none;\n    animation: showMenu 400ms ease forwards;\n  }\n  @keyframes showMenu {\n    to {\n      left: 0;\n    }\n  }\n  aside .logo {\n    margin-left: 1rem;\n  }\n  aside .logo h2 {\n    display: inline;\n  }\n  aside .sidebar h3 {\n    display: inline;\n  }\n  aside .sidebar a {\n    width: 100%;\n    height: 3.4rem;\n  }\n  /* aside .sidebar a:last-child {\n    position: absolute;\n    bottom: 5rem;\n  } */\n  aside .close {\n    display: inline-block;\n    cursor: pointer;\n  }\n  main {\n    margin: 8rem 2rem 2rem 2rem;\n    padding: 0 1rem;\n  }\n  main .histories {\n    position: relative;\n    margin: 3rem 0 0 0;\n    width: 100%;\n  }\n  main .histories .history-charts {\n    width: 100%;\n    justify-content: center;\n    align-items: center;\n    display: flex;\n    flex-direction: column;\n    justify-content: center;\n    align-items: center;\n  }\n  .right {\n    width: 90%;\n    margin: 0 auto 0rem auto;\n  }\n  .right .top {\n    position: fixed;\n    top: 0;\n    left: 0;\n    align-items: center;\n    padding: 0 0.8rem;\n    height: 4.6rem;\n    background: var(--color-white);\n    width: 100%;\n    margin: 0;\n    z-index: 2;\n    box-shadow: 0 1rem 1 rem var(--color-light);\n  }\n  .right .top .theme-toggler {\n    width: 4.4rem;\n    position: absolute;\n    right: 2rem;\n  }\n  .right .profile .info {\n    display: none;\n  }\n\n  .right .top button {\n    display: inline-block;\n    background: transparent;\n    cursor: pointer;\n    color: var(--color-dark);\n    position: absolute;\n    left: 1rem;\n  }\n  .right .top button span {\n    font-size: 2rem;\n  }\n}\n\n@media screen and (max-width: 600px) {\n  .container {\n    width: 100%;\n    grid-template-columns: 1fr;\n    margin: 1rem 0 1rem 0;\n  }\n  main {\n    margin: 5rem 1rem 1rem 1rem;\n    padding: 0 1rem;\n    width: 90vw;\n  }\n\n  main .insights {\n    gap: 0.4rem;\n  }\n  main .insights > div {\n    padding: 0.4rem;\n  }\n\n  main .history-charts {\n    display: grid;\n    grid-template-columns: 1fr;\n  }\n}\n",
        "output": "str",
        "x": 750,
        "y": 240,
        "wires": [
            [
                "c314763f331f3aab"
            ]
        ]
    },
    {
        "id": "1739fade323b0f46",
        "type": "rpi-dht22",
        "z": "098aa5ee3ff5101c",
        "name": "DHT22",
        "topic": "rpi-dht22",
        "dht": 22,
        "pintype": "0",
        "pin": 4,
        "x": 680,
        "y": 800,
        "wires": [
            [
                "8059c7171b64d953"
            ]
        ]
    },
    {
        "id": "8059c7171b64d953",
        "type": "debug",
        "z": "098aa5ee3ff5101c",
        "name": "debug 1",
        "active": true,
        "tosidebar": true,
        "console": false,
        "tostatus": false,
        "complete": "true",
        "targetType": "full",
        "statusVal": "",
        "statusType": "auto",
        "x": 900,
        "y": 800,
        "wires": []
    },
    {
        "id": "838964f36923c827",
        "type": "inject",
        "z": "098aa5ee3ff5101c",
        "name": "",
        "props": [
            {
                "p": "payload"
            },
            {
                "p": "topic",
                "vt": "str"
            }
        ],
        "repeat": "",
        "crontab": "",
        "once": false,
        "onceDelay": 0.1,
        "topic": "",
        "payload": "",
        "payloadType": "date",
        "x": 400,
        "y": 800,
        "wires": [
            [
                "1739fade323b0f46"
            ]
        ]
    },
    {
        "id": "94dfa9d81e3397dd",
        "type": "rpi-dht22",
        "z": "098aa5ee3ff5101c",
        "name": "",
        "topic": "DHT22",
        "dht": 22,
        "pintype": "0",
        "pin": 4,
        "x": 600,
        "y": 480,
        "wires": [
            [
                "26a53cd14d224d8d"
            ]
        ]
    },
    {
        "id": "4917ebb7a67b8cba",
        "type": "comment",
        "z": "098aa5ee3ff5101c",
        "name": "Web Page Flow",
        "info": "",
        "x": 540,
        "y": 200,
        "wires": []
    },
    {
        "id": "a39823b5b88a343f",
        "type": "comment",
        "z": "098aa5ee3ff5101c",
        "name": "Asynchronous Update - HTTP end point",
        "info": "",
        "x": 350,
        "y": 520,
        "wires": []
    },
    {
        "id": "0ffb8e91d747b7fb",
        "type": "comment",
        "z": "098aa5ee3ff5101c",
        "name": "DHT Manual trigger",
        "info": "",
        "x": 490,
        "y": 760,
        "wires": []
    }
]

Let us go through how I configured each node in our custom Node-Red DHT22/DHT11 dashboard flow.

Web Page flow

Node Red Web Page Flow

Let us go thru the configurations that I did.

Node-Red HTTP In Dashboard Node

This is our network HTTP In node where I configured the root of our application as “/dht22-dashboard

Node-Red Javascript Template

The Javascript template node is where we put our Javascript code and sets the property msg.payload.scripts

Node-Red CSS Template

The CSS template node is where we defined our CSS styling for our web page.

Node-Red HTML Template

The HTML node is where we defined our HTML page and import the CSS and Javascript using the Mustache template.

The last node which is the HTTP response node terminates this flow and returns the content to the user which in this case is our index.html page.

What about the HTML/CSS/Javascript code?

The code for the HTML/CSS/Javascript is taken from one of the projects I have worked on about How to build your own custom MQTT dashboard. I am using MQTT there but you can remove the code that connects to the MQTT broker and replace the code with an HTTP Request. I have explained what each line of the code does.

This code is available also in my GitHub repository.

Asynchronous HTTP endpoint flow

Asynchronous HTTP Endpoint Flow

This is the flow that periodically responds to our request for sensor readings.

Node Red HTTP In Sensor Readings

The URL maps to /sensorReadings with an HTTP GET method. This is mapped to the Javascript code below.

function retrieveSensorReadings() {
  fetch(`/sensorReadings`)
    .then((response) => response.json())
    .then((jsonResponse) => {
      updateSensorReadings(jsonResponse);
    });
}

// Continuos loop that runs evry 3 seconds to update our web page with the latest sensor readings
(function loop() {
  setTimeout(() => {
    retrieveSensorReadings();
    loop();
  }, 3000);
})();

Let us move on to the other part of this flow.

Node Red DHT22 Node

The DHT22 node is used to retrieve the sensor readings from our sensor. I have mapped it to the GPIO4 of my Raspberry Pi.

Node Red Function Node - JSON Response

This is where we mapped our payload to a JSON object by reading the DHT22 message.

Node Red Set JSON Response

We set the HTTP response to application/JSON and terminate the flow with an HTTP response node.

DHT Manual Trigger

Node Red DHT22 Manual Trigger

This is the manual flow of reading the DHT22/DHT11 sensor using the inject and debug node.

How to run the project?

Make sure that the DHT22/DHT11 node is added by installing it in the Manage Palette menu. Create a new flow and import the custom DHT22/DHT11 dashboard flow outline above.

Access the application by using the below URL. Change the IP address to that of your Node-Red IP or DNS name.

http://192.168.100.121:1880/dht22-dashboard

Wrap Up

We have successfully created our own custom Node-Red dashboard solution in this post.

I hope you learned something! Happy Exploring!

If you like my post then please consider sharing this. Thanks!

2 responses to “Node-Red – DHT11/DHT22 – Custom Weather Station Dashboard”

  1. Benny Riglos Avatar
    Benny Riglos

    Hi Sir,

    Nice!

    I would like to touch base with you to collab on my Automation project using Node-Red. From the tone and accent of you voice are a Pinoy? I am from PH here in Cabuyao, Laguna.

    My mobile (Globe) please txt me back (txt po muna Sir with message Node-Red)

    1. donsky Avatar
      donsky

      Send me a message on my Facebook page just search for donskytech. Yes Pinoy na Pinoy po! 🙂

Leave a Reply

Your email address will not be published. Required fields are marked *