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 [MultiFormat][MaybeMultiFormatConfig] string to customise the output of this block. See below for available placeholders | `[" $icon $average avg, $max max "]`
22//! `interval` | Update interval in seconds | `5`
23//! `scale` | Either `"celsius"` or `"fahrenheit"` | `"celsius"`
24//! `good` | Maximum temperature to set state to good | `20` °C (`68` °F)
25//! `idle` | Maximum temperature to set state to idle | `45` °C (`113` °F)
26//! `info` | Maximum temperature to set state to info | `60` °C (`140` °F)
27//! `warning` | Maximum temperature to set state to warning. Beyond this temperature, state is set to critical | `80` °C (`176` °F)
28//! `chip` | Narrows the results to a given chip name. `*` may be used as a wildcard. | None
29//! `inputs` | Narrows the results to individual inputs reported by each chip. | None
30//!
31//! Action          | Description                               | Default button
32//! ----------------|-------------------------------------------|---------------
33//! `toggle_format` **DEPRECATED** | Toggles between `format` and `format_alt` | -
34//! `next_format`  | Switches to the next format in the list     | Left
35//! `prev_format`  | Switches to the previous format in the list | Right
36//!
37//! Placeholder | Value                                | Type   | Unit
38//! ------------|--------------------------------------|--------|--------
39//! `min`       | Minimum temperature among all inputs | Number | Degrees
40//! `average`   | Average temperature among all inputs | Number | Degrees
41//! `max`       | Maximum temperature among all inputs | Number | Degrees
42//!
43//! Note that when block is collapsed, no placeholders are provided.
44//!
45//! # Example
46//!
47//! ```toml
48//! [[block]]
49//! block = "temperature"
50//! format = " $icon $max max "
51//! format_alt = " $icon $min min, $max max, $average avg "
52//! interval = 10
53//! chip = "*-isa-*"
54//! ```
55//!
56//! # Icons Used
57//! - `thermometer`
58
59use 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 async fn run(config: &Config, api: &CommonApi) -> Result<()> {
112    let mut actions = api.get_actions()?;
113    api.set_default_actions(&[
114        (MouseButton::Left, None, "next_format"),
115        (MouseButton::Right, None, "prev_format"),
116    ])?;
117
118    let mut formats = config
119        .formats
120        .with_default(" $icon $average avg, $max max ")?;
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(formats.get_format());
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                "next_format" | "toggle_format" => {
213                    formats.next_format();
214                }
215                "prev_format" => {
216                    formats.prev_format();
217                }
218                _ => (),
219            }
220        }
221    }
222}