1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215
//! The system temperature
//!
//! This block displays the system temperature, based on `libsensors` library.
//!
//! This block has two modes: "collapsed", which uses only color as an indicator, and "expanded",
//! which shows the content of a `format` string. The average, minimum, and maximum temperatures
//! are computed using all sensors displayed by `sensors`, or optionally filtered by `chip` and
//! `inputs`.
//!
//! Requires `libsensors` and appropriate kernel modules for your hardware.
//!
//! Run `sensors` command to list available chips and inputs.
//!
//! Note that the colour of the block is always determined by the maximum temperature across all
//! sensors, not the average. You may need to keep this in mind if you have a misbehaving sensor.
//!
//! # Configuration
//!
//! Key | Values | Default
//! ----|--------|--------
//! `format` | A string to customise the output of this block. See below for available placeholders | `" $icon $average avg, $max max "`
//! `format_alt` | If set, block will switch between `format` and `format_alt` on every click | `None`
//! `interval` | Update interval in seconds | `5`
//! `scale` | Either `"celsius"` or `"fahrenheit"` | `"celsius"`
//! `good` | Maximum temperature to set state to good | `20` °C (`68` °F)
//! `idle` | Maximum temperature to set state to idle | `45` °C (`113` °F)
//! `info` | Maximum temperature to set state to info | `60` °C (`140` °F)
//! `warning` | Maximum temperature to set state to warning. Beyond this temperature, state is set to critical | `80` °C (`176` °F)
//! `chip` | Narrows the results to a given chip name. `*` may be used as a wildcard. | None
//! `inputs` | Narrows the results to individual inputs reported by each chip. | None
//!
//! Action | Description | Default button
//! ----------------|-------------------------------------------|---------------
//! `toggle_format` | Toggles between `format` and `format_alt` | Left
//!
//! Placeholder | Value | Type | Unit
//! ------------|--------------------------------------|--------|--------
//! `min` | Minimum temperature among all inputs | Number | Degrees
//! `average` | Average temperature among all inputs | Number | Degrees
//! `max` | Maximum temperature among all inputs | Number | Degrees
//!
//! Note that when block is collapsed, no placeholders are provided.
//!
//! # Example
//!
//! ```toml
//! [[block]]
//! block = "temperature"
//! format = " $icon $max max "
//! format_alt = " $icon $min min, $max max, $average avg "
//! interval = 10
//! chip = "*-isa-*"
//! ```
//!
//! # Icons Used
//! - `thermometer`
use super::prelude::*;
use sensors::FeatureType::SENSORS_FEATURE_TEMP;
use sensors::Sensors;
use sensors::SubfeatureType::SENSORS_SUBFEATURE_TEMP_INPUT;
const DEFAULT_GOOD: f64 = 20.0;
const DEFAULT_IDLE: f64 = 45.0;
const DEFAULT_INFO: f64 = 60.0;
const DEFAULT_WARN: f64 = 80.0;
#[derive(Deserialize, Debug, SmartDefault)]
#[serde(deny_unknown_fields, default)]
pub struct Config {
pub format: FormatConfig,
pub format_alt: Option<FormatConfig>,
#[default(5.into())]
pub interval: Seconds,
pub scale: TemperatureScale,
pub good: Option<f64>,
pub idle: Option<f64>,
pub info: Option<f64>,
pub warning: Option<f64>,
pub chip: Option<String>,
pub inputs: Option<Vec<String>>,
}
#[derive(Deserialize, Debug, SmartDefault, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum TemperatureScale {
#[default]
Celsius,
Fahrenheit,
}
impl TemperatureScale {
#[allow(clippy::wrong_self_convention)]
pub fn from_celsius(self, val: f64) -> f64 {
match self {
Self::Celsius => val,
Self::Fahrenheit => val * 1.8 + 32.0,
}
}
}
pub async fn run(config: &Config, api: &CommonApi) -> Result<()> {
let mut actions = api.get_actions()?;
api.set_default_actions(&[(MouseButton::Left, None, "toggle_format")])?;
let mut format = config
.format
.with_default(" $icon $average avg, $max max ")?;
let mut format_alt = match &config.format_alt {
Some(f) => Some(f.with_default("")?),
None => None,
};
let good = config
.good
.unwrap_or_else(|| config.scale.from_celsius(DEFAULT_GOOD));
let idle = config
.idle
.unwrap_or_else(|| config.scale.from_celsius(DEFAULT_IDLE));
let info = config
.info
.unwrap_or_else(|| config.scale.from_celsius(DEFAULT_INFO));
let warn = config
.warning
.unwrap_or_else(|| config.scale.from_celsius(DEFAULT_WARN));
loop {
let chip = config.chip.clone();
let inputs = config.inputs.clone();
let config_scale = config.scale;
let temp = tokio::task::spawn_blocking(move || {
let mut vals = Vec::new();
let sensors = Sensors::new();
let chips = match &chip {
Some(chip) => sensors
.detected_chips(chip)
.error("Failed to create chip iterator")?,
None => sensors.into_iter(),
};
for chip in chips {
for feat in chip {
if *feat.feature_type() != SENSORS_FEATURE_TEMP {
continue;
}
if let Some(inputs) = &inputs {
let label = feat.get_label().error("Failed to get input label")?;
if !inputs.contains(&label) {
continue;
}
}
for subfeat in feat {
if *subfeat.subfeature_type() == SENSORS_SUBFEATURE_TEMP_INPUT {
if let Ok(value) = subfeat.get_value() {
if (-100.0..=150.0).contains(&value) {
vals.push(config_scale.from_celsius(value));
} else {
eprintln!(
"Temperature ({value}) outside of range ([-100, 150])"
);
}
}
}
}
}
}
Ok(vals)
})
.await
.error("Failed to join tokio task")??;
let min_temp = temp
.iter()
.min_by(|a, b| a.partial_cmp(b).unwrap())
.cloned()
.unwrap_or(0.0);
let max_temp = temp
.iter()
.max_by(|a, b| a.partial_cmp(b).unwrap())
.cloned()
.unwrap_or(0.0);
let avg_temp = temp.iter().sum::<f64>() / temp.len() as f64;
let mut widget = Widget::new().with_format(format.clone());
widget.state = match max_temp {
x if x <= good => State::Good,
x if x <= idle => State::Idle,
x if x <= info => State::Info,
x if x <= warn => State::Warning,
_ => State::Critical,
};
widget.set_values(map! {
"icon" => Value::icon_progression_bound("thermometer", max_temp, good, warn),
"average" => Value::degrees(avg_temp),
"min" => Value::degrees(min_temp),
"max" => Value::degrees(max_temp),
});
api.set_widget(widget)?;
select! {
_ = sleep(config.interval.0) => (),
_ = api.wait_for_update_request() => (),
Some(action) = actions.recv() => match action.as_ref() {
"toggle_format" => {
if let Some(format_alt) = &mut format_alt {
std::mem::swap(format_alt, &mut format);
}
}
_ => (),
}
}
}
}