roughing in pollen
This commit is contained in:
parent
45ff2ee635
commit
024956a12b
6 changed files with 534 additions and 64 deletions
|
@ -13,6 +13,7 @@ pub struct Config {
|
|||
pub calendar_event_count: usize,
|
||||
pub location: String,
|
||||
pub timezone: String,
|
||||
pub pollen_zip: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
|
71
src/main.rs
71
src/main.rs
|
@ -1,10 +1,12 @@
|
|||
mod config;
|
||||
mod pollen;
|
||||
mod weather;
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
use chrono::{TimeZone, Utc, DateTime};
|
||||
use chrono_tz::Tz;
|
||||
use config::Config;
|
||||
use weather::{fetch_weather, WeatherSummary};
|
||||
use pollen::fetch_pollen_api;
|
||||
use weather::{fetch_weather, geocode_city, WeatherSummary};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
|
@ -24,7 +26,15 @@ async fn main() {
|
|||
None
|
||||
}
|
||||
};
|
||||
if let Some(w) = weather {
|
||||
let (_lat, _lon) = if weather.is_some() {
|
||||
match geocode_city(&config.location, &config.weather_api_key).await {
|
||||
Ok((lat, lon)) => (lat, lon),
|
||||
Err(_) => (0.0, 0.0),
|
||||
}
|
||||
} else {
|
||||
(0.0, 0.0)
|
||||
};
|
||||
if let Some(w) = &weather {
|
||||
if w.obs_time_unix > 0 {
|
||||
match config.timezone.parse::<Tz>() {
|
||||
Ok(tz) => {
|
||||
|
@ -54,8 +64,50 @@ async fn main() {
|
|||
println!("Weather: N/A");
|
||||
}
|
||||
|
||||
let pollen = get_pollen_count(&config);
|
||||
println!("\nPollen count: {pollen}");
|
||||
match fetch_pollen_api(&config.pollen_zip).await {
|
||||
Ok(p) => {
|
||||
// Format pollen forecast date to match weather, in config.timezone
|
||||
if let Ok(dt) = DateTime::parse_from_rfc3339(&p.forecast_date) {
|
||||
match config.timezone.parse::<Tz>() {
|
||||
Ok(tz) => {
|
||||
let local_time = dt.with_timezone(&tz);
|
||||
println!(
|
||||
"\nPollen.com ({})\n Forecast for {}:",
|
||||
p.location,
|
||||
local_time.format("%Y-%m-%d %H:%M:%S %Z")
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
println!(
|
||||
"\nPollen.com ({})\n Forecast for {} (timezone parse error):",
|
||||
p.location, p.forecast_date
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"\nPollen.com ({})\n Forecast for {}:",
|
||||
p.location, p.forecast_date
|
||||
);
|
||||
}
|
||||
|
||||
for day in &["Yesterday", "Today", "Tomorrow"] {
|
||||
if let Some(period) = p.periods.get(*day) {
|
||||
println!(
|
||||
" {:9}: {:>4.1} ({})",
|
||||
day,
|
||||
period.index,
|
||||
period.triggers.join(", ")
|
||||
);
|
||||
} else {
|
||||
println!(" {:9}: N/A", day);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
println!("\nFailed to fetch pollen (API): {e}");
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!("Upcoming calendar events:");
|
||||
|
@ -72,14 +124,8 @@ async fn main() {
|
|||
}
|
||||
}
|
||||
|
||||
// Placeholder stubs, replace with actual API calls later
|
||||
fn get_pollen_count(_config: &Config) -> String {
|
||||
// TODO: Call pollen count API
|
||||
"Medium (Tree: 5, Grass: 2, Weed: 1)".to_string()
|
||||
}
|
||||
|
||||
// Placeholder stubs
|
||||
fn get_calendar_events(_config: &Config, n: usize) -> Vec<String> {
|
||||
// TODO: Call calendar API
|
||||
vec![
|
||||
"Team meeting at 10:00 AM".to_string(),
|
||||
"Lunch with Alex at 12:30 PM".to_string(),
|
||||
|
@ -91,7 +137,6 @@ fn get_calendar_events(_config: &Config, n: usize) -> Vec<String> {
|
|||
}
|
||||
|
||||
fn get_shopify_packages(_config: &Config) -> Vec<String> {
|
||||
// TODO: Call Shopify API
|
||||
vec![
|
||||
"Order #1234: Shipped - Arriving May 13".to_string(),
|
||||
"Order #5678: In transit - Arriving May 15".to_string(),
|
||||
|
|
79
src/pollen.rs
Normal file
79
src/pollen.rs
Normal file
|
@ -0,0 +1,79 @@
|
|||
use serde::Deserialize;
|
||||
use std::collections::HashMap;
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct PollenApiResponse {
|
||||
forecast_date: String,
|
||||
location: Option<Location>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Location {
|
||||
periods: Vec<Period>,
|
||||
#[serde(rename = "DisplayLocation")]
|
||||
display_location: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct Period {
|
||||
r#type: String,
|
||||
index: f32,
|
||||
triggers: Vec<Trigger>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct Trigger {
|
||||
name: String,
|
||||
#[allow(dead_code)]
|
||||
plant_type: String,
|
||||
#[allow(dead_code)]
|
||||
genus: String,
|
||||
}
|
||||
|
||||
pub struct PollenPeriod {
|
||||
pub index: f32,
|
||||
pub triggers: Vec<String>,
|
||||
}
|
||||
|
||||
pub struct PollenSummary {
|
||||
pub location: String,
|
||||
pub forecast_date: String,
|
||||
pub periods: HashMap<String, PollenPeriod>, // "Yesterday", "Today", "Tomorrow"
|
||||
}
|
||||
|
||||
pub async fn fetch_pollen_api(zip: &str) -> Result<PollenSummary, Box<dyn Error>> {
|
||||
let url = format!("https://www.pollen.com/api/forecast/current/pollen/{}", zip);
|
||||
let resp = reqwest::Client::new()
|
||||
.get(&url)
|
||||
.header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36")
|
||||
.header("Accept", "application/json, text/plain, */*")
|
||||
.header("Referer", &url)
|
||||
.header("Cookie", format!("geo={}", zip))
|
||||
.send().await?
|
||||
.text().await?;
|
||||
|
||||
let api: PollenApiResponse = serde_json::from_str(&resp)?;
|
||||
let loc = api.location.ok_or("No location in pollen.com response")?;
|
||||
let mut periods = HashMap::new();
|
||||
|
||||
for period in &loc.periods {
|
||||
let triggers: Vec<String> = period.triggers.iter().map(|t| t.name.clone()).collect();
|
||||
periods.insert(
|
||||
period.r#type.clone(),
|
||||
PollenPeriod {
|
||||
index: period.index,
|
||||
triggers,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Ok(PollenSummary {
|
||||
location: loc.display_location,
|
||||
forecast_date: api.forecast_date,
|
||||
periods,
|
||||
})
|
||||
}
|
|
@ -1,12 +1,47 @@
|
|||
use serde::Deserialize;
|
||||
use std::error::Error;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GeocodeResponse {
|
||||
lat: f64,
|
||||
lon: f64,
|
||||
#[allow(dead_code)]
|
||||
name: String,
|
||||
#[allow(dead_code)]
|
||||
country: String,
|
||||
}
|
||||
|
||||
// returns (lat, lon) for specified city name
|
||||
pub async fn geocode_city(city: &str, api_key: &str) -> Result<(f64, f64), Box<dyn Error>> {
|
||||
let client = reqwest::Client::new();
|
||||
let geocode_url = format!(
|
||||
"https://api.openweathermap.org/geo/1.0/direct?q={}&limit=1&appid={}",
|
||||
city, api_key
|
||||
);
|
||||
let geo_resp = client.get(&geocode_url).send().await?;
|
||||
let geo_text = geo_resp.text().await?;
|
||||
let geo_parsed: Result<Vec<GeocodeResponse>, serde_json::Error> =
|
||||
serde_json::from_str(&geo_text);
|
||||
match geo_parsed {
|
||||
Ok(geo_vec) => {
|
||||
if geo_vec.is_empty() {
|
||||
return Err(format!(
|
||||
"No geocoding result for city: {} (response: {})",
|
||||
city, geo_text
|
||||
)
|
||||
.into());
|
||||
}
|
||||
Ok((geo_vec[0].lat, geo_vec[0].lon))
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to decode geocoding response. Raw response: {geo_text}");
|
||||
Err(Box::new(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
// ------------------ WEATHER -------------------------
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WeatherCurrent {
|
||||
pub temp: f64,
|
||||
|
@ -46,33 +81,11 @@ pub struct WeatherSummary {
|
|||
pub obs_time_unix: i64,
|
||||
}
|
||||
|
||||
/// Returns WeatherSummary struct
|
||||
pub async fn fetch_weather(city: &str, api_key: &str) -> Result<WeatherSummary, Box<dyn Error>> {
|
||||
let (lat, lon) = geocode_city(city, api_key).await?;
|
||||
let client = reqwest::Client::new();
|
||||
let geocode_url = format!(
|
||||
"https://api.openweathermap.org/geo/1.0/direct?q={}&limit=1&appid={}",
|
||||
city, api_key
|
||||
);
|
||||
let geo_resp = client.get(&geocode_url).send().await?;
|
||||
let geo_text = geo_resp.text().await?;
|
||||
let geo_parsed: Result<Vec<GeocodeResponse>, serde_json::Error> =
|
||||
serde_json::from_str(&geo_text);
|
||||
let (lat, lon) = match geo_parsed {
|
||||
Ok(geo_vec) => {
|
||||
if geo_vec.is_empty() {
|
||||
return Err(format!(
|
||||
"No geocoding result for city: {} (response: {})",
|
||||
city, geo_text
|
||||
)
|
||||
.into());
|
||||
}
|
||||
(geo_vec[0].lat, geo_vec[0].lon)
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to decode geocoding response. Raw response: {geo_text}");
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
};
|
||||
|
||||
// Get weather data from One Call 3.0
|
||||
let onecall_url = format!(
|
||||
"https://api.openweathermap.org/data/3.0/onecall?lat={}&lon={}&appid={}&units=imperial",
|
||||
lat, lon, api_key
|
||||
|
@ -92,6 +105,7 @@ pub async fn fetch_weather(city: &str, api_key: &str) -> Result<WeatherSummary,
|
|||
}
|
||||
};
|
||||
|
||||
// Current conditions
|
||||
let temp = format!("{:.1}°F", one_parsed.current.temp);
|
||||
let current_desc = one_parsed
|
||||
.current
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue