[RELEASE] PurpleAir AQI monitoring driver

This driver uses the PurpleAir API to provide the air quality as reported by a selected sensor or by averaging the reports from sensors in the area you choose. There is a deprecated PurpleAir access method which some older drivers used, but I didn't see anything yet that uses this newer access method.

To begin using this driver you will need a free API key from PurpleAir which can be requested by sending an email to contact@purpleair.com.

Once you have your key just enter it and the driver will fetch all of the AQI reports in your area and report the average of them. The default search area is a square that extends 0.5 miles from the position reported by your Hub.

Alternatively, if you would like to choose a specific device to monitor you can find a device on map.purpleair.com and enter the device index. The device index can be found by selecting the device on the map and then looking for "Select=INDEX" in the URL. Currently only one device can be selected.

Below is the driver, please let me know if you have any questions or issues.

https://raw.githubusercontent.com/pfmiller0/Hubitat/main/PurpleAir%20AQI%20Virtual%20Sensor.groovy

6 Likes

Installed. Requested and input a new API from PurpleAir. I get this in the log:

`

dev:12432022-01-21 03:25:33.256 pm errororg.codehaus.groovy.runtime.metaclass.MissingMethodExceptionNoStack: No signature of method: user_driver_hyposphere_net_PurpleAir_AQI_Virtual_Sensor_1004.poll() is applicable for argument types: () values: []

`
UPDATE: I increased the search range and it found some sensors.

1 Like

I wasn't able to reproduce that error by decreasing my search area. I do already have a check for if no sensors are found in the specified area. When you got that error everything was using default values other than the key you entered?

I am thinking it may have been the newly issue API key was not yet active in their systems. Could that have cause the error ... if the API key used was not found?

I don't think that would cause it, there's a message about an http 403 error that should show up in the logs if the key is invalid. Adding a better error message for that is on my to-do list.

I've never seen your error in a few months of running this driver, hopefully you won't see it again!

Also I did just update the version in GitHub to a newer version I had on my Hubitat. Only real change is there's an option to average the devices found weighted by distance.

1 Like

Hi Matthew. Are you still using a PurpleAir product? If so, how do you like it?

I have never owned one. I use the driver to look at the readings of public devices near me.

Oh gotcha. Thanks

Given that PurpleAir retired the JSON API on May 26, 2022, this seems to be the only driver to access the value.
That said, I am unclear what the "Select=INDEX" is referring to. The PurpleAir Map only references an ID (JSON ID for that matter) which is however unique to the sensor, whereas the Name is not unique. If indeed it refers to the ID, maybe that can be clarified in the documentation.
Also, it would be great to have some more output from it, such as "AQI_Value AQI_Quality" and maybe as top or bottom secondary value a data time stamp, so we can monitor if sensors are up or not.

Update SENSOR=INDEX, refers to sensor_index, which is indeed the PurpleAir Station ID from the HTML string on the PurpleAir Map.

I am running this for the same station in two separate instances, once by Lat/Lon and once by using the station ID. Using the Station ID it throws an error:

dev:3482022-05-30 11:26:31.453 errorjava.lang.NullPointerException: null on line 204 (method httpResponse)
dev:3482022-05-30 11:26:31.410 debugunweighted av aqi: 9
dev:3482022-05-30 11:26:31.409 debugAQIs: [9]
dev:3482022-05-30 11:26:31.407 debugsites: [Claremont Hills]

The code for line 204 looks like this:
Float distance(Float[] a, def b) {
Float[] dist2deg = distance2degrees(a[0])
Float lat_diff = (a[0] - b[0])*dist2deg[0]
Float lon_diff = (a[1] - b[1])*dist2deg[1]
return Math.sqrt(lat_diff2 + lon_diff2)
}

Any thoughts? @pfmiller

Hi @schuppenhauer, sorry about that error. A new version has been updated with it fixed.

I was using weighted averages by default when the device id was specified and that was causing the error. Since only one ID is supported currently averaging isn't needed.

1 Like

Ok, thanks, that fixed that error.

I came across another one, which I think relates to how it calls the sensors. I have two indoor sensors and the both throw this error:
dev:1472022-05-31 16:28:45.422 errorNo sensors found in search area
dev:1472022-05-31 16:28:45.420 debugcoords: null
dev:1472022-05-31 16:28:45.419 debugunweighted av aqi: 0
dev:1472022-05-31 16:28:45.418 debugAQIs: []
dev:1472022-05-31 16:28:45.418 debugsites: []

I also noticed that in debug it now reports the coords which it did not do before. They are obviously null for sensor_index 'ed stations.

Another thought I am having is if the driver could not be modified to call way more parameters that might be useful, as well as to find a much shorter interval for calling the data, ie shorter than 15 min.

Are the measurement confidences for your devices both below 90%? I was filtering out results that are below that threshold and so maybe that's what was getting you. I disabled that filtering when the device is specified now, so if you think that's it please update and try again.

I probably should make that an option when searching for devices too, it may be too picky for some situations.

Both sensors are indoor sensors. Those only have one channel and not two, hence their confidence is likely not 100. In this case both show a confidence of 30. Again, I think the 30 is a function of this only being one channel devices used indoors only.
Personally I think having a confidence criteria if you poll only one sensor makes not a lot of sense. I would rather report this and a number of the other values out so they could be used for control purposes. For that however, the polling frequency needs to be adjusted to have options for 1, 5 and 10 minutes as well.

Yeah, confidence doesn't make sense for your own device. Mostly I wrote this with the search function in mind and for outdoor devices since that's what I need, but I'm happy to improve its support for other use cases. Did the version with confidence filtering disabled find your devices?

I've also just added 1, 5, and 10 minute intervals to the latest version. What other sensor data were you interested in being able to access, did you have any specific data in mind?

@pfmiller would it be possible to add support for private sensors?
If you are interested, let me know how i can help.

Sure, it looks like I just need to add a field for the private read key in order to access private devices. Let me know if the new version I just put up works.

@pfmiller when I use this driver with my Dashboard in Sharptools, it returns the value '5 AQI' as a value. Would it be possible to only see the number without the AQI? Thanks for your help!

@pfmiller When i add my API key, the private key and specify my sensor index i get
No sensors found in search area.

I dug into it a bit and it looks the the search query is not valid. I was able to hack it up a bit to get the search query to work, but the json response is WAY different then what your code is expecting. Let me know if its still worth supporting private devices or if it’s best to support public only.

JSON output of:

https://api.purpleair.com/v1/sensors/%sensorindex%?read_key=%Private key%


{
    "api_version": "V1.0.10-0.0.17",
    "time_stamp": 1654295776,
    "data_time_stamp": 1654295737,
    "sensor": {
        "sensor_index": 46491,
        "last_modified": 1579901318,
        "date_created": 1579615688,
        "last_seen": 1654295691,
        "private": 1,
        "is_owner": 0,
        "name": "Norwalk Square Inside",
        "icon": 0,
        "location_type": 1,
        "model": "PA-II",
        "hardware": "2.0+BME280+PMSX003-B+PMSX003-A",
        "led_brightness": 15,
        "firmware_version": "6.01",
        "firmware_upgrade": "6.06a",
        "rssi": -57,
        "uptime": 3,
        "memory": 15536,
        "position_rating": 5,
        "latitude": 30.289534,
        "longitude": -97.77127,
        "altitude": 540,
        "channel_state": 3,
        "channel_flags": 0,
        "channel_flags_manual": 0,
        "channel_flags_auto": 0,
        "confidence": 100,
        "confidence_auto": 100,
        "confidence_manual": 100,
        "humidity": 31,
        "humidity_a": 31,
        "temperature": 88,
        "temperature_a": 88,
        "pressure": 991.8,
        "pressure_a": 991.84,
        "analog_input": 0.0,
        "pm1.0": 17.5,
        "pm1.0_a": 16.8,
        "pm1.0_b": 18.2,
        "pm1.0_atm": 17.5,
        "pm1.0_cf_1": 17.5,
        "pm2.5": 27.3,
        "pm2.5_a": 26.2,
        "pm2.5_b": 28.4,
        "pm2.5_atm": 27.2,
        "pm2.5_cf_1": 27.3,
        "pm2.5_alt": 16.7,
        "pm2.5_alt_a": 16.2,
        "pm2.5_alt_b": 17.3,
        "pm10.0": 30.9,
        "pm10.0_a": 30.0,
        "pm10.0_b": 31.8,
        "pm10.0_atm": 30.9,
        "pm10.0_cf_1": 30.9,
        "scattering_coefficient": 46.3,
        "scattering_coefficient_a": 46.2,
        "scattering_coefficient_b": 46.3,
        "deciviews": 18.8,
        "deciviews_a": 18.8,
        "deciviews_b": 18.8,
        "visual_range": 59.5,
        "visual_range_a": 59.6,
        "visual_range_b": 59.5,
        "0.3_um_count": 3083,
        "0.3_um_count_a": 3082,
        "0.3_um_count_b": 3085,
        "0.5_um_count": 924,
        "0.5_um_count_a": 883,
        "0.5_um_count_b": 966,
        "1.0_um_count": 189,
        "1.0_um_count_a": 182,
        "1.0_um_count_b": 196,
        "2.5_um_count": 17,
        "2.5_um_count_a": 16,
        "2.5_um_count_b": 18,
        "5.0_um_count": 3,
        "5.0_um_count_a": 2,
        "5.0_um_count_b": 4,
        "10.0_um_count": 0,
        "10.0_um_count_a": 1,
        "10.0_um_count_b": 0,
        "pm1.0_atm_a": 16.8,
        "pm2.5_atm_a": 26.22,
        "pm10.0_atm_a": 30.05,
        "pm1.0_cf_1_a": 16.8,
        "pm2.5_cf_1_a": 26.22,
        "pm10.0_cf_1_a": 30.05,
        "pm1.0_atm_b": 18.18,
        "pm2.5_atm_b": 28.22,
        "pm10.0_atm_b": 31.8,
        "pm1.0_cf_1_b": 18.18,
        "pm2.5_cf_1_b": 28.36,
        "pm10.0_cf_1_b": 31.8,
        "primary_id_a": 966657,
        "primary_key_a": "HV4R1GXN3HCTB0C6",
        "primary_id_b": 966659,
        "primary_key_b": "6PIPXP9HQOFYX1WJ",
        "secondary_id_a": 966658,
        "secondary_key_a": "TEWT0HJJ8NMV4IEN",
        "secondary_id_b": 966660,
        "secondary_key_b": "ZRKBF1H9SN03X9L3",
        "stats": {
            "pm2.5": 27.3,
            "pm2.5_10minute": 40.4,
            "pm2.5_30minute": 36.0,
            "pm2.5_60minute": 26.2,
            "pm2.5_6hour": 27.2,
            "pm2.5_24hour": 21.4,
            "pm2.5_1week": 12.9,
            "time_stamp": 1654295691
        },
        "stats_a": {
            "pm2.5": 26.2,
            "pm2.5_10minute": 38.8,
            "pm2.5_30minute": 34.5,
            "pm2.5_60minute": 25.2,
            "pm2.5_6hour": 26.4,
            "pm2.5_24hour": 20.8,
            "pm2.5_1week": 12.6,
            "time_stamp": 1654295691
        },
        "stats_b": {
            "pm2.5": 28.4,
            "pm2.5_10minute": 42.0,
            "pm2.5_30minute": 37.5,
            "pm2.5_60minute": 27.3,
            "pm2.5_6hour": 28.1,
            "pm2.5_24hour": 21.9,
            "pm2.5_1week": 13.2,
            "time_stamp": 1654295691
        }
    }
}