WIP: llm-based prototype #1
					 8 changed files with 113 additions and 17 deletions
				
			
		
							
								
								
									
										1
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								Cargo.lock
									
										
									
										generated
									
									
									
								
							| 
						 | 
					@ -142,6 +142,7 @@ dependencies = [
 | 
				
			||||||
 "iana-time-zone",
 | 
					 "iana-time-zone",
 | 
				
			||||||
 "js-sys",
 | 
					 "js-sys",
 | 
				
			||||||
 "num-traits",
 | 
					 "num-traits",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 "wasm-bindgen",
 | 
					 "wasm-bindgen",
 | 
				
			||||||
 "windows-link",
 | 
					 "windows-link",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ toml = "0.8"
 | 
				
			||||||
# Only keep one of blocking+async if all features converted
 | 
					# Only keep one of blocking+async if all features converted
 | 
				
			||||||
reqwest = { version = "0.12", features = ["json", "rustls-tls" ] }
 | 
					reqwest = { version = "0.12", features = ["json", "rustls-tls" ] }
 | 
				
			||||||
dirs = "5"
 | 
					dirs = "5"
 | 
				
			||||||
chrono = "0.4"
 | 
					chrono = { version = "0.4", features = ["serde"] }
 | 
				
			||||||
serde_json = "1.0"
 | 
					serde_json = "1.0"
 | 
				
			||||||
chrono-tz = "0.8"
 | 
					chrono-tz = "0.8"
 | 
				
			||||||
tokio = { version = "1.37", features = ["full"] }
 | 
					tokio = { version = "1.37", features = ["full"] }
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										52
									
								
								src/cache.rs
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										52
									
								
								src/cache.rs
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,52 @@
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
 | 
					use std::{
 | 
				
			||||||
 | 
					    fs,
 | 
				
			||||||
 | 
					    path::PathBuf,
 | 
				
			||||||
 | 
					    time::{SystemTime, UNIX_EPOCH},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
 | 
					pub struct Cache<T> {
 | 
				
			||||||
 | 
					    pub timestamp: u64,
 | 
				
			||||||
 | 
					    pub data: T,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fn get_cache_path(report_type: &str) -> PathBuf {
 | 
				
			||||||
 | 
					    let home = std::env::var("HOME").expect("HOME not set");
 | 
				
			||||||
 | 
					    let cache_dir = PathBuf::from(format!("{}/.local/state/trmnl", home));
 | 
				
			||||||
 | 
					    let _ = fs::create_dir_all(&cache_dir);
 | 
				
			||||||
 | 
					    cache_dir.join(format!("{}.json", report_type))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use serde::de::DeserializeOwned;
 | 
				
			||||||
 | 
					pub fn load_cache<T: Serialize + DeserializeOwned>(
 | 
				
			||||||
 | 
					    report_type: &str,
 | 
				
			||||||
 | 
					    expiry_secs: u64,
 | 
				
			||||||
 | 
					) -> Option<T> {
 | 
				
			||||||
 | 
					    let path = get_cache_path(report_type);
 | 
				
			||||||
 | 
					    let contents = fs::read_to_string(&path).ok()?;
 | 
				
			||||||
 | 
					    let parsed: Cache<T> = serde_json::from_str(&contents).ok()?;
 | 
				
			||||||
 | 
					    let now = SystemTime::now()
 | 
				
			||||||
 | 
					        .duration_since(UNIX_EPOCH)
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					        .as_secs();
 | 
				
			||||||
 | 
					    if now - parsed.timestamp <= expiry_secs {
 | 
				
			||||||
 | 
					        println!("(using cached {} report)", report_type);
 | 
				
			||||||
 | 
					        Some(parsed.data)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        None
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn save_cache<T: Serialize>(report_type: &str, data: &T) {
 | 
				
			||||||
 | 
					    let path = get_cache_path(report_type);
 | 
				
			||||||
 | 
					    let to_save = Cache {
 | 
				
			||||||
 | 
					        timestamp: SystemTime::now()
 | 
				
			||||||
 | 
					            .duration_since(UNIX_EPOCH)
 | 
				
			||||||
 | 
					            .unwrap()
 | 
				
			||||||
 | 
					            .as_secs(),
 | 
				
			||||||
 | 
					        data: data,
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    let json = serde_json::to_string_pretty(&to_save).unwrap();
 | 
				
			||||||
 | 
					    let _ = fs::write(path, json);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -1,8 +1,10 @@
 | 
				
			||||||
use chrono::{DateTime, FixedOffset, NaiveDate, Offset, TimeZone, Utc};
 | 
					use chrono::{DateTime, FixedOffset, NaiveDate, Offset, TimeZone, Utc};
 | 
				
			||||||
use ical::IcalParser;
 | 
					use ical::IcalParser;
 | 
				
			||||||
use rrule::{RRule, RRuleSet, Unvalidated};
 | 
					use rrule::{RRule, RRuleSet, Unvalidated};
 | 
				
			||||||
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use std::error::Error;
 | 
					use std::error::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
pub struct CalendarEventSummary {
 | 
					pub struct CalendarEventSummary {
 | 
				
			||||||
    pub start: Option<DateTime<FixedOffset>>,
 | 
					    pub start: Option<DateTime<FixedOffset>>,
 | 
				
			||||||
    pub summary: String,
 | 
					    pub summary: String,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,6 +5,9 @@ use std::path::PathBuf;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[derive(Debug, Deserialize)]
 | 
					#[derive(Debug, Deserialize)]
 | 
				
			||||||
pub struct Config {
 | 
					pub struct Config {
 | 
				
			||||||
 | 
					    pub cache_weather_secs: Option<u64>,
 | 
				
			||||||
 | 
					    pub cache_pollen_secs: Option<u64>,
 | 
				
			||||||
 | 
					    pub cache_calendar_secs: Option<u64>,
 | 
				
			||||||
    pub weather_api_key: String,
 | 
					    pub weather_api_key: String,
 | 
				
			||||||
    pub pollen_api_key: String,
 | 
					    pub pollen_api_key: String,
 | 
				
			||||||
    pub calendar_api_key: String,
 | 
					    pub calendar_api_key: String,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
							
								
								
									
										63
									
								
								src/main.rs
									
										
									
									
									
								
							
							
						
						
									
										63
									
								
								src/main.rs
									
										
									
									
									
								
							| 
						 | 
					@ -1,3 +1,4 @@
 | 
				
			||||||
 | 
					mod cache;
 | 
				
			||||||
mod calendar;
 | 
					mod calendar;
 | 
				
			||||||
mod config;
 | 
					mod config;
 | 
				
			||||||
mod pollen;
 | 
					mod pollen;
 | 
				
			||||||
| 
						 | 
					@ -20,12 +21,21 @@ async fn main() {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Weather - with cache
 | 
				
			||||||
 | 
					    let cache_weather_secs = config.cache_weather_secs.unwrap_or(300);
 | 
				
			||||||
    let weather: Option<WeatherSummary> =
 | 
					    let weather: Option<WeatherSummary> =
 | 
				
			||||||
        match fetch_weather(&config.location, &config.weather_api_key).await {
 | 
					        if let Some(w) = cache::load_cache::<WeatherSummary>("weather", cache_weather_secs) {
 | 
				
			||||||
            Ok(data) => Some(data),
 | 
					            Some(w)
 | 
				
			||||||
            Err(e) => {
 | 
					        } else {
 | 
				
			||||||
                eprintln!("Failed to fetch weather: {e}");
 | 
					            match fetch_weather(&config.location, &config.weather_api_key).await {
 | 
				
			||||||
                None
 | 
					                Ok(data) => {
 | 
				
			||||||
 | 
					                    cache::save_cache("weather", &data);
 | 
				
			||||||
 | 
					                    Some(data)
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                Err(e) => {
 | 
				
			||||||
 | 
					                    eprintln!("Failed to fetch weather: {e}");
 | 
				
			||||||
 | 
					                    None
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
    let (_lat, _lon) = if weather.is_some() {
 | 
					    let (_lat, _lon) = if weather.is_some() {
 | 
				
			||||||
| 
						 | 
					@ -66,7 +76,19 @@ async fn main() {
 | 
				
			||||||
        println!("Weather: N/A");
 | 
					        println!("Weather: N/A");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match fetch_pollen_api(&config.pollen_zip).await {
 | 
					    // Pollen - with cache
 | 
				
			||||||
 | 
					    let cache_pollen_secs = config.cache_pollen_secs.unwrap_or(300);
 | 
				
			||||||
 | 
					    match if let Some(p) = cache::load_cache::<pollen::PollenSummary>("pollen", cache_pollen_secs) {
 | 
				
			||||||
 | 
					        Ok(p)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        match fetch_pollen_api(&config.pollen_zip).await {
 | 
				
			||||||
 | 
					            Ok(data) => {
 | 
				
			||||||
 | 
					                cache::save_cache("pollen", &data);
 | 
				
			||||||
 | 
					                Ok(data)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Err(e) => Err(e),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } {
 | 
				
			||||||
        Ok(p) => {
 | 
					        Ok(p) => {
 | 
				
			||||||
            if let Ok(dt) = DateTime::parse_from_rfc3339(&p.forecast_date) {
 | 
					            if let Ok(dt) = DateTime::parse_from_rfc3339(&p.forecast_date) {
 | 
				
			||||||
                match config.timezone.parse::<Tz>() {
 | 
					                match config.timezone.parse::<Tz>() {
 | 
				
			||||||
| 
						 | 
					@ -110,16 +132,29 @@ async fn main() {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    println!();
 | 
					    // Calendar - with cache
 | 
				
			||||||
    println!("Upcoming calendar events:");
 | 
					    let cache_calendar_secs = config.cache_calendar_secs.unwrap_or(300);
 | 
				
			||||||
 | 
					    println!("\nUpcoming calendar events:");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    match fetch_next_events(
 | 
					    match if let Some(events) =
 | 
				
			||||||
        &config.calendar_url,
 | 
					        cache::load_cache::<Vec<calendar::CalendarEventSummary>>("calendar", cache_calendar_secs)
 | 
				
			||||||
        config.calendar_event_count,
 | 
					 | 
				
			||||||
        &config.timezone,
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
    .await
 | 
					 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(events)
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        match fetch_next_events(
 | 
				
			||||||
 | 
					            &config.calendar_url,
 | 
				
			||||||
 | 
					            config.calendar_event_count,
 | 
				
			||||||
 | 
					            &config.timezone,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Ok(data) => {
 | 
				
			||||||
 | 
					                cache::save_cache("calendar", &data);
 | 
				
			||||||
 | 
					                Ok(data)
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            Err(e) => Err(e),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } {
 | 
				
			||||||
        Ok(events) if !events.is_empty() => {
 | 
					        Ok(events) if !events.is_empty() => {
 | 
				
			||||||
            for (i, event) in events.iter().enumerate() {
 | 
					            for (i, event) in events.iter().enumerate() {
 | 
				
			||||||
                let day = event
 | 
					                let day = event
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
use serde::Deserialize;
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use std::collections::HashMap;
 | 
					use std::collections::HashMap;
 | 
				
			||||||
use std::error::Error;
 | 
					use std::error::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,11 +34,13 @@ struct Trigger {
 | 
				
			||||||
    genus: String,
 | 
					    genus: String,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
pub struct PollenPeriod {
 | 
					pub struct PollenPeriod {
 | 
				
			||||||
    pub index: f32,
 | 
					    pub index: f32,
 | 
				
			||||||
    pub triggers: Vec<String>,
 | 
					    pub triggers: Vec<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
pub struct PollenSummary {
 | 
					pub struct PollenSummary {
 | 
				
			||||||
    pub location: String,
 | 
					    pub location: String,
 | 
				
			||||||
    pub forecast_date: String,
 | 
					    pub forecast_date: String,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
use serde::Deserialize;
 | 
					use serde::{Deserialize, Serialize};
 | 
				
			||||||
use std::error::Error;
 | 
					use std::error::Error;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#[allow(dead_code)]
 | 
					#[allow(dead_code)]
 | 
				
			||||||
| 
						 | 
					@ -72,6 +72,7 @@ pub struct OneCallResult {
 | 
				
			||||||
    pub daily: Vec<WeatherDaily>,
 | 
					    pub daily: Vec<WeatherDaily>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Serialize, Deserialize)]
 | 
				
			||||||
pub struct WeatherSummary {
 | 
					pub struct WeatherSummary {
 | 
				
			||||||
    pub temp: String,
 | 
					    pub temp: String,
 | 
				
			||||||
    pub current_desc: String,
 | 
					    pub current_desc: String,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue