diff --git a/Cargo.toml b/Cargo.toml index f4f278b..d98edc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,3 +14,4 @@ chrono = "0.4.41" chrono-tz = "0.10.3" iana-time-zone = "0.1.63" clap = { version = "4.5.1", features = ["derive"] } +thiserror = "1.0" diff --git a/src/bar_modules/bar_module_weather.rs b/src/bar_modules/bar_module_weather.rs index 62f9f57..94b3e1a 100644 --- a/src/bar_modules/bar_module_weather.rs +++ b/src/bar_modules/bar_module_weather.rs @@ -3,11 +3,19 @@ use curl::easy::{Easy, List}; use serde_json::json; use serde_json::Value; use regex::Regex; +use thiserror::Error; +use std::num::ParseFloatError; use crate::common; use crate::common::UpdateResult; use crate::bar_modules; +/// Number of cycles between weather updates +const UPDATE_INTERVAL: u32 = 300; + +/// Base URL for the National Weather Service API +const WEATHER_API_BASE: &str = "https://api.weather.gov"; + #[derive(Clone)] pub struct UnibarModuleWeather { opts: common::AppOptions, @@ -15,7 +23,27 @@ pub struct UnibarModuleWeather { update_cnt: u32, } +#[derive(Debug, Error)] +pub enum WeatherError { + #[error("Invalid temperature format: {0}")] + ParseError(String), + #[error("API response missing temperature field: {0}")] + ApiResponseStrErr(String), +} + +impl From for WeatherError { + fn from(err: ParseFloatError) -> Self { + WeatherError::ParseError(err.to_string()) + } +} + impl UnibarModuleWeather { + const FAHRENHEIT_MULTIPLIER: f64 = 9.0 / 5.0; + const FAHRENHEIT_OFFSET: f64 = 32.0; + + fn celsius_to_fahrenheit(celsius: f64) -> f64 { + celsius * Self::FAHRENHEIT_MULTIPLIER + Self::FAHRENHEIT_OFFSET + } // -------------------- pub fn new(o :common::AppOptions) -> Self { @@ -27,23 +55,27 @@ impl UnibarModuleWeather { } // -------------------- - fn get_current_temperature(&self) -> f32 { - let deg_c :f32; - - match self.weather_info { - serde_json::Value::Null => { - deg_c = 0.0; - } + fn get_current_temperature(&self) -> Result { + let deg_c = match &self.weather_info { + serde_json::Value::Null => 0.0, _ => { - deg_c = self.weather_info["features"][0]["properties"]["temperature"]["value"] - .to_string().parse().unwrap(); + // Safely navigate the JSON structure + self.weather_info + .get("features") + .and_then(|f| f.get(0)) + .and_then(|f| f.get("properties")) + .and_then(|p| p.get("temperature")) + .and_then(|t| t.get("value")) + .and_then(|v| v.as_f64()) + .ok_or(WeatherError::ApiResponseStrErr("as_str likely failed".to_string()))? + // .parse()? } - } + }; - match self.opts.weather_units { - common::TemperatureUnits::Metric => return deg_c, - common::TemperatureUnits::Imperial => return (deg_c * 9.0 / 5.0) + 32.0, - } + Ok(match self.opts.weather_units { + common::TemperatureUnits::Metric => deg_c, + common::TemperatureUnits::Imperial => Self::celsius_to_fahrenheit(deg_c), + }) } // -------------------- @@ -100,7 +132,8 @@ impl UnibarModuleWeather { let mut url_string = Vec::new(); let mut curl_ret = Vec::new(); - url_string.push("https://api.weather.gov/icons".to_owned()); + url_string.push(WEATHER_API_BASE.to_owned()); + url_string.push("/icons".to_owned()); curl.url(url_string.concat().as_str()).unwrap(); { @@ -171,7 +204,7 @@ impl bar_modules::BarModuleActions for UnibarModuleWeather { // Weather update every 60 cycles fn should_update(&mut self) -> UpdateResult { if self.update_cnt == 0 { - self.update_cnt = 300; + self.update_cnt = UPDATE_INTERVAL; return UpdateResult::Update; } else { self.update_cnt -= 1; @@ -192,7 +225,8 @@ impl bar_modules::BarModuleActions for UnibarModuleWeather { let mut curl_ret = Vec::new(); let mut curl_err = false; - url_string.push("https://api.weather.gov/stations/".to_owned()); + url_string.push(WEATHER_API_BASE.to_owned()); + url_string.push("/stations/".to_owned()); url_string.push(self.opts.weather_station.to_owned()); url_string.push("/observations?limit=1".to_owned()); @@ -233,8 +267,8 @@ impl bar_modules::BarModuleActions for UnibarModuleWeather { // -------------------- fn get_content(&self) -> String { - let temperature_value :f32 = self.get_current_temperature(); - let temperature_unit :String = self.get_temperature_unit(); + let temperature_value :f64 = self.get_current_temperature().expect("Temperature query"); + let temperature_unit :String = self.get_temperature_unit(); // let temperature_icon :String = self.get_icon(v.clone()); return format!("{:.0}{}", temperature_value, temperature_unit); @@ -251,6 +285,9 @@ impl bar_modules::BarModuleActions for UnibarModuleWeather { // "icon": "https://api.weather.gov/icons/land/night/ovc?size=medium", let re = Regex::new(r"(\w+)\?size").unwrap(); let json_val = self.weather_info["features"][0]["properties"]["icon"].to_string(); + if self.opts.debug { + println!("-----> weather_data - {:#?}", self.weather_info["features"][0]["properties"]); + } match self.weather_info["features"][0]["properties"]["icon"] { serde_json::Value::Null => { return "⁈".to_string(); diff --git a/src/bar_modules/mod.rs b/src/bar_modules/mod.rs index f9ab7c6..a3d7fd8 100644 --- a/src/bar_modules/mod.rs +++ b/src/bar_modules/mod.rs @@ -3,34 +3,34 @@ use crate::common::UpdateResult; // -------------------- /// All Bar modules must implement the actions pub trait BarModuleActions { - /// Return String with name of the module + /// Returns the name of the module fn get_name(&self) -> String; - /// Do necessary clean up before starting new fetch + /// Cleans up any state before refreshing data fn clear(&mut self); - /// Do necessary processing to generate data for this bar module + /// Fetches and processes new data for the module fn generate_data(&mut self); - /// Return String content to be displayed in the bar + /// Returns the formatted content to display in the bar fn get_content(&self) -> String; - /// Return a Unicode icon to display before content in the bar. - /// This icon may differ based on content of the data + /// Returns a Unicode icon representing the module's current state fn get_icon(&self) -> String; - /// Returns UpdateResult depending on whether the module - /// should be updated during the current update cycle + /// Determines if the module should update its data + /// Default implementation always returns Update fn should_update(&mut self) -> UpdateResult { return UpdateResult::Update; } } pub trait BarModuleDebug { - /// Print debug information at the end + /// Prints debug information about the module fn post_debug(&self); } +// Module declarations pub mod bar_module_weather; pub mod bar_module_music; pub mod bar_module_network;