1use std::fs;
8use std::path::Path;
9
10use super::{SysRoots, read_i64, read_trimmed, read_u64, sorted_entries};
11use crate::snapshot::{Fan, SensorChip, Temperature};
12
13const CPU_CHIPS: &[&str] = &["k10temp", "zenpower", "coretemp", "cpu_thermal"];
15
16#[derive(Debug, Clone, Default)]
18pub struct HwmonChip {
19 pub chip: SensorChip,
21 pub power_w: Vec<(String, f32)>,
23 pub is_apu: bool,
26}
27
28pub fn read_chips(roots: &SysRoots) -> Vec<HwmonChip> {
30 let class_dir = roots.sys.join("class/hwmon");
31 sorted_entries(&class_dir)
32 .into_iter()
33 .filter_map(|entry| read_chip(&class_dir.join(&entry), &entry))
34 .filter(|c| {
37 !c.chip.temperatures.is_empty() || !c.chip.fans.is_empty() || !c.power_w.is_empty()
38 })
39 .collect()
40}
41
42pub fn is_cpu_chip(chip: &SensorChip) -> bool {
44 CPU_CHIPS.contains(&chip.name.as_str())
45}
46
47fn read_chip(dir: &Path, entry: &str) -> Option<HwmonChip> {
49 let name = read_trimmed(&dir.join("name"))?;
50 let device = fs::read_link(dir.join("device"))
54 .ok()
55 .and_then(|target| target.file_name().map(|n| n.to_string_lossy().into_owned()))
56 .unwrap_or_else(|| entry.to_owned());
57 let key = format!("{name}:{device}");
58
59 let files = sorted_entries(dir);
60 let temperatures = channel_numbers(&files, "temp")
61 .into_iter()
62 .filter_map(|n| read_temperature(dir, &key, n))
63 .collect();
64 let fans = channel_numbers(&files, "fan")
65 .into_iter()
66 .filter_map(|n| {
67 let rpm = read_u64(&dir.join(format!("fan{n}_input")))?;
68 let label = read_trimmed(&dir.join(format!("fan{n}_label")))
69 .unwrap_or_else(|| format!("fan{n}"));
70 Some(Fan {
71 key: format!("{key}/fan{n}"),
72 label,
73 rpm: rpm.min(u32::MAX as u64) as u32,
74 })
75 })
76 .collect();
77 let power_w = channel_numbers(&files, "power")
78 .into_iter()
79 .filter_map(|n| {
80 let micro = read_u64(&dir.join(format!("power{n}_input")))
83 .or_else(|| read_u64(&dir.join(format!("power{n}_average"))))?;
84 let label = read_trimmed(&dir.join(format!("power{n}_label")))
85 .unwrap_or_else(|| format!("power{n}"));
86 Some((label, micro as f32 / 1_000_000.0))
87 })
88 .collect();
89
90 let is_apu = name == "amdgpu"
91 && read_trimmed(&dir.join("in1_label")).is_some_and(|label| label == "vddnb");
92 Some(HwmonChip {
93 chip: SensorChip {
94 key,
95 name,
96 temperatures,
97 fans,
98 },
99 power_w,
100 is_apu,
101 })
102}
103
104fn read_temperature(dir: &Path, chip_key: &str, n: u32) -> Option<Temperature> {
106 let milli =
107 |suffix: &str| read_i64(&dir.join(format!("temp{n}_{suffix}"))).map(|v| v as f32 / 1000.0);
108 let celsius = milli("input")?;
109 let label =
110 read_trimmed(&dir.join(format!("temp{n}_label"))).unwrap_or_else(|| format!("temp{n}"));
111 Some(Temperature {
112 key: format!("{chip_key}/temp{n}"),
113 label,
114 celsius,
115 max_celsius: milli("max"),
116 critical_celsius: milli("crit"),
117 })
118}
119
120fn channel_numbers(files: &[String], prefix: &str) -> Vec<u32> {
123 let mut numbers: Vec<u32> = files
124 .iter()
125 .filter_map(|f| {
126 let rest = f.strip_prefix(prefix)?;
127 let (num, suffix) = rest.split_once('_')?;
128 if suffix == "input" || (prefix == "power" && suffix == "average") {
129 num.parse().ok()
130 } else {
131 None
132 }
133 })
134 .collect();
135 numbers.sort_unstable();
136 numbers.dedup();
137 numbers
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use crate::linux::testutil::{symlink, write};
144 use std::path::PathBuf;
145
146 fn roots(dir: &Path) -> SysRoots {
147 SysRoots {
148 sys: dir.join("sys"),
149 proc: dir.join("proc"),
150 }
151 }
152
153 #[test]
154 fn reads_temperatures_fans_and_power() {
155 let tmp = tempfile::tempdir().unwrap();
156 let root = tmp.path();
157 write(root, "sys/class/hwmon/hwmon5/name", "k10temp\n");
159 write(root, "sys/class/hwmon/hwmon5/temp1_input", "45250\n");
160 write(root, "sys/class/hwmon/hwmon5/temp1_label", "Tctl\n");
161 write(root, "sys/class/hwmon/hwmon5/temp3_input", "40000\n");
162 write(root, "sys/class/hwmon/hwmon5/temp3_label", "Tccd1\n");
163 symlink(
164 root,
165 "sys/class/hwmon/hwmon5/device",
166 &PathBuf::from("../../../0000:00:18.3"),
167 );
168 write(root, "sys/class/hwmon/hwmon10/name", "nct6686\n");
170 write(root, "sys/class/hwmon/hwmon10/fan1_input", "886\n");
171 write(root, "sys/class/hwmon/hwmon10/fan2_input", "0\n");
172 symlink(
173 root,
174 "sys/class/hwmon/hwmon10/device",
175 &PathBuf::from("../../../nct6683.2592"),
176 );
177 write(root, "sys/class/hwmon/hwmon2/name", "amdgpu\n");
179 write(root, "sys/class/hwmon/hwmon2/power1_input", "12500000\n");
180 write(root, "sys/class/hwmon/hwmon2/power1_label", "PPT\n");
181 write(root, "sys/class/hwmon/hwmon2/in1_label", "vddnb\n");
182 write(root, "sys/class/hwmon/hwmon7/name", "amdgpu\n");
184 write(root, "sys/class/hwmon/hwmon7/power1_input", "80000000\n");
185 write(root, "sys/class/hwmon/hwmon7/in0_label", "vddgfx\n");
186
187 write(root, "sys/class/hwmon/hwmon3/name", "hid_battery\n");
189
190 let chips = read_chips(&roots(root));
191 let keys: Vec<&str> = chips.iter().map(|c| c.chip.key.as_str()).collect();
193 assert_eq!(
194 keys,
195 vec![
196 "amdgpu:hwmon2",
197 "k10temp:0000:00:18.3",
198 "amdgpu:hwmon7",
199 "nct6686:nct6683.2592"
200 ]
201 );
202
203 let amdgpu = &chips[0];
204 assert_eq!(amdgpu.power_w, vec![("PPT".to_owned(), 12.5)]);
205 assert!(amdgpu.is_apu);
206 assert!(!chips[2].is_apu, "discrete GPU");
207
208 let k10 = &chips[1].chip;
209 assert!(is_cpu_chip(k10));
210 assert_eq!(k10.temperatures.len(), 2);
211 assert_eq!(k10.temperatures[0].label, "Tctl");
212 assert_eq!(k10.temperatures[0].key, "k10temp:0000:00:18.3/temp1");
213 assert!((k10.temperatures[0].celsius - 45.25).abs() < 1e-6);
214 assert_eq!(k10.temperatures[1].label, "Tccd1");
215 assert_eq!(k10.temperatures[1].max_celsius, None);
216
217 let nct = &chips[3].chip;
218 assert_eq!(nct.fans.len(), 2);
219 assert_eq!(nct.fans[0].key, "nct6686:nct6683.2592/fan1");
220 assert_eq!(nct.fans[0].label, "fan1");
221 assert_eq!(nct.fans[0].rpm, 886);
222 assert_eq!(nct.fans[1].rpm, 0);
223 }
224
225 #[test]
226 fn missing_class_directory_yields_no_chips() {
227 let tmp = tempfile::tempdir().unwrap();
228 assert!(read_chips(&roots(tmp.path())).is_empty());
229 }
230}