You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
151 lines
4.8 KiB
151 lines
4.8 KiB
use clap::Parser;
|
|
use derive_more::From;
|
|
use scraper::{Html, Selector};
|
|
use colored::Colorize;
|
|
|
|
#[derive(Default, Parser)]
|
|
#[clap(author, version, about)]
|
|
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 value without descriptor")]
|
|
raw: 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,
|
|
#[clap(short, long, help = "Use color formatting tags for lemonbar and polybar")]
|
|
bar: bool,
|
|
}
|
|
|
|
#[derive(From, Debug)]
|
|
enum Error {
|
|
ReqwestError(reqwest::Error),
|
|
HttpError,
|
|
InvalidResponse,
|
|
}
|
|
|
|
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 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)
|
|
};
|
|
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!("{}", if arguments.concentration {
|
|
concentration
|
|
} else {
|
|
aqi
|
|
});
|
|
return Ok(());
|
|
}
|
|
let display = if arguments.concentration {
|
|
format!("{concentration} µg/m³")
|
|
} else {
|
|
format!("{aqi} AQI")
|
|
};
|
|
if arguments.bar {
|
|
print!("%{{F#{}}}", match aqi {
|
|
150.. => "b48ead",
|
|
100.. => "bf616a",
|
|
50.. => "d08770",
|
|
20.. => "ebcb8b",
|
|
0.. => "a3be8c",
|
|
});
|
|
}
|
|
print!("{}", descriptor);
|
|
println!("{} {}",
|
|
match aqi {
|
|
250.. => "Dangerous".purple(),
|
|
150.. => "Very Unhealthy".purple(),
|
|
100.. => "Unhealthy".magenta(),
|
|
50.. => "Poor".red(),
|
|
20.. => "Fair".yellow(),
|
|
0.. => "Excellent".green(),
|
|
}.bold(),
|
|
match aqi {
|
|
// 250.. => (no appropriate terminal color) // Dangerous 250+
|
|
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.bar {
|
|
print!("%{{F-}}");
|
|
}
|
|
return Ok(());
|
|
}
|