IQAir api access crate
init
| -rw-r--r-- | .gitignore | 1 | ||||
| -rw-r--r-- | Cargo.toml | 18 | ||||
| -rw-r--r-- | LICENSE | 21 | ||||
| -rw-r--r-- | README.md | 12 | ||||
| -rw-r--r-- | src/lib.rs | 192 | ||||
| -rw-r--r-- | src/main.rs | 26 |
6 files changed, 270 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ffa3bbd --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +Cargo.lock
\ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..35de837 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "iqair" +version = "0.1.1" +authors = ["bend-n <[email protected]>"] +license = "MIT" +edition = "2021" +description = "IQAir api accessor crate" +repository = "https://github.com/bend-n/iqair" +exclude = ["tdata", "benches/", ".gitignore"] +keywords = ["pollution", "api"] +categories = ["command-line-utilities", "api-bindings"] + +[dependencies] +serde = "1" +serde_derive = "1.0.217" +serde_json = "1.0.138" +time = { version = "0.3", features = ["formatting", "parsing", "serde"] } +ureq = "3.0.4" @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 bendn + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..850b2a3 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# iqair + +provides access to iqair's numbers. +uses the `nearest_city` endpoint. + +use either as library or binary. +binary usage allows + +``` +> iqair API-KEY +96 +``` diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ed585e4 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,192 @@ +use serde_derive::Deserialize; +use serde_derive::Serialize; +use time::OffsetDateTime; + +#[derive(Debug, Deserialize)] +struct Dat { + data: Data, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Data { + pub city: String, + pub state: String, + pub country: String, + pub location: Location, + pub current: Current, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Location { + /// Lat, Lon + pub coordinates: (f64, f64), +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Current { + pub pollution: Pollution, + pub weather: Weather, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub struct Pollution { + #[serde(rename = "ts")] + #[serde(with = "time::serde::iso8601")] + pub timestamp: OffsetDateTime, + // AQI value based on US EPA standard + #[serde(rename = "aqius")] + pub aqi_us: u32, + /// main pollutant for US AQI + #[serde(rename = "mainus")] + pub main_us: String, + #[serde(rename = "aqicn")] + pub aqi_cn: u32, + /// main pollutant for CN AQI + #[serde(rename = "maincn")] + pub main_cn: String, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct Weather { + #[serde(rename = "ts")] + #[serde(with = "time::serde::iso8601")] + pub timestamp: OffsetDateTime, + #[serde(rename = "tp")] + /// temperature in Celsius + pub temp: i32, + #[serde(rename = "pr")] + /// atmospheric pressure in hPa + pub pressure: i32, + #[serde(rename = "hu")] + /// humidity % + pub humidity: u16, + #[serde(rename = "ws")] + /// wind speed (m/s) + pub wind_speed: f32, + #[serde(rename = "wd")] + /// wind direction, as an angle of 360° (N=0, E=90, S=180, W=270) + pub wind_direction: i16, + #[serde(rename = "ic")] + /// weather icon code, see below for icon index + pub icon: Icon, +} + +#[derive(Debug, Deserialize, Serialize, Clone)] +pub enum Icon { + #[serde(rename = "01d")] + ClearDaySky, + #[serde(rename = "01n")] + ClearNightSky, + #[serde(rename = "02d")] + FewDayClouds, + #[serde(rename = "02n")] + FewNightClouds, + #[serde(rename = "03d")] + ScatteredClouds, + #[serde(rename = "04d")] + BrokenClouds, + #[serde(rename = "09d")] + ShowerRain, + #[serde(rename = "10d")] + DayRain, + #[serde(rename = "10n")] + NightRain, + #[serde(rename = "11d")] + Thunderstorm, + #[serde(rename = "13d")] + Snow, + #[serde(rename = "50d")] + Mist, + #[serde(untagged)] + Other(String), +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Error { + Success, + /// when minute/monthly limit is reached. + CallLimitReached, + /// when API key is expired. + ApiKeyExpired, + /// returned when using wrong API key. + IncorrectApiKey, + /// when service is unable to locate IP address of request. + IpLocationFailed, + /// when there is no nearest station within specified radius. + NoNearestStation, + /// when call requests a feature that is not available in chosen subscription plan. + FeatureNotAvailable, + /// when more than 10 calls per second are made. + TooManyRequests, + #[serde(skip)] + Io(std::io::Error), + #[serde(skip)] + Ureq(ureq::Error), + #[serde(skip)] + Serde(serde_json::Error), +} +impl std::fmt::Display for Error { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + use Error::*; + match self { + Success => unreachable!(), + CallLimitReached => f.write_str("minute/monthly limit is reached"), + ApiKeyExpired => f.write_str("API key is expired"), + Error::Ureq(ureq::Error::StatusCode(403)) | IncorrectApiKey => { + f.write_str("using wrong API key") + } + IpLocationFailed => f.write_str("service is unable to locate IP address of request"), + NoNearestStation => f.write_str("there is no nearest station within specified radius"), + FeatureNotAvailable => f.write_str( + "call requests a feature that is not available in chosen subscription plan", + ), + TooManyRequests => f.write_str("more than 10 calls per second are made"), + Error::Io(error) => write!(f, "{error}"), + Error::Ureq(error) => write!(f, "{error}"), + Error::Serde(error) => write!(f, "{error}"), + } + } +} +impl std::error::Error for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + Some(match self { + Error::Io(error) => error, + Error::Ureq(error) => error, + Error::Serde(error) => error, + _ => return None, + }) + } +} + +/// Get pollution data for nearest city. +pub fn nearest(key: &str) -> std::result::Result<Data, Error> { + let uri = format!("https://api.airvisual.com/v2/nearest_city?key={key}"); + let mut result = String::with_capacity(50); + std::io::Read::read_to_string( + &mut ureq::get(uri) + .call() + .map_err(Error::Ureq)? + .body_mut() + .as_reader(), + &mut result, + ) + .map_err(Error::Io)?; + #[derive(Deserialize)] + struct Status { + status: Error, + } + let Status { status } = serde_json::from_str::<Status>(&result).map_err(Error::Serde)?; + if !matches!(status, Error::Success) { + return Err(status); + } + serde_json::from_str::<Dat>(&result) + .map_err(Error::Serde) + .map(|Dat { data }| data) +} + +#[test] +fn x() { + dbg!(nearest("294e67bb-404b-41b5-a73d-60bb71f361f2").unwrap()); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2aa395a --- /dev/null +++ b/src/main.rs @@ -0,0 +1,26 @@ +use std::env::args; + +fn main() -> Result<(), iqair::Error> { + if matches!( + args().nth(1).as_deref(), + Some("--help" | "-h" | "help" | "h") + ) { + println!("{} (KEY) (raw)", args().next().unwrap()); + println!("provides iqair data for nearest city"); + println!("data refreshes hourly"); + return Ok(()); + } + let Some(key) = args().nth(1).or(std::env::var("AIR_KEY").ok()) else { + println!("\x1b[31;1mno key provided!\x1b[0m"); + println!("get one at https://dashboard.iqair.com/personal/api-keys for free"); + println!("then set AIR_KEY or pass it to this binary"); + std::process::exit(0); + }; + let data = iqair::nearest(&key)?; + if args().nth(1).as_deref() == Some("raw") || args().nth(2).as_deref() == Some("raw") { + println!("{}", serde_json::to_string_pretty(&data).unwrap()); + } else { + println!("{}", data.current.pollution.aqi_us) + } + Ok(()) +} |