use crate::Component;
use std::collections::HashMap;
use std::fs::{read_dir, File};
use std::io::Read;
use std::path::{Path, PathBuf};
#[derive(Default)]
pub(crate) struct ComponentInner {
device_model: Option<String>,
name: String,
temperature: Option<f32>,
max: Option<f32>,
threshold_max: Option<f32>,
threshold_min: Option<f32>,
threshold_critical: Option<f32>,
sensor_type: Option<TermalSensorType>,
label: String,
input_file: Option<PathBuf>,
highest_file: Option<PathBuf>,
}
fn get_file_line(file: &Path, capacity: usize) -> Option<String> {
let mut reader = String::with_capacity(capacity);
let mut f = File::open(file).ok()?;
f.read_to_string(&mut reader).ok()?;
reader.truncate(reader.trim_end().len());
Some(reader)
}
fn read_number_from_file<N>(file: &Path) -> Option<N>
where
N: std::str::FromStr,
{
let mut reader = [0u8; 32];
let mut f = File::open(file).ok()?;
let n = f.read(&mut reader).ok()?;
let number = &reader[..n];
let number = std::str::from_utf8(number).ok()?;
let number = number.trim();
if cfg!(feature = "debug") {
assert!(!number.contains('\n') && !number.contains('\0'));
}
number.parse().ok()
}
#[inline]
fn get_temperature_from_file(file: &Path) -> Option<f32> {
let temp = read_number_from_file(file);
convert_temp_celsius(temp)
}
#[inline]
fn convert_temp_celsius(temp: Option<i32>) -> Option<f32> {
temp.map(|n| (n as f32) / 1000f32)
}
enum TermalSensorType {
CPUEmbeddedDiode,
Transistor3904,
ThermalDiode,
Thermistor,
AMDAMDSI,
IntelPECI,
#[allow(dead_code)]
Unknown(u8),
}
impl From<u8> for TermalSensorType {
fn from(input: u8) -> Self {
match input {
0 => Self::CPUEmbeddedDiode,
1 => Self::Transistor3904,
3 => Self::ThermalDiode,
4 => Self::Thermistor,
5 => Self::AMDAMDSI,
6 => Self::IntelPECI,
n => Self::Unknown(n),
}
}
}
fn fill_component(component: &mut ComponentInner, item: &str, folder: &Path, file: &str) {
let hwmon_file = folder.join(file);
match item {
"type" => {
component.sensor_type =
read_number_from_file::<u8>(&hwmon_file).map(TermalSensorType::from)
}
"input" => {
let temperature = get_temperature_from_file(&hwmon_file);
component.input_file = Some(hwmon_file);
component.temperature = temperature;
if component.max.is_none() {
component.max = temperature;
}
}
"label" => component.label = get_file_line(&hwmon_file, 10).unwrap_or_default(),
"highest" => {
component.max = get_temperature_from_file(&hwmon_file).or(component.temperature);
component.highest_file = Some(hwmon_file);
}
"max" => component.threshold_max = get_temperature_from_file(&hwmon_file),
"min" => component.threshold_min = get_temperature_from_file(&hwmon_file),
"crit" => component.threshold_critical = get_temperature_from_file(&hwmon_file),
_ => {
sysinfo_debug!(
"This hwmon-temp file is still not supported! Contributions are appreciated.;) {:?}",
hwmon_file,
);
}
}
}
impl ComponentInner {
fn from_hwmon(components: &mut Vec<Component>, folder: &Path) -> Option<()> {
let dir = read_dir(folder).ok()?;
let mut matchings: HashMap<u32, Component> = HashMap::with_capacity(10);
for entry in dir.flatten() {
let Ok(file_type) = entry.file_type() else {
continue;
};
if file_type.is_dir() {
continue;
}
let entry = entry.path();
let filename = entry.file_name().and_then(|x| x.to_str()).unwrap_or("");
if !filename.starts_with("temp") {
continue;
}
let (id, item) = filename.split_once('_')?;
let id = id.get(4..)?.parse::<u32>().ok()?;
let component = matchings.entry(id).or_insert_with(|| Component {
inner: ComponentInner::default(),
});
let component = &mut component.inner;
let name = get_file_line(&folder.join("name"), 16);
component.name = name.unwrap_or_default();
let device_model = get_file_line(&folder.join("device/model"), 16);
component.device_model = device_model;
fill_component(component, item, folder, filename);
}
let compo = matchings
.into_iter()
.map(|(id, mut c)| {
c.inner.label = c.inner.format_label("temp", id);
c
})
.filter(|c| c.inner.input_file.is_some());
components.extend(compo);
Some(())
}
fn format_label(&self, class: &str, id: u32) -> String {
let ComponentInner {
device_model,
name,
label,
..
} = self;
let has_label = !label.is_empty();
match (has_label, device_model) {
(true, Some(device_model)) => {
format!("{name} {label} {device_model} {class}{id}")
}
(true, None) => format!("{name} {label}"),
(false, Some(device_model)) => format!("{name} {device_model}"),
(false, None) => format!("{name} {class}{id}"),
}
}
pub(crate) fn temperature(&self) -> f32 {
self.temperature.unwrap_or(f32::NAN)
}
pub(crate) fn max(&self) -> f32 {
self.max.unwrap_or(f32::NAN)
}
pub(crate) fn critical(&self) -> Option<f32> {
self.threshold_critical
}
pub(crate) fn label(&self) -> &str {
&self.label
}
pub(crate) fn refresh(&mut self) {
let current = self
.input_file
.as_ref()
.and_then(|file| get_temperature_from_file(file.as_path()));
let max = self
.highest_file
.as_ref()
.and_then(|file| get_temperature_from_file(file.as_path()))
.or_else(|| {
let last = self.temperature?;
let current = current?;
Some(last.max(current))
});
self.max = max;
self.temperature = current;
}
}
pub(crate) struct ComponentsInner {
components: Vec<Component>,
}
impl ComponentsInner {
pub(crate) fn new() -> Self {
Self {
components: Vec::with_capacity(4),
}
}
pub(crate) fn from_vec(components: Vec<Component>) -> Self {
Self { components }
}
pub(crate) fn into_vec(self) -> Vec<Component> {
self.components
}
pub(crate) fn list(&self) -> &[Component] {
&self.components
}
pub(crate) fn list_mut(&mut self) -> &mut [Component] {
&mut self.components
}
pub(crate) fn refresh_list(&mut self) {
self.components.clear();
if let Ok(dir) = read_dir(Path::new("/sys/class/hwmon/")) {
for entry in dir.flatten() {
let Ok(file_type) = entry.file_type() else {
continue;
};
let entry = entry.path();
if !file_type.is_file()
&& entry
.file_name()
.and_then(|x| x.to_str())
.unwrap_or("")
.starts_with("hwmon")
{
ComponentInner::from_hwmon(&mut self.components, &entry);
}
}
}
}
}