roughing in weather
This commit is contained in:
parent
d050cecc0e
commit
45ff2ee635
5 changed files with 571 additions and 40 deletions
|
@ -12,6 +12,7 @@ pub struct Config {
|
|||
pub shopify_store: String,
|
||||
pub calendar_event_count: usize,
|
||||
pub location: String,
|
||||
pub timezone: String,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
|
|
72
src/main.rs
72
src/main.rs
|
@ -1,9 +1,13 @@
|
|||
mod config;
|
||||
mod weather;
|
||||
|
||||
use chrono::{TimeZone, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use config::Config;
|
||||
use weather::{fetch_weather, WeatherSummary};
|
||||
|
||||
fn main() {
|
||||
// Load config
|
||||
#[tokio::main]
|
||||
async fn main() {
|
||||
let config = match Config::load() {
|
||||
Ok(cfg) => cfg,
|
||||
Err(e) => {
|
||||
|
@ -12,27 +16,56 @@ fn main() {
|
|||
}
|
||||
};
|
||||
|
||||
// Weather
|
||||
let (temp, conditions, high, low, forecast) = get_weather(&config);
|
||||
println!("Weather for today:");
|
||||
println!(" - Temperature: {temp}");
|
||||
println!(" - Conditions: {conditions}");
|
||||
println!(" - High: {high}, Low: {low}");
|
||||
println!(" - Forecast: {forecast}");
|
||||
let weather: Option<WeatherSummary> =
|
||||
match fetch_weather(&config.location, &config.weather_api_key).await {
|
||||
Ok(data) => Some(data),
|
||||
Err(e) => {
|
||||
eprintln!("Failed to fetch weather: {e}");
|
||||
None
|
||||
}
|
||||
};
|
||||
if let Some(w) = weather {
|
||||
if w.obs_time_unix > 0 {
|
||||
match config.timezone.parse::<Tz>() {
|
||||
Ok(tz) => {
|
||||
let dt_local = tz.from_utc_datetime(
|
||||
&Utc.timestamp_opt(w.obs_time_unix, 0).unwrap().naive_utc(),
|
||||
);
|
||||
println!(
|
||||
"Weather (as of {}):",
|
||||
dt_local.format("%Y-%m-%d %H:%M:%S %Z")
|
||||
);
|
||||
}
|
||||
Err(_) => {
|
||||
println!(
|
||||
"Weather (as of unix timestamp {}) (timezone parse error)",
|
||||
w.obs_time_unix
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
println!("Weather:");
|
||||
}
|
||||
println!(" - Temperature: {}", w.temp);
|
||||
println!(" - Conditions: {}", w.current_desc);
|
||||
println!(" - High: {}, Low: {}", w.high, w.low);
|
||||
println!(" - Forecast: {}", w.daily_desc);
|
||||
} else {
|
||||
println!("Weather: N/A");
|
||||
}
|
||||
|
||||
// Pollen count
|
||||
let pollen = get_pollen_count(&config);
|
||||
println!("\nPollen count: {pollen}");
|
||||
|
||||
// Calendar events
|
||||
println!("\nUpcoming calendar events:");
|
||||
println!();
|
||||
println!("Upcoming calendar events:");
|
||||
let events = get_calendar_events(&config, config.calendar_event_count);
|
||||
for (i, event) in events.iter().enumerate() {
|
||||
println!("{}. {}", i + 1, event);
|
||||
}
|
||||
|
||||
// Shopify inbound packages
|
||||
println!("\nInbound Shopify packages:");
|
||||
println!();
|
||||
println!("Inbound Shopify packages:");
|
||||
let packages = get_shopify_packages(&config);
|
||||
for (i, pkg) in packages.iter().enumerate() {
|
||||
println!("{}. {}", i + 1, pkg);
|
||||
|
@ -40,17 +73,6 @@ fn main() {
|
|||
}
|
||||
|
||||
// Placeholder stubs, replace with actual API calls later
|
||||
fn get_weather(_config: &Config) -> (String, String, String, String, String) {
|
||||
// TODO: Call weather API
|
||||
(
|
||||
"72°F".to_string(),
|
||||
"Partly cloudy".to_string(),
|
||||
"78°F".to_string(),
|
||||
"65°F".to_string(),
|
||||
"Mostly sunny, light breeze".to_string(),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_pollen_count(_config: &Config) -> String {
|
||||
// TODO: Call pollen count API
|
||||
"Medium (Tree: 5, Grass: 2, Weed: 1)".to_string()
|
||||
|
|
126
src/weather.rs
Normal file
126
src/weather.rs
Normal file
|
@ -0,0 +1,126 @@
|
|||
use serde::Deserialize;
|
||||
use std::error::Error;
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GeocodeResponse {
|
||||
lat: f64,
|
||||
lon: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WeatherCurrent {
|
||||
pub temp: f64,
|
||||
pub weather: Vec<WeatherDesc>,
|
||||
pub dt: i64, // Unix timestamp
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WeatherDailyTemp {
|
||||
pub min: f64,
|
||||
pub max: f64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WeatherDesc {
|
||||
pub description: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct WeatherDaily {
|
||||
pub temp: WeatherDailyTemp,
|
||||
pub weather: Vec<WeatherDesc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct OneCallResult {
|
||||
pub current: WeatherCurrent,
|
||||
pub daily: Vec<WeatherDaily>,
|
||||
}
|
||||
|
||||
pub struct WeatherSummary {
|
||||
pub temp: String,
|
||||
pub current_desc: String,
|
||||
pub high: String,
|
||||
pub low: String,
|
||||
pub daily_desc: String,
|
||||
pub obs_time_unix: i64,
|
||||
}
|
||||
|
||||
pub async fn fetch_weather(city: &str, api_key: &str) -> Result<WeatherSummary, 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);
|
||||
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));
|
||||
}
|
||||
};
|
||||
|
||||
let onecall_url = format!(
|
||||
"https://api.openweathermap.org/data/3.0/onecall?lat={}&lon={}&appid={}&units=imperial",
|
||||
lat, lon, api_key
|
||||
);
|
||||
let one_resp = client.get(&onecall_url).send().await?;
|
||||
let one_status = one_resp.status();
|
||||
let one_text = one_resp.text().await?;
|
||||
if !one_status.is_success() {
|
||||
return Err(format!("HTTP error {}: {}", one_status, one_text).into());
|
||||
}
|
||||
|
||||
let one_parsed: OneCallResult = match serde_json::from_str(&one_text) {
|
||||
Ok(data) => data,
|
||||
Err(e) => {
|
||||
eprintln!("Failed to decode One Call weather response. Raw response: {one_text}");
|
||||
return Err(Box::new(e));
|
||||
}
|
||||
};
|
||||
|
||||
let temp = format!("{:.1}°F", one_parsed.current.temp);
|
||||
let current_desc = one_parsed
|
||||
.current
|
||||
.weather
|
||||
.first()
|
||||
.map(|w| w.description.clone())
|
||||
.unwrap_or_else(|| "N/A".to_string());
|
||||
let current_dt = one_parsed.current.dt; // UNIX timestamp, UTC
|
||||
|
||||
// Today's forecast is daily[0]
|
||||
let (high, low, daily_desc) = if let Some(today) = one_parsed.daily.first() {
|
||||
let high = format!("{:.1}°F", today.temp.max);
|
||||
let low = format!("{:.1}°F", today.temp.min);
|
||||
let desc = today
|
||||
.weather
|
||||
.first()
|
||||
.map(|w| w.description.clone())
|
||||
.unwrap_or_else(|| "N/A".to_string());
|
||||
(high, low, desc)
|
||||
} else {
|
||||
("N/A".to_string(), "N/A".to_string(), "N/A".to_string())
|
||||
};
|
||||
|
||||
Ok(WeatherSummary {
|
||||
temp,
|
||||
current_desc,
|
||||
high,
|
||||
low,
|
||||
daily_desc,
|
||||
obs_time_unix: current_dt,
|
||||
})
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue