IQAir api access crate
bendn 11 months ago
commit 417fefe
-rw-r--r--.gitignore1
-rw-r--r--Cargo.toml18
-rw-r--r--LICENSE21
-rw-r--r--README.md12
-rw-r--r--src/lib.rs192
-rw-r--r--src/main.rs26
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"
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..bf3f588
--- /dev/null
+++ b/LICENSE
@@ -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(())
+}