NowCast AQI for PM2.5

This with calculates proper AQI and Nowcast-AQI from PM2.5 sensors readings.

Expect a feed of PM2.5 sensor readings, that are stored in a queue, each hour, we take the average of these values and store in an hour array.

We then compute AQI and NowCast-AQI for 24, 12, 6, and 3 hours, and send them out as MQTT data and in JSON format.

References:


                US EPA AQI Calculation Method 
                https://forum.airnowtech.org/t/daily-and-hourly-aqi-pm2-5/171
                https://github.com/ThangLeQuoc/aqi-bot
                https://www3.epa.gov/airnow/aqicalctest/nowcast.htm
                https://www3.epa.gov/airnow/aqi-technical-assistance-document-sept2018.pdf
[{"id":"727f8e6b8a5480e5","type":"tab","label":"PM2.5 AQI","disabled":false,"info":""},{"id":"3cede9b7d787649c","type":"mqtt in","z":"727f8e6b8a5480e5","name":"TTN","topic":"my-topic","qos":"0","datatype":"auto","broker":"f58040f5.4d815","nl":false,"rap":false,"x":150,"y":220,"wires":[["36b38059fb3d3a45","1f23082c089fd39b","1fa49afeed5b107f"]]},{"id":"36b38059fb3d3a45","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":450,"y":100,"wires":[]},{"id":"1f23082c089fd39b","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"topic","x":440,"y":140,"wires":[]},{"id":"1fa49afeed5b107f","type":"function","z":"727f8e6b8a5480e5","name":"TTN LS-113 PM2.5 Decrypt RSF","func":"\n\n// msgx, we test for correct device\nvar msgx = { payload: msg.payload.length };\nmsgx.payload = JSON.parse(msg.payload);\n\nif ( msgx.payload.end_device_ids.device_id == \"ls-113-rsf-1\")\n{\n    // msg1 - PM2.5 PPM\n    var msg1 = {};\n    msg1.payload = msgx.payload.uplink_message.decoded_payload.ppm;\n    msg1.payload = msg1.payload.toFixed(0);\n    msg1.topic   = \"put\";\n    node.send (msg1);\n\n}\n\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":540,"y":220,"wires":[["69bd4c7a04c603dd","0463ab3e28215bf9","660eb6dd1ba3f1f2"]]},{"id":"69bd4c7a04c603dd","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1070,"y":220,"wires":[]},{"id":"0463ab3e28215bf9","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"topic","x":1060,"y":260,"wires":[]},{"id":"fa3fe95c48431ae4","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":1070,"y":400,"wires":[]},{"id":"660eb6dd1ba3f1f2","type":"function","z":"727f8e6b8a5480e5","name":"Gather PM2.5 Data","func":"/*******************************************************************************\n\n  This is free software; you can redistribute it and/or\n  modify it under the terms of the GNU Lesser General Public\n  License as published by the Free Software Foundation; either\n  version 2.1 of the License, or (at your option) any later version.\n\n  This is distributed in the hope that it will be useful,\n  but WITHOUT ANY WARRANTY; without even the implied warranty of\n  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n  Lesser General Public License for more details.\n\n  You should have received a copy of the GNU Lesser General Public\n  License along with this library; if not, write to the Free Software\n  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA\n\n  [email protected]\n\n * *****************************************************************************\n\n    CHANGE LOG:\n\n    DATE         REV  DESCRIPTION\n    -----------  ---  ----------------------------------------------------------\n    01-Aug-2021  1.0  TRL - First Build   \n    07-Sep-2021  1.1  TRL - Code clean up \n\n    Notes:  1)  This function will gather all PM2.5 sensor data for 1 hour into the input queue, \n                we will then sum up all sensor values and then take an average, \n                then store the values based on time in the hour array\n            2)  if msg.topic = \"Put\", we will save msg.payload sensor data in queue\n                if msg.topic = \"time-1hour\", we will average all sensor data in queue,\n                an store it in our hour array based on current time in msg.payload,\n                and send it via MQTT and JSON formats\n      \n    Todo:   1)\n      \n    Reference's:\n                US EPA AQI Calculation Method \n                https://forum.airnowtech.org/t/daily-and-hourly-aqi-pm2-5/171\n                https://github.com/ThangLeQuoc/aqi-bot\n                https://www3.epa.gov/airnow/aqicalctest/nowcast.htm\n\n*/\n\n\n// let get array's from flow storage...\nvar queue     = flow.get(\"queue\");          // Get input queue array if we have it\nvar hourArray = flow.get(\"hourArray\");      // Get hour array if we have it\n\nvar msg1 = {};\n\n// lets check our arrays from flow storage if there valid, if not lets build them...\nif (!Array.isArray(queue))          // check to see if we have an array \n    {\n        queue = [];                 // if not, build it and clear the array\n    }\nelse if ( queue.length >= 60 )      // check max length (60 should be more than enought!)\n    {\n        queue.pop(1);               // if queue is too large, pop oldest value...\n    }\n\nif (!Array.isArray(hourArray))      // check to see if we have an array, if not build it... \n    {\n        hourArray = [-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1,-1]  // if not, build it and set the array to -1\n    }\n\n\nmsg.payload = parseFloat(msg.payload);          // if a string, convert to number\n\nswitch(msg.topic) \n        {\n            case \"put\":                         // saving current value\n                queue.unshift(msg.payload);     // save it\n                break;\n            \n            case \"time-1hour\":\n\n                if ( queue.length > 0)\n                {\n                    var AvdOfQueue = (queue.reduce(ArraySum) / queue.length).toFixed(2);    // compute the average of all values\n                }\n                else { AvdOfQueue = -1; }                                                   // if we don't have a value yet, set to -1\n                \n                hourArray[msg.payload]  = parseFloat(AvdOfQueue);\n                \n                msg1.topic   = \"RSF/ls113a/PM25/\";                                          // MQTT header\n                msg1.payload = hourArray;                                                   // current hour array\n                msg1.time    = msg.payload;                                                 // current hour\n                node.send (msg1);\n                \n                queue.length = 0;                                                           // clear the input queue array\n                break;\n        }\n  \n// lets save our arrays\nflow.set(\"queue\", queue);               // Save queue array in flow\nflow.set(\"hourArray\", hourArray);       // Save hour array in flow\n\nreturn \n\n\n/* ********************** Functions.... ****************************** */\n\n// This function calculates the sum of all elements in an array\nfunction ArraySum(total, value, index, array) \n{\n  return total + value;     // sum up array\n}\n\n/*\n// This function copy elements from the current index, to the new array in declining order...\n//  ie:  if the first element to copy is [6], next element copied is [5]... [4] etc...\n//  function wraps from first element in the array to last element in array  [0] --> [n], then [n-1]...\nfunction CopyArray(OldArray, length, index)\n{\n\nif ( length > OldArray.length) length = OldArray.length;\nlet newIndex = 0;       // current index in new array\nlet newArray = [];      // our new array\n\nfor (i = (length - 1); i >= 0; i--)\n    {  \n    if ( index == 0)    index = OldArray.length;\n        newArray[newIndex] = OldArray[(index % OldArray.length) ];\n        newIndex++;\n        index--;\n    }  \n    return newArray;\n}\n*/\n\n/* *************************** The End *********************** */\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":830,"y":440,"wires":[["fa3fe95c48431ae4","f33c51f4fed4e3c2","80d59fc14c1461e4"]]},{"id":"2d32505f0f24b3f5","type":"inject","z":"727f8e6b8a5480e5","name":"Every Second","repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"time","payload":"","payloadType":"date","x":200,"y":680,"wires":[["4329f73ec4e6d0ef"]]},{"id":"7c38a01a488f49d5","type":"rbe","z":"727f8e6b8a5480e5","name":"On Change Only","func":"rbe","gap":"","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":580,"y":680,"wires":[["7308a1889442f94f","f51efceab8bb7d59","39b5b524448fa8b7","660eb6dd1ba3f1f2"]]},{"id":"4329f73ec4e6d0ef","type":"function","z":"727f8e6b8a5480e5","name":"Get 1-Hour","func":"\nvar hour = gethour();\nmsg.payload = hour ;//% 24;\nmsg.topic   = \"time-1hour\";\n\nreturn [ msg ];\n\n\n// My Funcyions\nfunction gethour() \n{\n var date = new Date();\n var hour = (\"0\" + date.getHours()).substr(-2);\n //var hour = (\"0\" + date.getMinutes()).substr(-2);\n return hour;\n}\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":390,"y":680,"wires":[["7c38a01a488f49d5"]]},{"id":"7308a1889442f94f","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","statusVal":"","statusType":"auto","x":810,"y":760,"wires":[]},{"id":"c46b6f07ba242c06","type":"inject","z":"727f8e6b8a5480e5","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"time-1hour","payload":"14","payloadType":"num","x":190,"y":440,"wires":[["660eb6dd1ba3f1f2","69bd4c7a04c603dd","0463ab3e28215bf9"]]},{"id":"f51efceab8bb7d59","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"topic","targetType":"msg","statusVal":"","statusType":"auto","x":820,"y":720,"wires":[]},{"id":"39b5b524448fa8b7","type":"debug","z":"727f8e6b8a5480e5","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":830,"y":680,"wires":[]},{"id":"7b6fa61dc3e7099c","type":"comment","z":"727f8e6b8a5480e5","name":"Sends a time tick every hour, on the hour   ","info":"","x":240,"y":640,"wires":[]},{"id":"f33c51f4fed4e3c2","type":"function","z":"727f8e6b8a5480e5","name":"Calculate AQI to MQTT/JSON","func":"/*******************************************************************************\n\n  This is free software; you can redistribute it and/or\n  modify it under the terms of the GNU Lesser General Public\n  License as published by the Free Software Foundation; either\n  version 2.1 of the License, or (at your option) any later version.\n\n  This is distributed in the hope that it will be useful,\n  but WITHOUT ANY WARRANTY; without even the implied warranty of\n  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU\n  Lesser General Public License for more details.\n\n  You should have received a copy of the GNU Lesser General Public\n  License along with this library; if not, write to the Free Software\n  Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA\n\n  [email protected]\n\n * *****************************************************************************\n\n    CHANGE LOG:\n\n    DATE         REV  DESCRIPTION\n    -----------  ---  ----------------------------------------------------------\n    01-Aug-2021  1.0  TRL - First Build   \n    07-Sep-2021  1.1  TRL - Code clean up \n\n    Notes:  1)  This function expect a msg.payload array of [24] hourly values with current concentrations data \n            2)  msg.topic is prefix to MQTT string. \n            3)  Supports AQI and NowCast-AQI Calculation\n                NCAQIx will re-sort the array with a time offset and range as needed.\n            4)  Developed from work of ThangLeQuoc.  https://github.com/ThangLeQuoc/aqi-bot\n\n      \n    Todo:   1)\n      \n    Reference's:\n                US EPA AQI Calculation Method \n                https://forum.airnowtech.org/t/daily-and-hourly-aqi-pm2-5/171\n                https://github.com/ThangLeQuoc/aqi-bot\n                https://www3.epa.gov/airnow/aqicalctest/nowcast.htm\n\n*/\n\n\nvar msg1 = {};\n\nlet Myconcentrations = msg.payload;\n\n// Test data is preloaded in correct time order...\n\n// Expect AVD = 7.83, AQI = 33, NowCast AVD = 3.2, NowCast12 AQI = 13, time = 11\n// let Myconcentrations = [3.2,13.1,5.6,8.7,9.4,12.3,13.2,5.4,7.6,3.2,2.1,2.5,3.6,4.1,4.7,4.6,4.3,12.3,18.2,20.1,14.2,5.2,6.3,4.1];\n\n// Expect AVD = 19.44, AQI = 66, NowCast AVD = 3.5, NowCast12 AQI = 15, time = 11\n// let Myconcentrations = [12.3,4.5,6.5,8.6,9.3,2.0,9.8,6.5,4.5,8.7,1.8,2.5,3.6,6.6,5.6,5.0,4.0,2.3,12.2,24.3,66.6,78.9,80.2,100.3];\n\n// Expect AVD = 44.56, AQI = 123, NowCast AVD = 28.4, NowCast AQI12 = 85, time = 11 (-1 is missing data...)\n// let Myconcentrations = [21,-1,35,49.2,48.6,53.7,66.2,69.2,64.9,50,43,34.9];\n\n\n/* NowCast expect the concentrations array to be in time order, first element [0] is current reading,\n    2nd element [1] is last hour ect.... We use the function CopyArray(OldArray, length, index) to do this.\n*/\n\n// We will send last hours PM2.5 PPM value\nvar CurrentHour     = msg.time;\nvar CurrentPM25PPM  = Myconcentrations[CurrentHour];\nmsg1.payload        = CurrentPM25PPM;\nmsg1.topic          = msg.topic + \"PPM/\";\nnode.send (msg1);\n\n// You can calculate AQI based on average of concentrations data or use Max value of array\n// to get largest value in array...\n// var max      = Math.max(...Myconcentrations);\n// var MyAQI    = parseFloat(calculateAQI(max));\n\nvar SumOfArray  = Myconcentrations.reduce(ArraySum);\nvar AvdOfArray  = parseFloat((SumOfArray / Myconcentrations.length).toFixed(2));\nvar MyAQI       = parseFloat(calculateAQI(AvdOfArray));\n\nmsg1.payload    = MyAQI;\nmsg1.topic      = msg.topic + \"AQI/\";\nnode.send (msg1);\n\n\n// Calculate NowCast AQI based on PM25 Concentration values for the last 24hr\nvar PM25Array           = CopyArray(Myconcentrations, 24, CurrentHour);       // sort in time order from current hour\nvar PM25Concentration   = parseFloat(NowCastConcentration(PM25Array));\nvar NowCastAQI24        = parseFloat(calculateAQI(PM25Concentration));\nmsg1.payload            = NowCastAQI24;\nmsg1.topic              = msg.topic + \"NCAQI24/\";\nmsg1.PM25AVD24          = PM25Concentration;\nmsg1.PM25Array24        = PM25Array;\nnode.send (msg1);\nPM25Array.length = 0;\n\n// Calculate NowCast AQI based on PM25 Concentration values for the last 12hr\nPM25Array           = CopyArray(Myconcentrations, 12, CurrentHour);\nPM25Concentration   = parseFloat(NowCastConcentration(PM25Array));\nvar NowCastAQI12    = parseFloat(calculateAQI(PM25Concentration));\nmsg1.payload        = NowCastAQI12;\nmsg1.topic          = msg.topic + \"NCAQI12/\";\nmsg1.PM25AVD12      = PM25Concentration;\nmsg1.PM25Array12    = PM25Array;\nnode.send (msg1);\nPM25Array.length = 0;\n\n// Calculate NowCast AQI based on PM25 Concentration values for the last 6hr\nPM25Array           = CopyArray(Myconcentrations, 6, CurrentHour);\nPM25Concentration   = parseFloat(NowCastConcentration(PM25Array));\nvar NowCastAQI6     = parseFloat(calculateAQI(PM25Concentration));\nmsg1.payload        = NowCastAQI6;\nmsg1.topic          = msg.topic + \"NCAQI6/\";\nmsg1.PM25AVD6       = PM25Concentration;\nmsg1.PM25Array6     = PM25Array;\nnode.send (msg1);\nPM25Array.length = 0;\n\n// Calculate NowCast AQI based on PM25 Concentration values for the last 3hr\nPM25Array           = CopyArray(Myconcentrations, 3, CurrentHour);\nPM25Concentration   = parseFloat(NowCastConcentration(PM25Array));\nvar NowCastAQI3     = parseFloat(calculateAQI(PM25Concentration));\nmsg1.payload        = NowCastAQI3;\nmsg1.topic          = msg.topic + \"NCAQI3/\";\nmsg1.PM25AVD3       = PM25Concentration;\nmsg1.PM25Array3     = PM25Array;\nnode.send (msg1);\n\n// let also send a JSON object...\nmsg1.payload =   {\"PPM\":CurrentPM25PPM,\n                  \"AQI\":MyAQI,\n                  \"NCAQI24\":NowCastAQI24,\n                  \"NCAQI12\":NowCastAQI12,\n                  \"NCAQI6\":NowCastAQI6,\n                  \"NCAQI3\":NowCastAQI3\n                 };\nmsg1.payload = JSON.stringify(msg1.payload);\nmsg1.topic   = msg.topic + \"JSON/\";\nnode.send (msg1);\n\nreturn \n\n\n/* ********************** Functions.... ****************************** */\n\n/*  We calculate AQI based on \"US EPA AQI Breakpoint\", it expect the hightest value\n     in the reporting interval (24hr...) or average of all elements?\n     we expect a value range of 0 to 500\n*/\nfunction       calculateAQI(PM25Concentration)\n{\nlet breakpoint = 0;\n\nif (PM25Concentration < 0 ) PM25Concentration = 0;\nif (isNaN (PM25Concentration)) node.error (\"Array is not a number\")\nelse\n{\n            if      (PM25Concentration <= 12)       // Green: Good Quality\n                        breakpoint = \n                        {\n                            \"min\": 0,\n                            \"max\": 12,\n                            \"index\": \n                                {\n                                    \"min\": 0,\n                                    \"max\": 50\n                                }\n                        };\n            \n            else if (PM25Concentration <= 35.4)     // Yellow: Moderate\n                        breakpoint = \n                        {\n                            \"min\": 12.1,\n                            \"max\": 35.4,\n                            \"index\": \n                                {\n                                    \"min\": 51,\n                                    \"max\": 100\n                                }\n                        };\n           \n            else if (PM25Concentration <= 55.4)     // Orange: Sensitive\n                        breakpoint = \n                        {\n                            \"min\": 35.5,\n                            \"max\": 55.4,\n                            \"index\": \n                                {\n                                    \"min\": 101,\n                                    \"max\": 150\n                                }\n                        };\n            \n            else if (PM25Concentration <= 150.4)    // Red: Unheality \n                        breakpoint = \n                        {\n                            \"min\": 55.5,\n                            \"max\": 150.4,\n                            \"index\": \n                                {\n                                    \"min\": 151,\n                                    \"max\": 200\n                                }\n                        };\n            \n            else if (PM25Concentration <= 250.4)    // Purple: Very Unheality \n                        breakpoint = \n                        {\n                            \"min\": 150.5,\n                            \"max\": 250.4,\n                            \"index\": \n                                {\n                                    \"min\": 201,\n                                    \"max\": 300\n                                }\n                        };\n            \n            else if (PM25Concentration <= 350.4)    // Maroon: Hazardous \n                        breakpoint = \n                        {\n                            \"min\": 250.5,\n                            \"max\": 350.4,\n                            \"index\": \n                                {\n                                    \"min\": 301,\n                                    \"max\": 400\n                                }\n                        };\n            \n            else if (PM25Concentration <= 500)      // Maroon: Very Hazardous  \n                        breakpoint = \n                        {\n                            \"min\": 350.5,\n                            \"max\": 500,\n                            \"index\": \n                                {\n                                    \"min\": 401,\n                                    \"max\": 500\n                                }\n                        };\n\n            else if (PM25Concentration > 500)    return 500;\n                \n            return parseFloat (calculateBreakpoint(PM25Concentration, breakpoint));\n    }\n    return 0;\n}\n\n/*  This function calculates AQI for a given concentration of PM2.5 */\nfunction  calculateBreakpoint(concentration, breakpoint) \n  {\n    let cHigh   = breakpoint.max;\n    let cLow    = breakpoint.min;\n    let iHigh   = breakpoint.index.max;\n    let iLow    = breakpoint.index.min;\n    let result  = (iHigh - iLow) / (cHigh - cLow) * (concentration - cLow) + iLow;\n    return Math.round(result);\n  }\n\n\n/* This function calulates NowCast Concentration for an array of PM2.5 values\n     NowCast expect the concentrations array to be in time order, first element [0] is current reading,\n     2nd element [1] is last hour ect.... array is [24] values in hour order\n*/\nfunction NowCastConcentration(concentrations) \n{\n    let  getWeightFactor = (concentrations) => \n    {\n        let maxConcentration = Number.MIN_VALUE;\n        let minConcentration = Number.MAX_VALUE;\n        concentrations.forEach((concentration) => \n        {\n        if (concentration < 0)\n            return;\n        else \n        {\n        if (concentration > maxConcentration)\n            maxConcentration = concentration;\n        if (concentration < minConcentration) \n        {\n            minConcentration = concentration;\n        }\n    }\n  });\n\n    let range = maxConcentration - minConcentration;\n    let weightFactor = 1 - range / maxConcentration;\n\n    /* For Particulate Matter PM2.5, Minimum weight factor is 0.5 */\n    return (weightFactor > 0.5) ? weightFactor : 0.5;\n    }\n    \n// let's computer NowCast AQI...\n\n      let totalConcentrationWithWeight = 0;\n      let weight = getWeightFactor(concentrations);\n      let totalWeight = 0;\n\n      for (let i = 0; i < concentrations.length; i++) \n      {\n        if (concentrations[i] < 0)\n          continue;\n        else \n        {\n          totalConcentrationWithWeight += concentrations[i] * Math.pow(weight, i);\n          totalWeight += Math.pow(weight, i);\n        }\n      }\n    \n    if (totalWeight != 0)   return (totalConcentrationWithWeight / totalWeight).toFixed(1);\n    else return \"\";\n}\n\n\n/*  This function calculates the sum of all elements in an array */\nfunction ArraySum(total, value, index, array) \n{\n  return total + value;     // sum up our array\n}\n\n\n/* This function copy elements from the current index, to the new array in declining order...\n    ie:  if the first element to copy is [6], next element copied will be [5]... [4] etc...\n    function wraps from first element in the array to last element [n] in array  [0] --> [n], then [n-1]... \n*/\nfunction CopyArray(OldArray, length, index)\n{\n\nif ( length > OldArray.length) length = OldArray.length;\nlet newIndex = 0;       // current index in new array\nlet newArray = [];      // our new array\n\nfor (let i = (length - 1); i >= 0; i--)\n    {  \n    if ( index == 0)    index = OldArray.length;\n        newArray[newIndex] = OldArray[(index % OldArray.length) ];\n        newIndex++;\n        index--;\n    }  \n    return newArray;\n}\n\n/* *************************** The End *********************** */\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"// Code added here will be run when the\n// node is being stopped or re-deployed.\n\n\n// var MyAQI    = parseFloat(calculateAQI(max));\n\n// var max = Math.max(...Myconcentrations);","libs":[],"x":1130,"y":560,"wires":[["9d063b79ad937f19","790b4c9c9341b2c6","82548213337515c4","2973787dafc7fe02","7807abd6a99b315c","c07d72269257d632","f4fa55b808783074","fce0fe2b8d0289e1","e4a1a6b7745cdce4","04e393b50963255c","7f7f0d1f6dcae15f","570f93ede3243a41"]]},{"id":"6283e6b3ea9fdb5b","type":"inject","z":"727f8e6b8a5480e5","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":180,"y":560,"wires":[["28917b8db7d44351"]]},{"id":"9d063b79ad937f19","type":"debug","z":"727f8e6b8a5480e5","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1410,"y":480,"wires":[]},{"id":"382f66b0cae6f78e","type":"comment","z":"727f8e6b8a5480e5","name":"MQTT Message's sent","info":"","x":1610,"y":480,"wires":[]},{"id":"28917b8db7d44351","type":"function","z":"727f8e6b8a5480e5","name":"Test Message","func":"\n\n// Test data is preloaded in correct time ordr... (no need to use copyarray())\n\n// Expect AVD = 7.83, AQI = 33, NowCast AVD = 3.2, NowCast12 AQI = 13, time = 11\n let Myconcentrations = [3.2,13.1,5.6,8.7,9.4,12.3,13.2,5.4,7.6,3.2,2.1,2.5,3.6,4.1,4.7,4.6,4.3,12.3,18.2,20.1,14.2,5.2,6.3,4.1];\n\n// Expect AVD = 19.44, AQI = 66, NowCast AVD = 3.5, NowCast12 AQI = 15, time = 11\n// let Myconcentrations = [12.3,4.5,6.5,8.6,9.3,2.0,9.8,6.5,4.5,8.7,1.8,2.5,3.6,6.6,5.6,5.0,4.0,2.3,12.2,24.3,66.6,78.9,80.2,100.3];\n\n// Expect AVD = 44.56, AQI = 123, NowCast AVD = 28.4, NowCast AQI12 = 85, time = 11 (-1 is missing data...)\n// let Myconcentrations = [21,-1,35,49.2,48.6,53.7,66.2,69.2,64.9,50,43,34.9];\n\nmsg.payload = Myconcentrations;\nmsg.topic   = \"RSF/ls113a/PM25/\";\nmsg.time    = 11;   // 0--> 23 is 24 hours\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":400,"y":560,"wires":[["f33c51f4fed4e3c2"]]},{"id":"80d59fc14c1461e4","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1050,"y":440,"wires":[]},{"id":"790b4c9c9341b2c6","type":"mqtt out","z":"727f8e6b8a5480e5","name":"local-mqtt-server","topic":"","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"386e425790269597","x":1420,"y":560,"wires":[]},{"id":"2973787dafc7fe02","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1390,"y":440,"wires":[]},{"id":"82548213337515c4","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"topic","targetType":"msg","statusVal":"","statusType":"auto","x":1400,"y":520,"wires":[]},{"id":"066e36fd4b0a8b65","type":"comment","z":"727f8e6b8a5480e5","name":"MQTT Topic sent","info":"","x":1600,"y":520,"wires":[]},{"id":"18fca9ab772b090c","type":"comment","z":"727f8e6b8a5480e5","name":"Sends a test message array","info":"","x":210,"y":520,"wires":[]},{"id":"dab4d1ef4eaed71b","type":"comment","z":"727f8e6b8a5480e5","name":"Sends a 1 hour tick","info":"","x":180,"y":400,"wires":[]},{"id":"5b6f3f79dfcf0f82","type":"inject","z":"727f8e6b8a5480e5","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"Put","payload":"23","payloadType":"num","x":170,"y":340,"wires":[["660eb6dd1ba3f1f2","69bd4c7a04c603dd","0463ab3e28215bf9"]]},{"id":"842b2f352327bbae","type":"comment","z":"727f8e6b8a5480e5","name":"Sends test sensor data","info":"","x":190,"y":300,"wires":[]},{"id":"dbfe60bbe7e8d933","type":"comment","z":"727f8e6b8a5480e5","name":"Get sensor data","info":"","x":170,"y":180,"wires":[]},{"id":"0b7f2e157ff1c12a","type":"comment","z":"727f8e6b8a5480e5","name":"In this case, we get PM2.5 sensor data from TTN MQTT server","info":"","x":310,"y":60,"wires":[]},{"id":"149a17f72b06ddf2","type":"comment","z":"727f8e6b8a5480e5","name":"We extract the data, format it, and pass on","info":"","x":550,"y":180,"wires":[]},{"id":"7807abd6a99b315c","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25AVD12","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":360,"wires":[]},{"id":"c07d72269257d632","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25AVD24","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":400,"wires":[]},{"id":"f4fa55b808783074","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25Array24","targetType":"msg","statusVal":"","statusType":"auto","x":1430,"y":240,"wires":[]},{"id":"fce0fe2b8d0289e1","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25Array12","targetType":"msg","statusVal":"","statusType":"auto","x":1430,"y":200,"wires":[]},{"id":"e4a1a6b7745cdce4","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25Array6","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":160,"wires":[]},{"id":"04e393b50963255c","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25Array3","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":120,"wires":[]},{"id":"7f7f0d1f6dcae15f","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25AVD6","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":320,"wires":[]},{"id":"570f93ede3243a41","type":"debug","z":"727f8e6b8a5480e5","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25AVD3","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":280,"wires":[]},{"id":"f58040f5.4d815","type":"mqtt-broker","name":"TTV-LS-11x","broker":"nam1.cloud.thethings.network","port":"1883","clientid":"","usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"sessionExpiry":""},{"id":"386e425790269597","type":"mqtt-broker","name":"","broker":"127.0.0.1","port":"1883","clientid":"","usetls":false,"protocolVersion":"4","keepalive":"60","cleansession":true,"birthTopic":"","birthQos":"0","birthPayload":"","birthMsg":{},"closeTopic":"","closeQos":"0","closePayload":"","closeMsg":{},"willTopic":"","willQos":"0","willPayload":"","willMsg":{},"sessionExpiry":""}]

Flow Info

Created 2 years, 10 months ago
Rating: 5 1

Owner

Actions

Rate:

Node Types

Core
  • comment (x9)
  • debug (x20)
  • function (x5)
  • inject (x4)
  • mqtt in (x1)
  • mqtt out (x1)
  • mqtt-broker (x2)
  • rbe (x1)
Other
  • tab (x1)

Tags

  • NowCast
  • PM2.5
  • AQI
  • air quality
  • MQTT
  • JSON
Copy this flow JSON to your clipboard and then import into Node-RED using the Import From > Clipboard (Ctrl-I) menu option