#!/bin/perl # For temperature: subtract 8 from the F value for the actual temperature # Get PurpleAir sensor ID if ($ENV{'SENSOR_ID'} == "") { print STDERR "Missing required environment variable 'SENSOR_ID'.\n"; exit 1; } $sensor_id = $ENV{'SENSOR_ID'}; # Get measurement type. Default: US_AQI @valid_types = ("US_AQI", "EU_AQI", "CA_AQHI", "IMECA", "IAS"); $type = 'US_AQI'; if (grep(/^$ENV{'TYPE'}$/, @valid_types)) { $type = $ENV{'TYPE'}; } elsif ($ENV{'TYPE'}) { print STDERR "Unrecognized TYPE: $ENV{'TYPE'}. Valid values are: [@valid_types]"; exit 1; } # Read pm2.5 10-minute average from the PurpleAir sensor $pm2_5 = `curl "http://purpleair.com/json?show=$sensor_id" 2>/dev/null \\ | jq -r '.results | map(select(has("Stats")) | .Stats | fromjson | .v1) | add / length'`; ################### # IMPLEMENTATIONS # ################### sub UsAqi { # See https://forum.airnowtech.org/t/the-aqi-equation/169 # # PM2.5 | ConcLo | ConcHi | AQILo | AQIHi # Good | 0.0 | 12.0 | 0 | 50 # Moderate | 12.1 | 35.4 | 51 | 100 # Unhealthy Sensitive | 35.5 | 55.4 | 101 | 150 # Unhealthy | 55.5 | 150.4 | 151 | 200 # Very Unhealthy | 150.5 | 250.4 | 201 | 300 # Hazardous | 250.5 | 500.4 | 301 | 500 # # |------------Multiplier-----------| Threshold Addition # (AQIHi - AQILo) / (ConcHi - ConcLo) * (ConcNow - ConcLo) + AQILo @colors = ("#00e400", "#ffff00", "#ff7e00", "#ff0000", "#8f3f97", "#7e0023"); $pm2_5 = $_[0]; # Table of values: Threshold, Multiplier, Addition @t = ( [0.0, 4.1667, 0.0], [12.1, 2.1030, 51], [35.5, 2.4623, 101], [55.5, 0.5163, 151], [150.5, 0.9910, 201], [250.5, 0.7963, 301] ); # Find relevant row for computation based on pm2.5 threshold for ($i = 5; $i >= 0; $i--) { if ($pm2_5 >= $t[$i][0]) { $r = $i; last; } } # Compute AQI from raw pm2.5 concentration $aqi = $t[$r][1] * ($pm2_5 - $t[$r][0]) + $t[$r][2]; return ( 'val' => sprintf("%.0f", $aqi), 'colors' => [@colors], 'color_idx' => $r ); } sub EuAqi { # See https://airindex.eea.europa.eu/Map/AQI/Viewer/ # European Air Quality Index uses the pm2.5 concentration directly. @colors = ("#50f0e6", "#50ccaa", "#f0e641", "#ff5050", "#960032", "#7d2181"); $pm2_5 = $_[0]; # Thresholds for color indices @t = (0.0, 10.0, 20.0, 25.0, 50.0, 75.0); # Find relevant row for computation based on pm2.5 threshold for ($i = 5; $i >= 0; $i--) { if ($pm2_5 >= $t[$i]) { $r = $i; last; } } return ( 'val' => sprintf("%.0f", $pm2_5), 'colors' => [@colors], 'color_idx' => $r ); } sub CaAqhi { # See https://en.wikipedia.org/wiki/Air_Quality_Health_Index_(Canada)#Calculation # Note: AQHI should also incorporate O3 and NO2, but PurpleAir only has PM2.5 # The pm2.5 calculation is used alone and multiplied by 3. # 1-3: Low # 4-6: Moderate # 7-10: High # 11+: Very High @colors = ( "#00ccff", "#0099cc", "#006699", "#ffff00", "#ffcc00", "#ff9933", "#ff6666", "#ff0000", "#cc0000", "#990000", "#660000"); $pm2_5 = $_[0]; $aqhi = 3 * 1000 / 10.4 * ((exp(0.000487 * $pm2_5) - 1)); return ( 'val' => sprintf("%.0f", $aqhi >= 1 ? $aqhi : 1), 'colors' => [@colors], 'color_idx' => $r ); } sub Imeca { # See http://rama.edomex.gob.mx/imeca # # PM2.5 | Threshold | Multiplier | Addition # Buena | 0.0 | 4.17 | 0 # Regular | 12.1 | 1.49 | 51 # Mala | 45.1 | 0.94 | 101 # Muy Mala | 97.5 | 0.93 | 151 # Extremadamente Mala | 150.5 | 0.99 | 201 # Extremadamente Mala | 250.5 | 0.99 | 301 # Extremadamente Mala | 350.5 | 0.66 | 401 # # Multiplier * (pm2.5 - Threshold) + Addition @colors = ("#00e400", "#ffff00", "#ff7e00", "#ff0000", "#99004c", "#99004c", "#99004c"); $pm2_5 = $_[0]; # Table of values: Threshold, Multiplier, Addition @t = ( [0.0, 4.17, 0.0], [12.1, 1.49, 51.0], [45.1, 0.94, 101.0], [97.5, 0.93, 151.0], [150.5, 0.99, 201.0], [250.5, 0.99, 301.0], [350.5, 0.66, 401.0] ); # Find relevant row for computation based on pm2.5 threshold for ($i = 6; $i >= 0; $i--) { if ($pm2_5 >= $t[$i][0]) { $r = $i; last; } } # Compute AQI from raw pm2.5 concentration $aqi = $t[$r][1] * ($pm2_5 - $t[$r][0]) + $t[$r][2]; return ( 'val' => sprintf("%.0f", $aqi), 'colors' => [@colors], 'color_idx' => $r ); } sub Ias { # See https://www.dof.gob.mx/nota_detalle_popup.php?codigo=5576807 # Índice de aire y salud uses the pm2.5 concentration directly. @colors = ("#00e400", "#ffff00", "#ff7e00", "#ff0000", "#99004c"); $pm2_5 = $_[0]; # Thresholds for color indices @t = (0.0, 26.0, 46.0, 80.0, 148.0); # Find relevant row for computation based on pm2.5 threshold for ($i = 4; $i >= 0; $i--) { if ($pm2_5 >= $t[$i]) { $r = $i; last; } } return ( 'val' => sprintf("%.0f", $pm2_5), 'colors' => [@colors], 'color_idx' => $r ); } ################## # OUTPUT RESULTS # ################## # Compute requested value # Expected return value is a hash of the form: # ( # 'val' => , # 'colors' => [], # 'color_idx' => <0-based index of color array>, # ) if ($type eq "US_AQI") { %result = UsAqi($pm2_5); } elsif ($type eq "EU_AQI") { %result = EuAqi($pm2_5); } elsif ($type eq "CA_AQHI") { %result = CaAqhi($pm2_5); } elsif ($type eq "IMECA") { %result = Imeca($pm2_5); } elsif ($type eq "IAS") { %result = Ias($pm2_5); } else { # It should not be possible to reach here. %result = ( 'val' => 'ERR', 'colors' => ("#ff0000"), 'color_idx' => 0, 'num_colors' => 1 ); } # Get color overrides, if any. if ($ENV{'COLORS'}) { @colors = split(',', $ENV{'COLORS'}); if (scalar(@colors) < scalar(@{ $result{'colors'} })) { print STDERR "Warning: number of color overrides is fewer than the number". " of buckets in type $type\n"; } } else { @colors = @{ $result{'colors'} }; } # Adjust color index if greater than colors length $color_idx = $result{'color_idx'}; if ($color_idx >= scalar(@colors)) { $color_idx = scalar(@colors) - 1; } # Full text, short text, color print "$result{'val'}\n"; print "$result{'val'}\n"; if (!$ENV{'NO_COLOR'}) { print "$colors[$color_idx]\n"; }