use chrono::{DateTime, Utc};
use reqwest::Client;
use serde::Serialize;
use serde::{self, Deserialize};
use serde_json::Value;
use thiserror::Error;
const URL: &str = "https://data.sensor.community";
#[derive(Error, Debug)]
pub enum FetchError {
#[error("network request failed")]
NetworkError(#[from] reqwest::Error),
#[error("failed to parse JSON")]
JsonError(#[from] serde_json::Error),
#[error("unsupported value")]
UnsupportedError(),
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Location {
pub altitude: String,
pub country: String,
pub exact_location: u64,
pub id: u64,
pub indoor: u64,
pub latitude: String,
pub longitude: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SensorType {
pub id: u64,
pub manufacturer: String,
pub name: String,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Sensor {
pub id: u64,
pub pin: String,
pub sensor_type: SensorType,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SensorDataValue {
pub id: Option<u64>,
pub value: Value,
pub value_type: String,
}
#[derive(Deserialize, Debug)]
pub struct Sample {
pub id: u64,
pub location: Location,
pub sensor: Sensor,
pub sensordatavalues: Vec<SensorDataValue>,
#[serde(with = "simple_date_format")]
pub timestamp: DateTime<Utc>,
}
#[derive(Serialize, Debug)]
pub struct SampleUpload {
pub software_version: String,
pub sensordatavalues: Vec<SensorDataValue>,
}
mod simple_date_format {
use chrono::{DateTime, NaiveDateTime, Utc};
use serde::{self, Deserialize, Deserializer};
const FORMAT: &str = "%Y-%m-%d %H:%M:%S";
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
let dt = NaiveDateTime::parse_from_str(&s, FORMAT).map_err(serde::de::Error::custom)?;
Ok(DateTime::<Utc>::from_naive_utc_and_offset(dt, Utc))
}
}
pub async fn fetch_by_query(q: &str, contact: &str) -> Result<Vec<Sample>, FetchError> {
const URL: &str = "https://data.sensor.community/airrohr/v1/filter/";
let url = format!("{URL}{q}");
let client = Client::new();
let resp = client.get(url).header("User-Agent", contact).send().await?;
Ok(resp.json::<Vec<Sample>>().await?)
}
pub async fn fetch_batch(selector: &str, contact: &str) -> Result<Vec<Sample>, FetchError> {
let valid = ["", "1h", "24h", "dust.min", "temp.min"];
if !valid.contains(&selector) {
return Err(FetchError::UnsupportedError());
}
let url = {
if selector.is_empty() {
format!("{URL}/static/v2/data.json")
} else {
format!("{URL}/static/v2/data.{selector}.json")
}
};
let client = Client::new();
let resp = client.get(url).header("User-Agent", contact).send().await?;
Ok(resp.json::<Vec<Sample>>().await?)
}
pub async fn fetch_by_sensor(api_id: u64, contact: &str) -> Result<Vec<Sample>, FetchError> {
let url = format!("https://data.sensor.community/airrohr/v1/sensor/{api_id}/");
let client = Client::new();
let resp = client.get(url).header("User-Agent", contact).send().await?;
Ok(resp.json::<Vec<Sample>>().await?)
}
pub async fn upload(
station_uinq_id: &str,
pin: u32,
sample: SampleUpload,
contact: &str,
) -> Result<(), FetchError> {
const URL: &str = "https://api.sensor.community/v1/push-sensor-data/";
let client = Client::new();
let resp = client
.post(URL)
.header("User-Agent", contact)
.header("X-PIN", pin)
.header("X-Sensor", station_uinq_id)
.json(&sample)
.send()
.await?;
Ok(resp.json().await?)
}