i3status_rs/blocks/
temperature.rs1use super::prelude::*;
59use crate::util::celsius_to_fahrenheit;
60use sensors::FeatureType::SENSORS_FEATURE_TEMP;
61use sensors::Sensors;
62use sensors::SubfeatureType::SENSORS_SUBFEATURE_TEMP_INPUT;
63
64const DEFAULT_GOOD: f64 = 20.0;
65const DEFAULT_IDLE: f64 = 45.0;
66const DEFAULT_INFO: f64 = 60.0;
67const DEFAULT_WARN: f64 = 80.0;
68
69#[derive(Deserialize, Debug, SmartDefault)]
70#[serde(deny_unknown_fields, default)]
71pub struct Config {
72 pub format: FormatConfig,
73 pub format_alt: Option<FormatConfig>,
74 #[default(5.into())]
75 pub interval: Seconds,
76 pub scale: TemperatureScale,
77 pub good: Option<f64>,
78 pub idle: Option<f64>,
79 pub info: Option<f64>,
80 pub warning: Option<f64>,
81 pub chip: Option<String>,
82 pub inputs: Option<Vec<String>>,
83}
84
85#[derive(Deserialize, Debug, SmartDefault, Clone, Copy, PartialEq, Eq)]
86#[serde(rename_all = "lowercase")]
87pub enum TemperatureScale {
88 #[default]
89 Celsius,
90 Fahrenheit,
91}
92
93impl TemperatureScale {
94 #[allow(clippy::wrong_self_convention)]
95 pub fn from_celsius(self, val: f64) -> f64 {
96 match self {
97 Self::Celsius => val,
98 Self::Fahrenheit => celsius_to_fahrenheit(val),
99 }
100 }
101
102 pub fn as_value(self, val: f64) -> Value {
103 match self {
104 Self::Celsius => Value::degrees_c(val),
105 Self::Fahrenheit => Value::degrees_f(val),
106 }
107 }
108}
109
110pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
111 let mut actions = api.get_actions()?;
112 api.set_default_actions(&[(MouseButton::Left, None, "toggle_format")])?;
113
114 let mut format = config
115 .format
116 .with_default(" $icon $average avg, $max max ")?;
117 let mut format_alt = match &config.format_alt {
118 Some(f) => Some(f.with_default("")?),
119 None => None,
120 };
121
122 let good = config
123 .good
124 .unwrap_or_else(|| config.scale.from_celsius(DEFAULT_GOOD));
125 let idle = config
126 .idle
127 .unwrap_or_else(|| config.scale.from_celsius(DEFAULT_IDLE));
128 let info = config
129 .info
130 .unwrap_or_else(|| config.scale.from_celsius(DEFAULT_INFO));
131 let warn = config
132 .warning
133 .unwrap_or_else(|| config.scale.from_celsius(DEFAULT_WARN));
134
135 loop {
136 let chip = config.chip.clone();
137 let inputs = config.inputs.clone();
138 let config_scale = config.scale;
139 let temp = tokio::task::spawn_blocking(move || {
140 let mut vals = Vec::new();
141 let sensors = Sensors::new();
142 let chips = match &chip {
143 Some(chip) => sensors
144 .detected_chips(chip)
145 .error("Failed to create chip iterator")?,
146 None => sensors.into_iter(),
147 };
148 for chip in chips {
149 for feat in chip {
150 if *feat.feature_type() != SENSORS_FEATURE_TEMP {
151 continue;
152 }
153 if let Some(inputs) = &inputs {
154 let label = feat.get_label().error("Failed to get input label")?;
155 if !inputs.contains(&label) {
156 continue;
157 }
158 }
159 for subfeat in feat {
160 if *subfeat.subfeature_type() == SENSORS_SUBFEATURE_TEMP_INPUT
161 && let Ok(value) = subfeat.get_value()
162 {
163 if (-100.0..=150.0).contains(&value) {
164 vals.push(config_scale.from_celsius(value));
165 } else {
166 eprintln!("Temperature ({value}) outside of range ([-100, 150])");
167 }
168 }
169 }
170 }
171 }
172 Ok(vals)
173 })
174 .await
175 .error("Failed to join tokio task")??;
176
177 let min_temp = temp
178 .iter()
179 .min_by(|a, b| a.partial_cmp(b).unwrap())
180 .cloned()
181 .unwrap_or(0.0);
182 let max_temp = temp
183 .iter()
184 .max_by(|a, b| a.partial_cmp(b).unwrap())
185 .cloned()
186 .unwrap_or(0.0);
187 let avg_temp = temp.iter().sum::<f64>() / temp.len() as f64;
188
189 let mut widget = Widget::new().with_format(format.clone());
190
191 widget.state = match max_temp {
192 x if x <= good => State::Good,
193 x if x <= idle => State::Idle,
194 x if x <= info => State::Info,
195 x if x <= warn => State::Warning,
196 _ => State::Critical,
197 };
198
199 widget.set_values(map! {
200 "icon" => Value::icon_progression_bound("thermometer", max_temp, good, warn),
201 "average" => config_scale.as_value(avg_temp),
202 "min" => config_scale.as_value(min_temp),
203 "max" => config_scale.as_value(max_temp),
204 });
205
206 api.set_widget(widget)?;
207
208 select! {
209 _ = sleep(config.interval.0) => (),
210 _ = api.wait_for_update_request() => (),
211 Some(action) = actions.recv() => match action.as_ref() {
212 "toggle_format" => {
213 if let Some(format_alt) = &mut format_alt {
214 std::mem::swap(format_alt, &mut format);
215 }
216 }
217 _ => (),
218 }
219 }
220 }
221}