Announcement

Collapse
No announcement yet.

RFC: Polling PurpleAir Air Quality Sensors

Collapse
X
 
  • Filter
  • Time
  • Show
Clear All
new posts

    RFC: Polling PurpleAir Air Quality Sensors

    Up until now I've been using an HS3 plugin to poll PurpleAir sensors but I decided to re-implement this in Node-RED instead of paying to upgrade to the HS4 version of the plugin. I also like the fact that I can see and control exactly what's going on with the response processing and that nothing is "hidden" behind a plugin curtain.

    The flow uses a recurring inject node to trigger polling every 10 minutes. The polling is done with an HTTP Request node. The trigger is delayed 2 seconds between the request nodes just to keep PurpleAir from getting upset with too many requests in a short period of time. This delay also allows the results to be processed by the same change node and finally routed to the HS Device nodes with a switch node.

    Click image for larger version

Name:	Screen Shot 2022-04-09 at 3.20.51 PM.png
Views:	351
Size:	38.2 KB
ID:	1536014
    With the exception of the change node, the other nodes are basic and straightforward. The request nodes should be set to return a parsed JSON object in msg.payload. Note that the sensors I'm showing here are sensors I randomly picked out in the framing community of Modesto, CA.

    The change node moves msg.payload to msg.save, which is generally a good idea so that nothing in the JSON payload affects follow-on nodes that unexpectedly may interpret the msg. It then sets up the new message for the HS Device node and also sets msg.sensor_id to ID of the PurpleAir sensor.

    Click image for larger version

Name:	Screen Shot 2022-04-09 at 3.39.36 PM.png
Views:	141
Size:	61.3 KB
ID:	1536015

    The switch node routes the msg to the appropriate HS Device node based on msg.sensor_id that was set by the change node.

    Code:
    [
    {
    "id": "76915431a1cecbc2",
    "type": "tab",
    "label": "PurpleAir",
    "disabled": false,
    "info": "",
    "env": []
    },
    {
    "id": "157fe75fbc04079e",
    "type": "http request",
    "z": "76915431a1cecbc2",
    "name": "Poll 73501",
    "method": "GET",
    "ret": "obj",
    "paytoqs": "ignore",
    "url": "https://www.purpleair.com/json?show=73501",
    "tls": "",
    "persist": false,
    "proxy": "",
    "authType": "",
    "senderr": false,
    "x": 510,
    "y": 180,
    "wires": [
    [
    "62d6719dee1a7ad8"
    ]
    ]
    },
    {
    "id": "02a654e63f671d92",
    "type": "inject",
    "z": "76915431a1cecbc2",
    "name": "Polling Trigger",
    "props": [
    {
    "p": "payload"
    },
    {
    "p": "topic",
    "vt": "str"
    }
    ],
    "repeat": "600",
    "crontab": "",
    "once": true,
    "onceDelay": "5",
    "topic": "",
    "payload": "",
    "payloadType": "date",
    "x": 120,
    "y": 180,
    "wires": [
    [
    "157fe75fbc04079e",
    "25a17ec1df5b53cd"
    ]
    ]
    },
    {
    "id": "acfee28bda839a3d",
    "type": "http request",
    "z": "76915431a1cecbc2",
    "name": "Poll 13489",
    "method": "GET",
    "ret": "obj",
    "paytoqs": "ignore",
    "url": "https://www.purpleair.com/json?show=13489",
    "tls": "",
    "persist": false,
    "proxy": "",
    "authType": "",
    "senderr": false,
    "x": 510,
    "y": 220,
    "wires": [
    [
    "62d6719dee1a7ad8"
    ]
    ]
    },
    {
    "id": "498c17d0e80b2513",
    "type": "hs-device",
    "z": "76915431a1cecbc2",
    "name": "Aurora",
    "device": "55",
    "server": "cca8073e1d5285f1",
    "feature": "56",
    "reportonstartup": false,
    "x": 1110,
    "y": 180,
    "wires": [
    []
    ]
    },
    {
    "id": "c2603bb730b922f8",
    "type": "debug",
    "z": "76915431a1cecbc2",
    "name": "",
    "active": false,
    "tosidebar": true,
    "console": false,
    "tostatus": false,
    "complete": "true",
    "targetType": "full",
    "statusVal": "",
    "statusType": "auto",
    "x": 930,
    "y": 140,
    "wires": []
    },
    {
    "id": "62d6719dee1a7ad8",
    "type": "change",
    "z": "76915431a1cecbc2",
    "name": "",
    "rules": [
    {
    "t": "move",
    "p": "payload",
    "pt": "msg",
    "to": "save",
    "tot": "msg"
    },
    {
    "t": "set",
    "p": "topic",
    "pt": "msg",
    "to": "update",
    "tot": "str"
    },
    {
    "t": "set",
    "p": "payload.value",
    "pt": "msg",
    "to": "save.results[0].PM2_5Value",
    "tot": "msg"
    },
    {
    "t": "set",
    "p": "payload.status",
    "pt": "msg",
    "to": "save.results[0].PM2_5Value",
    "tot": "msg"
    },
    {
    "t": "set",
    "p": "sensor_id",
    "pt": "msg",
    "to": "save.results[0].ID",
    "tot": "msg"
    }
    ],
    "action": "",
    "property": "",
    "from": "",
    "to": "",
    "reg": false,
    "x": 740,
    "y": 200,
    "wires": [
    [
    "80b0b6e2904278a6",
    "c2603bb730b922f8"
    ]
    ]
    },
    {
    "id": "41d3c92200d53d66",
    "type": "hs-device",
    "z": "76915431a1cecbc2",
    "name": "AMTS Modesto",
    "device": "55",
    "server": "cca8073e1d5285f1",
    "feature": "58",
    "reportonstartup": false,
    "x": 1140,
    "y": 240,
    "wires": [
    []
    ]
    },
    {
    "id": "25a17ec1df5b53cd",
    "type": "delay",
    "z": "76915431a1cecbc2",
    "name": "2 Secs",
    "pauseType": "delay",
    "timeout": "2",
    "timeoutUnits": "seconds",
    "rate": "1",
    "nbRateUnits": "1",
    "rateUnits": "second",
    "randomFirst": "1",
    "randomLast": "5",
    "randomUnits": "seconds",
    "drop": false,
    "allowrate": false,
    "outputs": 1,
    "x": 330,
    "y": 220,
    "wires": [
    [
    "acfee28bda839a3d"
    ]
    ]
    },
    {
    "id": "80b0b6e2904278a6",
    "type": "switch",
    "z": "76915431a1cecbc2",
    "name": "",
    "property": "sensor_id",
    "propertyType": "msg",
    "rules": [
    {
    "t": "eq",
    "v": "73501",
    "vt": "str"
    },
    {
    "t": "eq",
    "v": "13489",
    "vt": "str"
    }
    ],
    "checkall": "true",
    "repair": false,
    "outputs": 2,
    "x": 930,
    "y": 200,
    "wires": [
    [
    "498c17d0e80b2513"
    ],
    [
    "41d3c92200d53d66"
    ]
    ]
    },
    {
    "id": "cca8073e1d5285f1",
    "type": "hs-server",
    "name": "HS4Pi",
    "host": "172.29.1.17",
    "port": "80"
    }
    ]
    "if I have seen further [than others], it is by standing on the shoulders of giants." --Sir Isaac Newton (1675)

    #2
    Very nice!

    I am curious: What do you use this data for. I understand air quality is a bigger issue in some areas, especially compared to what I have here. I am curious how you would use the information in your home automations, though. After all, Node-RED could send you a message as opposed to adding a middle man if a notification is the only reason.
    Karl S
    HS4Pro on Windows 10
    1070 Devices
    56 Z-Wave Nodes
    104 Events
    HSTouch Clients: 3 Android, 1 iOS
    Google Home: 3 Mini units, 1 Pair Audios, 2 Displays

    Comment


      #3
      TBH, for most of the year it's just informational. I have several virtual devices that signal whether it's hotter outside vs inside or cooler outside vs inside. These are used to tell us to close the windows and turn on the A/C or to turn off the A/C and open the windows. The SF Bay area has a lot of natural air conditioning with cool air rushing through the South Bay to get to the Central Valley. During fire season, our local AQI (we have our own sensor) as well as the surrounding sensors are factored into determining the state of those indicators. Actually, they are factored in all of the time, it's just that the AQI is usually low enough not to have an impact.

      Since the indoor temp sensors are Z-Wave, I add the outdoor readings from a TEMP05 via Node-RED, as well as the AQI, and I perform the calculations for the status indicators with VB scripts. I may look into moving some of this to Node-RED in the future, but for now, it works so I'm leaving it alone.
      "if I have seen further [than others], it is by standing on the shoulders of giants." --Sir Isaac Newton (1675)

      Comment


        #4
        Update: This polling method stopped working as of 5/26/2022 per this post: https://community.purpleair.com/t/di...-json-urls/713

        They now require an API key in order to access sensor data. https://community.purpleair.com/t/ma...pleair-api/180

        I'm working on an updated flow and I'll post it here when finished and tested.
        "if I have seen further [than others], it is by standing on the shoulders of giants." --Sir Isaac Newton (1675)

        Comment


          #5
          Edited

          how’s the projet doing now?

          Comment


            #6
            Originally posted by pistacheL0 View Post
            Edited

            how’s the projet doing now?
            Apologies for not posting the updated flow yet. I hacked it together to get it working but was hoping to add a configuration node to clean it up. I haven't got to that yet.

            Let me do a little cleanup this wekend and post what I have.
            "if I have seen further [than others], it is by standing on the shoulders of giants." --Sir Isaac Newton (1675)

            Comment


              #7
              This flow is updated to use the new API Key that is now required by PurpleAir. You can request a free API key by contacting PurpleAir support. I also added the PM10 readings in addition to the PM2.5 readings. Note that I'm only requesting the specific fields I'm interested in. Read the API Docs to see how the "fields" parameters work.

              1. Edit each "Poll" node and add your API key. (Look for 'Put API Key Here')
              2. Edit each "Poll node and change the sensor_ids to match the sensors you are interested in.
              2. Edit each "Select ID" node to match the sensor_ids in the "Poll" nodes.

              Ideally, the API key and sensor_ids should be put into a configuration node but I don't know how to do that yet.

              Feel free to ask questions.


              Here's the flow:
              Click image for larger version

Name:	Screen Shot 2022-09-04 at 10.17.13 AM.png
Views:	209
Size:	59.6 KB
ID:	1564594

              And the JSON:
              Code:
              [
              {
              "id": "b5c87f0d37512bcc",
              "type": "tab",
              "label": "PurpleAir with API Key",
              "disabled": false,
              "info": "",
              "env": []
              },
              {
              "id": "fd805cfe25ae7adc",
              "type": "http request",
              "z": "b5c87f0d37512bcc",
              "name": "",
              "method": "GET",
              "ret": "obj",
              "paytoqs": "query",
              "url": "",
              "tls": "",
              "persist": false,
              "proxy": "",
              "authType": "",
              "senderr": false,
              "x": 690,
              "y": 320,
              "wires": [
              [
              "08eeaf08d5e90a03",
              "caabd10b587f4862"
              ]
              ]
              },
              {
              "id": "dacd8335d6e6a1ed",
              "type": "inject",
              "z": "b5c87f0d37512bcc",
              "name": "",
              "props": [
              {
              "p": "payload"
              }
              ],
              "repeat": "600",
              "crontab": "",
              "once": false,
              "onceDelay": 0.1,
              "topic": "",
              "payload": "",
              "payloadType": "date",
              "x": 110,
              "y": 320,
              "wires": [
              [
              "7d81061263fdbaac",
              "12ee14cc0d589a5b"
              ]
              ]
              },
              {
              "id": "7b496e7d30eb4966",
              "type": "function",
              "z": "b5c87f0d37512bcc",
              "name": "PM25toAQI",
              "func": "let pm25val = msg.payload.sensor.stats[\"pm2.5_10minute\"];\nlet Conc, ConcHigh, ConcLow, AQIHigh, AQILow;\nlet aqival = 0;\n\nConc = (Math.floor(10 * pm25val)) / 10;\n\nswitch(true) {\n case Conc < 12.1:\n AQIHigh = 50; AQILow = 0; ConcHigh = 12; ConcLow = 0;\n break;\n case Conc < 35.5:\n AQIHigh = 100; AQILow = 51; ConcHigh = 35.4; ConcLow = 12.1;\n break;\n case Conc < 55.5:\n AQIHigh = 150; AQILow = 101; ConcHigh = 55.4; ConcLow = 35.5;\n break;\n case Conc < 150.5:\n AQIHigh = 200; AQILow = 151; ConcHigh = 150.4; ConcLow = 55.5;\n break;\n case Conc < 250.5:\n AQIHigh = 300; AQILow = 201; ConcHigh = 250.4; ConcLow = 150.5;\n break;\n case Conc < 350.5:\n AQIHigh = 400; AQILow = 301; ConcHigh = 350.4; ConcLow = 250.5;\n break;\n case Conc < 500.5:\n AQIHigh = 500; AQILow = 401; ConcHigh = 500.4; ConcLow = 350.5;\n break;\n default:\n AQIHigh = 1000; AQILow = 501; ConcHigh = 999.5; ConcLow = 500.5;\n}\n\naqival = ((Conc - ConcLow) / (ConcHigh - ConcLow)) * (AQIHigh - AQILow) + AQILow;\n\nmsg.payload.value = Math.round(aqival);\nmsg.payload.status = Math.round(aqival);\nmsg.topic = \"update\";\n\nreturn msg;",
              "outputs": 1,
              "noerr": 0,
              "initialize": "",
              "finalize": "",
              "libs": [],
              "x": 1030,
              "y": 340,
              "wires": [
              [
              "5b7d3a732140c7cc"
              ]
              ]
              },
              {
              "id": "5b7d3a732140c7cc",
              "type": "switch",
              "z": "b5c87f0d37512bcc",
              "name": "Select ID",
              "property": "payload.sensor.sensor_index",
              "propertyType": "msg",
              "rules": [
              {
              "t": "eq",
              "v": "73501",
              "vt": "str"
              },
              {
              "t": "eq",
              "v": "13489",
              "vt": "str"
              }
              ],
              "checkall": "true",
              "repair": false,
              "outputs": 2,
              "x": 1200,
              "y": 340,
              "wires": [
              [
              "2ddb3d8f685b4f1a",
              "00be0fc5ef9e3bab"
              ],
              [
              "d594d1bff7691ccb",
              "00be0fc5ef9e3bab"
              ]
              ]
              },
              {
              "id": "08eeaf08d5e90a03",
              "type": "switch",
              "z": "b5c87f0d37512bcc",
              "name": "200?",
              "property": "statusCode",
              "propertyType": "msg",
              "rules": [
              {
              "t": "neq",
              "v": "200",
              "vt": "num"
              },
              {
              "t": "eq",
              "v": "200",
              "vt": "num"
              }
              ],
              "checkall": "true",
              "repair": false,
              "outputs": 2,
              "x": 850,
              "y": 320,
              "wires": [
              [
              "97c677cb426f5016"
              ],
              [
              "7b496e7d30eb4966",
              "546175eaaae02622"
              ]
              ]
              },
              {
              "id": "caabd10b587f4862",
              "type": "debug",
              "z": "b5c87f0d37512bcc",
              "name": "",
              "active": true,
              "tosidebar": true,
              "console": false,
              "tostatus": false,
              "complete": "true",
              "targetType": "full",
              "statusVal": "",
              "statusType": "auto",
              "x": 850,
              "y": 260,
              "wires": []
              },
              {
              "id": "7d81061263fdbaac",
              "type": "change",
              "z": "b5c87f0d37512bcc",
              "name": "Poll Aurora",
              "rules": [
              {
              "t": "set",
              "p": "headers['X-API-Key']",
              "pt": "msg",
              "to": "Put API Key Here",
              "tot": "str"
              },
              {
              "t": "set",
              "p": "headers['Content-Type']",
              "pt": "msg",
              "to": "application/json",
              "tot": "str"
              },
              {
              "t": "set",
              "p": "url",
              "pt": "msg",
              "to": "https://api.purpleair.com/v1/sensors/73501",
              "tot": "str"
              },
              {
              "t": "set",
              "p": "payload",
              "pt": "msg",
              "to": "{\"fields\":\"pm2.5_10minute,pm10.0\"}",
              "tot": "json"
              }
              ],
              "action": "",
              "property": "",
              "from": "",
              "to": "",
              "reg": false,
              "x": 450,
              "y": 320,
              "wires": [
              [
              "fd805cfe25ae7adc"
              ]
              ]
              },
              {
              "id": "2ddb3d8f685b4f1a",
              "type": "hs-device",
              "z": "b5c87f0d37512bcc",
              "name": "Aurora PM2.5",
              "device": "55",
              "server": "cca8073e1d5285f1",
              "feature": "56",
              "reportonstartup": false,
              "x": 1420,
              "y": 280,
              "wires": [
              []
              ]
              },
              {
              "id": "0dc582804cb42c51",
              "type": "change",
              "z": "b5c87f0d37512bcc",
              "name": "Poll AMTS Modesto",
              "rules": [
              {
              "t": "set",
              "p": "headers['X-API-Key']",
              "pt": "msg",
              "to": "Put API Key Here",
              "tot": "str"
              },
              {
              "t": "set",
              "p": "headers['Content-Type']",
              "pt": "msg",
              "to": "application/json",
              "tot": "str"
              },
              {
              "t": "set",
              "p": "url",
              "pt": "msg",
              "to": "https://api.purpleair.com/v1/sensors/13489",
              "tot": "str"
              },
              {
              "t": "set",
              "p": "payload",
              "pt": "msg",
              "to": "{\"fields\":\"pm2.5_10minute,pm10.0\"}",
              "tot": "json"
              }
              ],
              "action": "",
              "property": "",
              "from": "",
              "to": "",
              "reg": false,
              "x": 460,
              "y": 380,
              "wires": [
              [
              "fd805cfe25ae7adc"
              ]
              ]
              },
              {
              "id": "12ee14cc0d589a5b",
              "type": "delay",
              "z": "b5c87f0d37512bcc",
              "name": "2s",
              "pauseType": "delay",
              "timeout": "2",
              "timeoutUnits": "seconds",
              "rate": "1",
              "nbRateUnits": "1",
              "rateUnits": "second",
              "randomFirst": "1",
              "randomLast": "5",
              "randomUnits": "seconds",
              "drop": false,
              "allowrate": false,
              "outputs": 1,
              "x": 290,
              "y": 380,
              "wires": [
              [
              "0dc582804cb42c51"
              ]
              ]
              },
              {
              "id": "d594d1bff7691ccb",
              "type": "hs-device",
              "z": "b5c87f0d37512bcc",
              "name": "AMTS Modesto PM2.5",
              "device": "55",
              "server": "cca8073e1d5285f1",
              "feature": "58",
              "reportonstartup": false,
              "x": 1440,
              "y": 400,
              "wires": [
              []
              ]
              },
              {
              "id": "00be0fc5ef9e3bab",
              "type": "debug",
              "z": "b5c87f0d37512bcc",
              "name": "",
              "active": false,
              "tosidebar": true,
              "console": false,
              "tostatus": false,
              "complete": "true",
              "targetType": "full",
              "statusVal": "",
              "statusType": "auto",
              "x": 1390,
              "y": 340,
              "wires": []
              },
              {
              "id": "97c677cb426f5016",
              "type": "hs-event",
              "z": "b5c87f0d37512bcc",
              "name": "Log PurpleAir Error",
              "event": "2",
              "server": "cca8073e1d5285f1",
              "x": 1050,
              "y": 280,
              "wires": [
              []
              ]
              },
              {
              "id": "546175eaaae02622",
              "type": "function",
              "z": "b5c87f0d37512bcc",
              "name": "PM10toAQI",
              "func": "let pm10val = msg.payload.sensor[\"pm10.0\"];\nlet Conc, ConcHigh, ConcLow, AQIHigh, AQILow;\nlet aqival = 0;\n\nConc = Math.floor(pm10val);\n\nswitch(true) {\n case Conc < 55:\n AQIHigh = 50; AQILow = 0; ConcHigh = 54; ConcLow = 0;\n break;\n case Conc < 155:\n AQIHigh = 100; AQILow = 51; ConcHigh = 154; ConcLow = 55;\n break;\n case Conc < 255:\n AQIHigh = 150; AQILow = 101; ConcHigh = 254; ConcLow = 155;\n break;\n case Conc < 355:\n AQIHigh = 200; AQILow = 151; ConcHigh = 354; ConcLow = 255;\n break;\n case Conc < 425:\n AQIHigh = 300; AQILow = 201; ConcHigh = 424; ConcLow = 355;\n break;\n case Conc < 605:\n AQIHigh = 500; AQILow = 301; ConcHigh = 604; ConcLow = 425;\n break;\n default:\n AQIHigh = 1000; AQILow = 501; ConcHigh = 999.5; ConcLow = 605;\n}\n\naqival = ((Conc - ConcLow) / (ConcHigh - ConcLow)) * (AQIHigh - AQILow) + AQILow;\n\nmsg.payload.value = Math.round(aqival);\nmsg.payload.status = Math.round(aqival);\nmsg.topic = \"update\";\n\nreturn msg;",
              "outputs": 1,
              "noerr": 0,
              "initialize": "",
              "finalize": "",
              "libs": [],
              "x": 1030,
              "y": 520,
              "wires": [
              [
              "26a815790d87ce33"
              ]
              ]
              },
              {
              "id": "26a815790d87ce33",
              "type": "switch",
              "z": "b5c87f0d37512bcc",
              "name": "Select ID",
              "property": "payload.sensor.sensor_index",
              "propertyType": "msg",
              "rules": [
              {
              "t": "eq",
              "v": "73501",
              "vt": "str"
              },
              {
              "t": "eq",
              "v": "13489",
              "vt": "str"
              }
              ],
              "checkall": "true",
              "repair": false,
              "outputs": 2,
              "x": 1200,
              "y": 520,
              "wires": [
              [
              "36747cd43d3ae3ae",
              "513a4d84f39d7546"
              ],
              [
              "1172e658b91086f8",
              "513a4d84f39d7546"
              ]
              ]
              },
              {
              "id": "36747cd43d3ae3ae",
              "type": "hs-device",
              "z": "b5c87f0d37512bcc",
              "name": "Aurora PM10",
              "device": "55",
              "server": "cca8073e1d5285f1",
              "feature": "79",
              "reportonstartup": false,
              "x": 1410,
              "y": 460,
              "wires": [
              []
              ]
              },
              {
              "id": "1172e658b91086f8",
              "type": "hs-device",
              "z": "b5c87f0d37512bcc",
              "name": "AMTS Modesto PM10",
              "device": "55",
              "server": "cca8073e1d5285f1",
              "feature": "80",
              "reportonstartup": false,
              "x": 1440,
              "y": 580,
              "wires": [
              []
              ]
              },
              {
              "id": "513a4d84f39d7546",
              "type": "debug",
              "z": "b5c87f0d37512bcc",
              "name": "",
              "active": false,
              "tosidebar": true,
              "console": false,
              "tostatus": false,
              "complete": "true",
              "targetType": "full",
              "statusVal": "",
              "statusType": "auto",
              "x": 1390,
              "y": 520,
              "wires": []
              },
              {
              "id": "cca8073e1d5285f1",
              "type": "hs-server",
              "name": "HS4Pi",
              "host": "172.29.1.17",
              "port": "80"
              }
              ]
              "if I have seen further [than others], it is by standing on the shoulders of giants." --Sir Isaac Newton (1675)

              Comment

              Working...
              X