@ -8,10 +8,18 @@ use colored::Colorize;
struct Arguments {
#[ clap(help = " Plume Labs location ID. For example, https://air.plumelabs.com/air-quality-in-XXX " ) ]
location : String ,
#[ clap(short, long, help = " Display raw Air Quality Index " ) ]
#[ clap(short, long, help = " Display raw value without descriptor " ) ]
raw : bool ,
#[ clap(short, long, help = " Display AccuWeather Air Quality Index description " ) ]
description : bool ,
#[ clap(short, long, help = " Display raw concentration in µg/m³ " ) ]
concentration : bool ,
#[ clap(short = 'p', long = " pm25 " , help = " Display fine PM2.5 inhalable particulate matter " ) ]
fine_particulate : bool ,
#[ clap(short = 'P', long = " pm10 " , help = " Display coarse PM10 inhalable particulate matter " ) ]
coarse_particulate : bool ,
#[ clap(short, long = " no2 " , help = " Display nitrogen dioxide NO2 " ) ]
nitrogen_dioxide : bool ,
#[ clap(short, long = " o3 " , help = " Display ozone O3 " ) ]
ozone : bool ,
}
#[ derive(From, Debug) ]
@ -21,26 +29,91 @@ enum Error {
InvalidResponse ,
}
fn main ( ) -> Result < ( ) , Error > {
let arguments = Arguments ::parse ( ) ;
let response = reqwest ::blocking ::get ( format! ( "https://air.plumelabs.com/air-quality-in-{}" , arguments . location ) ) ? ;
struct Reading {
concentration : u32 ,
aqi : u32 ,
}
struct Data {
aqi : u32 ,
fine_particulate : Reading ,
coarse_particulate : Reading ,
nitrogen_dioxide : Reading ,
ozone : Reading ,
}
impl Data {
fn fetch ( location : & str ) -> Result < Self , Error > {
let response = reqwest ::blocking ::get ( format! ( "https://air.plumelabs.com/air-quality-in-{location}" ) ) ? ;
if ! response . status ( ) . is_success ( ) {
return Err ( Error ::HttpError ) ;
}
let content = response . text_with_charset ( "utf-8" ) ? ;
let document = Html ::parse_document ( & content ) ;
let selector = Selector ::parse ( "span[data-role=\"current-pi\"]" ) . unwrap ( ) ;
let aqi = match match document . select ( & selector ) . next ( ) {
let get_value = | selector : & str | {
let selector = selector . to_owned ( ) ;
let selector = Selector ::parse ( & selector ) . unwrap ( ) ;
Ok ( match match document . select ( & selector ) . next ( ) {
Some ( element ) = > element ,
None = > return Err ( Error ::InvalidResponse ) ,
} . inner_html ( ) . trim ( ) . parse ::< f32 > ( ) {
Ok ( aqi ) = > aqi ,
Err ( _ ) = > return Err ( Error ::InvalidResponse ) ,
} as u32 ;
} as u32 )
} ;
let get_reading = | name | -> Result < Reading , Error > {
Ok ( Reading {
concentration : get_value ( & format! ( "li[data-id=\"{name}\"][data-format=\"value_upm\"] div[data-role=\"pollutant-level\"]" ) ) ? ,
aqi : get_value ( & format! ( "li[data-id=\"{name}\"][data-format=\"pi\"] div[data-role=\"pollutant-level\"]" ) ) ? ,
} )
} ;
Ok ( Self {
aqi : get_value ( "span[data-role=\"current-pi\"" ) ? ,
fine_particulate : get_reading ( "PM25" ) ? ,
coarse_particulate : get_reading ( "PM10" ) ? ,
nitrogen_dioxide : get_reading ( "NO2" ) ? ,
ozone : get_reading ( "O3" ) ? ,
} )
}
}
fn main ( ) -> Result < ( ) , Error > {
let arguments = Arguments ::parse ( ) ;
let data = Data ::fetch ( & arguments . location ) ? ;
let descriptor ;
let Reading { aqi , concentration } = if arguments . fine_particulate {
descriptor = "PM2.5 " ;
data . fine_particulate
} else if arguments . coarse_particulate {
descriptor = "PM10 " ;
data . coarse_particulate
} else if arguments . nitrogen_dioxide {
descriptor = "NO2 " ;
data . nitrogen_dioxide
} else if arguments . ozone {
descriptor = "O3 " ;
data . ozone
} else {
descriptor = "" ;
Reading {
aqi : data . aqi ,
concentration : data . aqi ,
}
} ;
if arguments . raw {
println! ( "{}" , aqi ) ;
println! ( "{}" , if arguments . concentration {
concentration
} else {
aqi
} ) ;
return Ok ( ( ) ) ;
}
let display = if arguments . concentration {
format! ( "{concentration} µg/m³" )
} else {
format! ( "{aqi} AQI" )
} ;
print! ( "{}" , descriptor ) ;
println! ( "{} {}" ,
match aqi {
250 .. = > "Dangerous" . purple ( ) ,
@ -52,22 +125,12 @@ fn main() -> Result<(), Error> {
} . bold ( ) ,
match aqi {
// 250.. => (no appropriate terminal color) // Dangerous 250+
150 .. = > aqi. to_string ( ) . purple ( ) , // Very Unhealthy 150-249
100 .. = > aqi. to_string ( ) . magenta ( ) , // Unhealthy 100-149
50 .. = > aqi. to_string ( ) . red ( ) , // Poor 50-99
20 .. = > aqi. to_string ( ) . yellow ( ) , // Fair 20-49
0 .. = > aqi. to_string ( ) . green ( ) , // Excellent 0-19
150 .. = > display . purple ( ) , // Very Unhealthy 150-249
100 .. = > display . magenta ( ) , // Unhealthy 100-149
50 .. = > display . red ( ) , // Poor 50-99
20 .. = > display . yellow ( ) , // Fair 20-49
0 .. = > display . green ( ) , // Excellent 0-19
} . bold ( )
) ;
if arguments . description {
println! ( "{}" , match aqi {
250 .. = > "Any exposure to the air, even for a few minutes, can lead to serious health effects on everybody. Avoid outdoor activities." ,
150 .. = > "Health effects will be immediately felt by sensitive groups and should avoid outdoor activity. Healthy individuals are likely to experience difficulty breathing and throat irritation; consider staying indoors and rescheduling outdoor activities." ,
100 .. = > "Health effects can be immediately felt by sensitive groups. Healthy individuals may experience difficulty breathing and throat irritation with prolonged exposure. Limit outdoor activity." ,
50 .. = > "The air has reached a high level of pollution and is unhealthy for sensitive groups. Reduce time spent outside if you are feeling symptoms such as difficulty breathing or throat irritation." ,
20 .. = > "The air quality is generally acceptable for most individuals. However, sensitive groups may experience minor to moderate symptoms from long-term exposure." ,
0 .. = > "The air quality is ideal for most individuals; enjoy your normal outdoor activities." ,
} ) ;
}
return Ok ( ( ) ) ;
}