Skip to main content

i3status_rs/blocks/
temperature.rs

1//! The system temperature
2//!
3//! This block displays the system temperature, based on `libsensors` library.
4//!
5//! This block has two modes: "collapsed", which uses only color as an indicator, and "expanded",
6//! which shows the content of a `format` string. The average, minimum, and maximum temperatures
7//! are computed using all sensors displayed by `sensors`, or optionally filtered by `chip` and
8//! `inputs`.
9//!
10//! Requires `libsensors` and appropriate kernel modules for your hardware.
11//!
12//! Run `sensors` command to list available chips and inputs.
13//!
14//! Note that the colour of the block is always determined by the maximum temperature across all
15//! sensors, not the average. You may need to keep this in mind if you have a misbehaving sensor.
16//!
17//! # Configuration
18//!
19//! Key | Values | Default
20//! ----|--------|--------
21//! `format` | A string to customise the output of this block. See below for available placeholders | `" $icon $average avg, $max max "`
22//! `format_alt` | If set, block will switch between `format` and `format_alt` on every click | `None`
23//! `interval` | Update interval in seconds | `5`
24//! `scale` | Either `"celsius"` or `"fahrenheit"` | `"celsius"`
25//! `good` | Maximum temperature to set state to good | `20` °C (`68` °F)
26//! `idle` | Maximum temperature to set state to idle | `45` °C (`113` °F)
27//! `info` | Maximum temperature to set state to info | `60` °C (`140` °F)
28//! `warning` | Maximum temperature to set state to warning. Beyond this temperature, state is set to critical | `80` °C (`176` °F)
29//! `chip` | Narrows the results to a given chip name. `*` may be used as a wildcard. | None
30//! `inputs` | Narrows the results to individual inputs reported by each chip. | None
31//!
32//! Action          | Description                               | Default button
33//! ----------------|-------------------------------------------|---------------
34//! `toggle_format` | Toggles between `format` and `format_alt` | Left
35//!
36//! Placeholder | Value                                | Type   | Unit
37//! ------------|--------------------------------------|--------|--------
38//! `min`       | Minimum temperature among all inputs | Number | Degrees
39//! `average`   | Average temperature among all inputs | Number | Degrees
40//! `max`       | Maximum temperature among all inputs | Number | Degrees
41//!
42//! Note that when block is collapsed, no placeholders are provided.
43//!
44//! # Example
45//!
46//! ```toml
47//! [[block]]
48//! block = "temperature"
49//! format = " $icon $max max "
50//! format_alt = " $icon $min min, $max max, $average avg "
51//! interval = 10
52//! chip = "*-isa-*"
53//! ```
54//!
55//! # Icons Used
56//! - `thermometer`
57
58use 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}