NAME

Weather::PurpleAir::API -- Client interface to Purple Air air quality API

SYNOPSIS

use Weather::PurpleAir::API;
use Data::Dumper;

my $api = Weather::PurpleAir::API->new();
my @sensors = qw(38887 22961 26363);
my $report = $api->report(\@sensors);
for my $sensor_name (keys %$report) {
  print join("\t", ($sensor_name, @{$report->{$sensor_name}})), "\n";
}

DESCRIPTION

Weather::PurpleAir::API provides a convenient interface to the Purple Air air quality API. It will pull down data for specified sensors, transform them as desired (for instance, converting from raw PM2.5 concentration to USA EPA AQI number, averaging results from dual-sensor nodes, etc) and provide a concise report by sensor name.

The Purple Air map of sensors is located at https://www.purpleair.com/map.

This module is very much a work in progress and 0.x releases might not be suitable for use.

For a simple commandline script wrapping this module, see bin/aqi in this package.

Please do not poll sensors too frequently or the Purple Air server might block your IP.

PACKAGE ATTRIBUTES

$Weather::PurpleAir::API::VERSION (string)

The version number of this package (#.## format).

OBJECT ATTRIBUTES

sensor_hr (hashref)

Keys on sensor ID numbers, maps to sensor names.

It is empty until the report() method is called.

name_hr (hashref)

Keys on sensor names, maps to sensor ID numbers.

It is empty until the report() method is called.

ok (string)

Indicates status of most recent operation: "OK", "WARNING" or "ERROR".

    * "OK" means everything completed as expected.

    * "WARNING" means everything completed and possibly-useful data was returned, but something suspicious happened.

    * "ERROR" means something went horribly wrong and the operation was unable to complete.

err (string or arrayref)

Set by WARNING and ERROR conditions, describes details of what went wrong.

n_err (integer)

Incremented every time an error occurs.

n_warn (integer)

Incremented every time a warning occurs.

ex (exception or exception string)

Set to $@ when an exception is caught (for instance, when a JSON string does not decode).

js_or (JSON::MaybeXS object reference)

Contains a reference to the JSON decoder object used internally. May be overridden by the user when different behavior is desired. The default instance has parameters ascii = 1, allow_nonref => 1, space_after => 1>.

METHODS

All functionality is available via a Weather::PurpleAir::API object. Start with new and use the resulting object to generate reports. Additional functionality (like finding sensors by name, or finding sensors near locations) will come in future releases.

new(%options)

Instantiates and returns a Weather::PurpleAir::API object. Options passed here may be overridden by passing options to methods of the object.

All options have hopefully-sane defaults:

api_url = string

Override the URL at which the API is queried. Useful for testing, or if you have your own server.

Default is "https://www.purpleair.com/json?show=", to which the sensor ID is appended by the sensor_url method.

average = 0 or 1

Averages AQI metrics from a node's sensors instead of returning the AQI metrics from each sensor in a node.

Default is 0 (do not average).

d = 0 or 1

Activate debugging logic, which will print horribly confusing things to STDOUT. Default is 0 (off).

g = 0 or 1

Indicate that reports should include a "GUESSING" entry, providing a pruned average of all results from all sensors. In future releases it might incorporate other heuristics (like using older cached values when bad metrics are detected).

The format of this entry is a little different from the sensor entries. Instead of $report->{GUESSING} referring to an array of AQI metrics, it refers to an array containing the guessed gestalt AQI metric and its standard deviation (a measure of statistical skew). The higher the standard deviation, the lower the confidence you should have in the guessed metric.

For example:

{ GUESSING => [123.45, 1.09]}  # AQI is 123.45, with high confidence
{ GUESSING => [123.45, 10.3]}  # AQI is 123.45, with somewhat less confidence
{ GUESSING => [123.45, 67.5]}  # AQI is 123.45, with very low confidence!

Default is 0 (do not guess).

http_or = HTTP::Tiny object

Provide the HTTP::Tiny instance used to query the API. This is useful when you need to customize the query timeout, set a user agent string or specify an https proxy.

Default is undef (an HTTP::Tiny object will be instantiated internally).

no_errors = 0 or 1

Set this to suppress writing error messages to stderr. Default is 0 (errors will be displayed).

no_warnings = 0 or 1

Set this to suppress writing warning messages to stderr. Default is 0 (warnings will be displayed).

now = 0 or 1

Normally reports will use the ten-minute average AQI from each sensor. Specify now to use the current AQI instead.

Default is 0 (report will use ten-minute average AQI metrics).

prune_threshold = fraction between 0 and 1

When the g (GUESSING) option is set, the guessing heuristic may prune more then one high and one low outlier from the sensor data, if doing so will leave sufficient data left over for meaningful averaging. The prune threshold determines how close to an outlier other data points must be, as a fraction of the outlier value, for them to be pruned as well.

For instance, if prune_threshold = 0.1 (10%) and the high outlier is 90, then 90 * 0.1 = 9, so data points which are 81 or higher might also be pruned. If prune_threshold = 0.05 (5%) and the high outlier is 90, 90 * 0.05 = 4.5, so data points which are 85.5 or higher might be pruned, etc.

Default is 0.1 (10%).

q = 0 or 1

"q" is for "quiet". Setting this is equivalent to setting no_errors and no_warnings.

Default is 0.

raw = 0 or 1

When set, report will not convert the API's raw concentration numbers to USA EPA PM2.5 AQI scores (the metric displayed on the Purple Air website map). Part-per-million concentrations of 2.5 micrometer diameter particles will be provided instead.

Default is 0 (concentrations will be converted to USA EPA PM2.5 AQI scores).

sensor = ID-number
sensor = [ID-number, ID-number, ...]
sensor = "ID-number ID-number ..."

Normally sensor IDs are passed to the report method, but a default sensor or sensors can also be given to new at object instantiation time.

Right now Weather::PurpleAir::API can only work with numeric sensor IDs and there isn't a really good way to find the IDs of sensors. If you point a browser at the Purple Air map, some browsers will expose the IDs of specific sensors and others will not.

If you download the all-sensors blob (either from the API at https://www.purpleair.com/json or the cached blob at http://ciar.org/h/sensors.all.json) you can pick through the list and find IDs of sensors of interest, which is a pain in the ass.

Future releases of Weather::PurpleAir::API will support functions for finding sensors near a location, but this one doesn't, which is one of the reasons it's a 0.x release.

Some example sensors and their IDs:

25407   Gravenstein School
38887   Litchfield
22961   City Of Santa Rosa Laguna Treatment Plant
26363   Geek Orchard

The default is 25407 (Gravenstein School, in Sonoma County, California)

stash_path = path string

When a stash_path is provided, report will store a copy of each sensor's JSON blob in the specified directory, under the name aqi.$sensor_id.json.

For example, if stash_path = "/tmp" and sensor = 25407, the JSON blob will be stored in file "/tmp/aqi.25407.json".

The default is undef (not set, will not stash).

v = 0 or 1

"v" is for verbosity. When set, the reported-upon sensors (and gestalt guess, when g parameter is set) will be printed to stdout. This is normally set by the bin/aqi utility.

Future releases might implement higher verbosity levels.

The default is 0 (do not print to stdout).

report()

report([sensor-id, sensor-id, ...])

report([sensor-id, sensor-id, ...], {option = value, ...})>

    The report method retrieves current data from each specified sensor and returns a hashref with sensor name keys and arrayref values. The values represent the air quality at the sensor's location (where higher values = more gunk in the air). Higher than 50 is bad, lower than 20 is good.

    If no list of sensor IDs is used, and no list was provided to new(sensor = ...)> a default of 25407 (Gravenstein School) will be used, which probably is not very useful to you.

    See the notes under sensor in the section for new regarding sensor IDs and how to find them.

    report optionally accepts a hashref options, which are the same as those documented for new.

    Returns undef on error and sets the object's ok and err attributes.

    Returns a hashref as described on success and sets the object's ok and err attributes to "OK" and "" respectively.

AUTHOR

TTK Ciar, <ttk[at]ciar[dot]org>

COPYRIGHT AND LICENSE

Copyright 2020 by TTK Ciar

This library is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

concentration_to_epa($concentration)

This method implements the official USA EPA guideline for converting PM2.5 concentration to AQI, per https://www3.epa.gov/airnow/aqi-technical-assistance-document-sept2018.pdf.

The sensors do not report AQI metrics, only raw concentrations, so conversion is necessary. The report method does this automatically (unless the raw option is set).

The official equation is a dumb-ass piecewise function of linear interpolations, but nobody ever said government was smart.

Takes a PM2.5 concentration as input, returns an AQI metric as output. The EPA guideline asserts that the AQI must be truncated to a whole integer. I leave that as an exercise for the programmer.

ABOUT RELIABILITY

The sensor readings are not always reliable. A car starting or a person smoking near the sensor can produce a false high reading. Right now the workaround is to specify multiple sensors and use the -g option. Future releases will provide alternative remedies.

TO DO

Write more documentation. Some methods are undocumented.

Write more unit tests.

Save some state and perform time averaging, perhaps throw out dramatic changes and reuse last known value.

Pull down and cache the all-sensors blob, implement relevant operations:

    * find sensors by name

    * find sensors nearest a latitude/longitude, or near another sensor, maybe do some light GIS-fu

HISTORY

This was originally a throw-away script that just yanked JSON from the API, parsed out the sensor name and first reading's PM2.5 concentration, and printed to stdout.

It was quickly apparent that this left something to be desired, so the script was refactored into this module and associated wrapper utility.

My friend Matt using the throw-away script made me feel embarrassed at its shortcomings, which provided some of the motivation to do better.

SEE ALSO

The PurpleAir Website

The PurpleAir API FAQ (stored in more convenient form by purpleairpy project)

Python API Client by ReagentX, providing a different approach to the interface.

Air Concentration to USA EPA PM2.5 AQI Calculator an unfortunately crude tool corresponding PM2.5 concentration to the AQI metric.

USA EPA PM2.5 AQI Calculation Guidelines