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

Hey,
I just started reading about Node-Red yesterday and I was just amazed at how easy it is to develop your own custom user interface using this tool and have everything running.

Today, I was able to develop my first simple IoT project which is a Custom DHT22/DHT11 Weather Station that automatically updates itself using a simple Fetch API. The graphs are driven by the Plotly.js library and I want the user interface to be responsive even on mobile.

I only had some issues with the Node-Red-Dashboard module as I am not able to get the styling that I want but I was able to display a simple user interface. Maybe I am just a newbie so that is why I am struggling so I just thought of rolling up my own custom dashboard solution.

I documented this in my personal blog including the video demo if you would like to take a look. I also would like to ask the experts if there are some things that I could improve in my project or if I might have done something wrong, especially the flow stuff. I am all ears.

The flow.json is here:

[
    {
        "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": []
    }
]

The Node-Red project documentation was superb and easy to follow.

The next challenge is using the WebSocket and MQTT tomorrow.

Thank you for coming up with this project I am excited to learn more and use this tool.


5 Likes

Welcome @donskytech, glad you are enjoying Node-RED.

Don't know if you have seen them, but there are some very good videos - Node-RED Essentials.

From personal experience, some nice gauges can be found using the Hamburger Menu | Palette Manager - search gauges.

There are also a couple of Dashboard Nodes currently being developed Flexdash and uibuilder which allow Custom Dashboards to be developed.

1 Like