Skip to content

Instantly share code, notes, and snippets.

@trlafleur
Last active January 20, 2024 17:01
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save trlafleur/a51d11c82ba800a8183c630d57d02be0 to your computer and use it in GitHub Desktop.
Save trlafleur/a51d11c82ba800a8183c630d57d02be0 to your computer and use it in GitHub Desktop.
NowCast AQI for PM2.5

This will 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":"6e8721324f8a76c4","type":"tab","label":"PM2.5 AQI","disabled":false,"info":""},{"id":"ddcd374c2e0ad1b7","type":"mqtt in","z":"6e8721324f8a76c4","name":"MQTT from sensor","topic":"mytopic/","qos":"0","datatype":"auto","broker":"386e425790269597","nl":false,"rap":false,"x":170,"y":220,"wires":[["68f1f07d968a2ef2","b202a8cfe16e9c18","a5014702c2d1bcf5"]]},{"id":"68f1f07d968a2ef2","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":440,"y":100,"wires":[]},{"id":"b202a8cfe16e9c18","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"topic","x":430,"y":140,"wires":[]},{"id":"a5014702c2d1bcf5","type":"function","z":"6e8721324f8a76c4","name":"PM2.5 Decrypt ","func":"\n\n// msgx, we test for correct device\nvar msgx = { payload: msg.payload.length };\nmsgx.payload = JSON.parse(msg.payload);\n// check for you unique device ID here\nif ( msgx.payload.end_device_ids.device_id == \"MyDeviceID\")\n{\n // msg1 - PM2.5 PPM\n var msg1 = {};\n msg1.payload = msgx.payload.uplink_message.decoded_payload.ppm;\n msg1.payload = parseFloat(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":440,"y":220,"wires":[["115443e584652aa3","4472fdc5f570c36e","49052a7cb6909e65"]]},{"id":"115443e584652aa3","type":"debug","z":"6e8721324f8a76c4","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","x":1080,"y":220,"wires":[]},{"id":"4472fdc5f570c36e","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"topic","x":1070,"y":260,"wires":[]},{"id":"05d676a0664e98a6","type":"debug","z":"6e8721324f8a76c4","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","x":1080,"y":400,"wires":[]},{"id":"49052a7cb6909e65","type":"function","z":"6e8721324f8a76c4","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 tom@lafleur.us\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 an input-queue\n if msg.topic = \"time-1hour\", we will average all sensor data in the queue,\n an store it in our hour array based with current time, and send it in msg.payload,\n to be process and sent via MQTT and JSON formats by another node...\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\nvar msg1 = {};\n\n// let get the 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\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 // if not, build it and set the array values to -1\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] \n }\n\n// get data from message...\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 any value yet, set to -1\n \n hourArray[msg.payload] = parseFloat(AvdOfQueue); // save it in hour array\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\n/* ************************ The End ********************************** */\n\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":830,"y":440,"wires":[["05d676a0664e98a6","bcc719f3cf50cbec","c564e24c2131e9f6"]]},{"id":"d9bf6cfef2ba03d3","type":"inject","z":"6e8721324f8a76c4","name":"Every Second","repeat":"1","crontab":"","once":false,"onceDelay":0.1,"topic":"time","payload":"","payloadType":"date","x":190,"y":680,"wires":[["74e9fa136035f55e"]]},{"id":"1eb7b4acfb508eb6","type":"rbe","z":"6e8721324f8a76c4","name":"On Change Only","func":"rbe","gap":"","start":"","inout":"out","septopics":true,"property":"payload","topi":"topic","x":580,"y":680,"wires":[["a338b285784d359f","b995a662584e9619","b224474a8a0160fb","49052a7cb6909e65"]]},{"id":"74e9fa136035f55e","type":"function","z":"6e8721324f8a76c4","name":"Get 1-Hour","func":"\nvar hour = gethour();\nmsg.payload = parseInt(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":[["1eb7b4acfb508eb6"]]},{"id":"a338b285784d359f","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","statusVal":"","statusType":"auto","x":810,"y":760,"wires":[]},{"id":"6ab98ffe4056b59d","type":"inject","z":"6e8721324f8a76c4","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"time-1hour","payload":"14","payloadType":"num","x":180,"y":440,"wires":[["49052a7cb6909e65","115443e584652aa3","4472fdc5f570c36e"]]},{"id":"b995a662584e9619","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"topic","targetType":"msg","statusVal":"","statusType":"auto","x":830,"y":720,"wires":[]},{"id":"b224474a8a0160fb","type":"debug","z":"6e8721324f8a76c4","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":840,"y":680,"wires":[]},{"id":"9ca15b50d533c06d","type":"comment","z":"6e8721324f8a76c4","name":"Sends a time tick every hour, on the hour ","info":"","x":240,"y":640,"wires":[]},{"id":"bcc719f3cf50cbec","type":"function","z":"6e8721324f8a76c4","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 tom@lafleur.us\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 for missings values we expect to see a -1\n 2) msg.topic is prefix to MQTT string. \n 3) Supports AQI and NowCast-AQI Calculation\n 4) NCAQIx will re-sort the array with a time offset and range as needed.\n 5) 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\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\nvar CurrentHour = msg.time;\nvar CurrentPM25PPM = Myconcentrations[CurrentHour];\n\n// We will also send last hours PM2.5 PPM value\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// Calculate NowCast AQI based on PM25 Concentration values for the last 24hr from current hour index\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);\nPM25Array.length = 0;\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 upto [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 compute 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":1120,"y":560,"wires":[["53bce68ebdae3852","ad8f0ff508531392","8d7c5b096eee9f54","58db0bb48b609eb7","7dc14201e1765e6c","fca8b4e2f1a89e38","d0df2a3bb8888b25","1d94e1d52ba9d3cd","fedbfa240bac9542","e3cf8996b1e196d9","9c70971bc37d7e39","5ae6f8a78d32cfab"]]},{"id":"c40ce2db3009af5e","type":"inject","z":"6e8721324f8a76c4","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"","payloadType":"date","x":170,"y":560,"wires":[["dbdc3de65fb2289e"]]},{"id":"53bce68ebdae3852","type":"debug","z":"6e8721324f8a76c4","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":1410,"y":480,"wires":[]},{"id":"d175f95aa094ad8b","type":"comment","z":"6e8721324f8a76c4","name":"MQTT Message's sent","info":"","x":1650,"y":480,"wires":[]},{"id":"dbdc3de65fb2289e","type":"function","z":"6e8721324f8a76c4","name":"Test Message","func":"\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\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":[["bcc719f3cf50cbec"]]},{"id":"c564e24c2131e9f6","type":"debug","z":"6e8721324f8a76c4","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1050,"y":440,"wires":[]},{"id":"ad8f0ff508531392","type":"mqtt out","z":"6e8721324f8a76c4","name":"To MQTT Server","topic":"","qos":"","retain":"","respTopic":"","contentType":"","userProps":"","correl":"","expiry":"","broker":"386e425790269597","x":1430,"y":560,"wires":[]},{"id":"58db0bb48b609eb7","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":1390,"y":440,"wires":[]},{"id":"8d7c5b096eee9f54","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"topic","targetType":"msg","statusVal":"","statusType":"auto","x":1400,"y":520,"wires":[]},{"id":"702eaaf0c65d0d73","type":"comment","z":"6e8721324f8a76c4","name":"MQTT Topic sent","info":"","x":1630,"y":520,"wires":[]},{"id":"4b11a4b235ceda64","type":"comment","z":"6e8721324f8a76c4","name":"Sends a test message array","info":"","x":210,"y":520,"wires":[]},{"id":"71f223619332cfd1","type":"comment","z":"6e8721324f8a76c4","name":"Sends a 1 hour tick","info":"","x":180,"y":400,"wires":[]},{"id":"43a87b6997c663c3","type":"inject","z":"6e8721324f8a76c4","name":"","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"Put","payload":"23","payloadType":"num","x":150,"y":340,"wires":[["49052a7cb6909e65","115443e584652aa3","4472fdc5f570c36e"]]},{"id":"b3af09515feb52d2","type":"comment","z":"6e8721324f8a76c4","name":"Sends test sensor data","info":"","x":190,"y":300,"wires":[]},{"id":"502e249977381f7c","type":"comment","z":"6e8721324f8a76c4","name":"Get sensor data","info":"","x":160,"y":180,"wires":[]},{"id":"eca8e234ace086b0","type":"comment","z":"6e8721324f8a76c4","name":"We get PM2.5 sensor data from a MQTT server","info":"","x":270,"y":60,"wires":[]},{"id":"e9863d7bb3e20444","type":"comment","z":"6e8721324f8a76c4","name":"We extract the data, format it, and pass on","info":"","x":540,"y":180,"wires":[]},{"id":"7dc14201e1765e6c","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25AVD12","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":360,"wires":[]},{"id":"fca8b4e2f1a89e38","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25AVD24","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":400,"wires":[]},{"id":"d0df2a3bb8888b25","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25Array24","targetType":"msg","statusVal":"","statusType":"auto","x":1430,"y":240,"wires":[]},{"id":"1d94e1d52ba9d3cd","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25Array12","targetType":"msg","statusVal":"","statusType":"auto","x":1430,"y":200,"wires":[]},{"id":"fedbfa240bac9542","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25Array6","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":160,"wires":[]},{"id":"e3cf8996b1e196d9","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25Array3","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":120,"wires":[]},{"id":"9c70971bc37d7e39","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25AVD6","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":320,"wires":[]},{"id":"5ae6f8a78d32cfab","type":"debug","z":"6e8721324f8a76c4","name":"","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"PM25AVD3","targetType":"msg","statusVal":"","statusType":"auto","x":1420,"y":280,"wires":[]},{"id":"50cd4f707269c7b0","type":"comment","z":"6e8721324f8a76c4","name":"TTN Keys","info":"\na good place to save any keys...","x":140,"y":100,"wires":[]},{"id":"553e5a4da7cdc871","type":"comment","z":"6e8721324f8a76c4","name":"We use these for debuging...","info":"","x":1450,"y":80,"wires":[]},{"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":""}]
@trlafleur
Copy link
Author

updated flow...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment