Weather::PurpleAir::API(3)

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).

"temperature" => "C" or "F"
    Also display the sensor's reported temperature, in either degrees Celsius or Fahrenheit.

"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.

  concentration_to_epa($concentration)
This method implements the official USA EPA guideline for converting PM2.5 concentration to AQI, per <https://www.airnow.gov/sites/default/files/2020-05/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.

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.

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 <http://purpleair.com>

The PurpleAir API FAQ <https://github.com/bomeara/purpleairpy/blob/master/api.md> (stored in more convenient form by purpleairpy project)

Python API Client <https://github.com/ReagentX/purple_air_api> by ReagentX, providing a different approach to the interface.

Air Concentration to USA EPA PM2.5 AQI Calculator <https://aqicn.org/calculator/> an unfortunately crude tool corresponding PM2.5 concentration to the AQI metric.

USA EPA PM2.5 AQI Calculation Guidelines <https://www3.epa.gov/airnow/aqi-technical-assistance-document-sept2018.pdf>