i3status_rs/blocks/
temperature.rs1use super::prelude::*;
60use crate::util::celsius_to_fahrenheit;
61use sensors::FeatureType::SENSORS_FEATURE_TEMP;
62use sensors::Sensors;
63use sensors::SubfeatureType::SENSORS_SUBFEATURE_TEMP_INPUT;
64
65const DEFAULT_GOOD: f64 = 20.0;
66const DEFAULT_IDLE: f64 = 45.0;
67const DEFAULT_INFO: f64 = 60.0;
68const DEFAULT_WARN: f64 = 80.0;
69
70#[derive(Deserialize, Debug, SmartDefault)]
71#[serde(default)]
72pub struct Config {
73 #[serde(flatten)]
74 pub formats: MaybeMultiFormatConfig,
75 #[default(5.into())]
76 pub interval: Seconds,
77 pub scale: TemperatureScale,
78 pub good: Option<f64>,
79 pub idle: Option<f64>,
80 pub info: Option<f64>,
81 pub warning: Option<f64>,
82 pub chip: Option<String>,
83 pub inputs: Option<Vec<String>>,
84}
85
86#[derive(Deserialize, Debug, SmartDefault, Clone, Copy, PartialEq, Eq)]
87#[serde(rename_all = "lowercase")]
88pub enum TemperatureScale {
89 #[default]
90 Celsius,
91 Fahrenheit,
92}
93
94impl TemperatureScale {
95 #[allow(clippy::wrong_self_convention)]
96 pub fn from_celsius(self, val: f64) -> f64 {
97 match self {
98 Self::Celsius => val,
99 Self::Fahrenheit => celsius_to_fahrenheit(val),
100 }
101 }
102
103 pub fn as_value(self, val: f64) -> Value {
104 match self {
105 Self::Celsius => Value::degrees_c(val),
106 Self::Fahrenheit => Value::degrees_f(val),
107 }
108 }
109}
110
111pub(crate) fn prepare(config: &Config) -> Result<Arc<BlockPlan>> {
112 let declare = |output: OutputPlan| output.icon("icon", IconChoices::one(icons::THERMOMETER));
113 let formats = config
114 .formats
115 .with_default(" $icon $average avg, $max max ")?;
116 BlockPlan::new(format_outputs(formats, declare))
117}
118
119pub(crate) async fn run(config: &Config, api: &CommonApi, plan: &Arc<BlockPlan>) -> Result<()> {
120 let mut actions = api.get_actions()?;
121 api.set_default_actions(&[
122 (MouseButton::Left, None, "next_format"),
123 (MouseButton::Right, None, "prev_format"),
124 ])?;
125
126 let mut formats = FormatRotation::new(plan)?;
127
128 let good = config
129 .good
130 .unwrap_or_else(|| config.scale.from_celsius(DEFAULT_GOOD));
131 let idle = config
132 .idle
133 .unwrap_or_else(|| config.scale.from_celsius(DEFAULT_IDLE));
134 let info = config
135 .info
136 .unwrap_or_else(|| config.scale.from_celsius(DEFAULT_INFO));
137 let warn = config
138 .warning
139 .unwrap_or_else(|| config.scale.from_celsius(DEFAULT_WARN));
140
141 loop {
142 let chip = config.chip.clone();
143 let inputs = config.inputs.clone();
144 let config_scale = config.scale;
145 let temp = tokio::task::spawn_blocking(move || {
146 let mut vals = Vec::new();
147 let sensors = Sensors::new();
148 let chips = match &chip {
149 Some(chip) => sensors
150 .detected_chips(chip)
151 .error("Failed to create chip iterator")?,
152 None => sensors.into_iter(),
153 };
154 for chip in chips {
155 for feat in chip {
156 if *feat.feature_type() != SENSORS_FEATURE_TEMP {
157 continue;
158 }
159 if let Some(inputs) = &inputs {
160 let label = feat.get_label().error("Failed to get input label")?;
161 if !inputs.contains(&label) {
162 continue;
163 }
164 }
165 for subfeat in feat {
166 if *subfeat.subfeature_type() == SENSORS_SUBFEATURE_TEMP_INPUT
167 && let Ok(value) = subfeat.get_value()
168 {
169 if (-100.0..=150.0).contains(&value) {
170 vals.push(config_scale.from_celsius(value));
171 } else {
172 eprintln!("Temperature ({value}) outside of range ([-100, 150])");
173 }
174 }
175 }
176 }
177 }
178 Ok(vals)
179 })
180 .await
181 .error("Failed to join tokio task")??;
182
183 let min_temp = temp
184 .iter()
185 .min_by(|a, b| a.partial_cmp(b).unwrap())
186 .cloned()
187 .unwrap_or(0.0);
188 let max_temp = temp
189 .iter()
190 .max_by(|a, b| a.partial_cmp(b).unwrap())
191 .cloned()
192 .unwrap_or(0.0);
193 let avg_temp = temp.iter().sum::<f64>() / temp.len() as f64;
194
195 let output = formats.current();
196 let mut widget = output.new_widget();
197
198 widget.state = match max_temp {
199 x if x <= good => State::Good,
200 x if x <= idle => State::Idle,
201 x if x <= info => State::Info,
202 x if x <= warn => State::Warning,
203 _ => State::Critical,
204 };
205
206 widget.set_values(map! {
207 "icon" => Value::icon_progression_bound(icons::THERMOMETER, max_temp, good, warn),
208 "average" => config_scale.as_value(avg_temp),
209 "min" => config_scale.as_value(min_temp),
210 "max" => config_scale.as_value(max_temp),
211 });
212
213 api.set_widget(widget)?;
214
215 select! {
216 _ = sleep(config.interval.0) => (),
217 _ = api.wait_for_update_request() => (),
218 Some(action) = actions.recv() => match action.as_ref() {
219 "next_format" | "toggle_format" => {
220 formats.next();
221 }
222 "prev_format" => {
223 formats.prev();
224 }
225 _ => (),
226 }
227 }
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234
235 fn config(toml_str: &str) -> Config {
236 toml::from_str(toml_str).unwrap()
237 }
238
239 #[test]
240 fn plan_declares_thermometer_icon() {
241 let plan = prepare(&Config::default()).unwrap();
242 let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
243 assert_eq!(ids, ["format"]);
244 let main = plan.output("format").unwrap();
245 assert_eq!(main.single_icon("icon").unwrap(), "thermometer");
246 assert!(main.format().contains_key("average"));
247 }
248
249 #[test]
250 fn every_configured_format_gets_an_output() {
251 let plan = prepare(&Config::default()).unwrap();
252 assert!(plan.output("format2").is_err());
253
254 let plan = prepare(&config(
255 r#"format = [" $icon $max max ", " $icon $min min "]"#,
256 ))
257 .unwrap();
258 let ids: Vec<_> = plan.outputs().map(|o| o.id()).collect();
259 assert_eq!(ids, ["format", "format2"]);
260 let alt = plan.output("format2").unwrap();
261 assert!(alt.format().contains_key("min"));
262 assert_eq!(alt.single_icon("icon").unwrap(), "thermometer");
263 }
264
265 #[test]
266 fn format_alt_still_produces_a_second_output() {
267 let plan = prepare(&config(r#"format_alt = " $icon $min min ""#)).unwrap();
268 let alt = plan.output("format2").unwrap();
269 assert!(alt.format().contains_key("min"));
270 assert_eq!(alt.single_icon("icon").unwrap(), "thermometer");
271 }
272}