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