View Javadoc
1   /*
2    * Copyright 2016-2025 The OSHI Project Contributors
3    * SPDX-License-Identifier: MIT
4    */
5   package oshi.hardware.platform.linux;
6   
7   import java.io.File;
8   import java.io.FileFilter;
9   import java.io.IOException;
10  import java.nio.file.Paths;
11  import java.util.ArrayList;
12  import java.util.HashMap;
13  import java.util.List;
14  import java.util.Locale;
15  import java.util.Map;
16  import java.util.function.ToIntFunction;
17  import java.util.regex.Pattern;
18  import java.util.stream.Collectors;
19  import java.util.stream.Stream;
20  
21  import oshi.annotation.concurrent.ThreadSafe;
22  import oshi.hardware.common.AbstractSensors;
23  import oshi.util.ExecutingCommand;
24  import oshi.util.FileUtil;
25  import oshi.util.GlobalConfig;
26  import oshi.util.ParseUtil;
27  import oshi.util.platform.linux.SysPath;
28  
29  /**
30   * Sensors from WMI or Open Hardware Monitor
31   */
32  @ThreadSafe
33  final class LinuxSensors extends AbstractSensors {
34  
35      /**
36       * Configuration property for prioritizing hwmon temperature sensors by name. Common sensor names include:
37       * <ul>
38       * <li>coretemp: Intel CPU temperature</li>
39       * <li>k10temp: AMD CPU temperature (K10+ cores)</li>
40       * <li>zenpower: AMD Zen CPU temperature</li>
41       * <li>k8temp: AMD K8 CPU temperature</li>
42       * <li>via-cputemp: VIA CPU temperature</li>
43       * </ul>
44       */
45      public static final String OSHI_HWMON_NAME_PRIORITY = "oshi.os.linux.sensors.hwmon.names";
46      public static final String OSHI_THERMAL_ZONE_TYPE_PRIORITY = "oshi.os.linux.sensors.cpuTemperature.types";
47  
48      private static final List<String> HWMON_NAME_PRIORITY = Stream.of(GlobalConfig
49              .get(OSHI_HWMON_NAME_PRIORITY, "coretemp,k10temp,zenpower,k8temp,via-cputemp,acpitz").split(","))
50              .filter((s) -> !s.isEmpty()).collect(Collectors.toList());
51      private static final List<String> THERMAL_ZONE_TYPE_PRIORITY = Stream
52              .of(GlobalConfig.get(OSHI_THERMAL_ZONE_TYPE_PRIORITY, "cpu-thermal,x86_pkg_temp").split(","))
53              .filter((s) -> !s.isEmpty()).collect(Collectors.toList());
54  
55      private static final String TYPE = "type";
56      private static final String NAME = "/name";
57      // Possible sensor types. See sysfs documentation for others, e.g. current
58      private static final String TEMP = "temp";
59      private static final String FAN = "fan";
60      private static final String VOLTAGE = "in";
61      // Compile pattern for "temp<digits>_input"
62      private static final String INPUT_SUFFIX = "_input";
63      private static final Pattern TEMP_INPUT_PATTERN = Pattern.compile("^" + TEMP + "\\d+" + INPUT_SUFFIX + "$");
64  
65      // Base HWMON path, adds 0, 1, etc. to end for various sensors
66      private static final String HWMON = "hwmon";
67      private static final String HWMON_PATH = SysPath.HWMON + HWMON;
68      // Base THERMAL_ZONE path, adds 0, 1, etc. to end for temperature sensors
69      private static final String THERMAL_ZONE = "thermal_zone";
70      private static final String THERMAL_ZONE_PATH = SysPath.THERMAL + THERMAL_ZONE;
71  
72      // Initial test to see if we are running on a Pi
73      private static final boolean IS_PI = queryCpuTemperatureFromVcGenCmd() > 0;
74  
75      // Map from sensor to path. Built by constructor, so thread safe
76      private final Map<String, String> sensorsMap = new HashMap<>();
77  
78      /**
79       * <p>
80       * Constructor for LinuxSensors.
81       * </p>
82       */
83      LinuxSensors() {
84          if (!IS_PI) {
85              populateSensorsMapFromHwmon();
86              // if no temperature sensor is found in hwmon, try thermal_zone
87              if (!this.sensorsMap.containsKey(TEMP)) {
88                  populateSensorsMapFromThermalZone();
89              }
90          }
91      }
92  
93      /*
94       * Iterate over all hwmon* directories and look for sensor files, e.g., /sys/class/hwmon/hwmon0/temp1_input
95       */
96      private void populateSensorsMapFromHwmon() {
97          String selectedTempPath = null;
98          int selectedPriority = Integer.MAX_VALUE;
99  
100         int i = 0;
101         while (Paths.get(HWMON_PATH + i).toFile().isDirectory()) {
102             String path = HWMON_PATH + i;
103 
104             // Read the name file
105             String sensorName = FileUtil.getStringFromFile(path + NAME).trim();
106 
107             // Check if this is a temperature sensor with valid readings
108             File dir = new File(path);
109             File[] tempInputs = dir.listFiles((d, name) -> TEMP_INPUT_PATTERN.matcher(name).matches());
110 
111             if (tempInputs != null && tempInputs.length > 0) {
112                 int priority = HWMON_NAME_PRIORITY.indexOf(sensorName);
113                 if (priority >= 0 && priority < selectedPriority) {
114                     // Check if we can read at least one valid temperature
115                     for (File tempInput : tempInputs) {
116                         long temp = FileUtil.getLongFromFile(tempInput.getPath());
117                         if (temp > 0) {
118                             selectedPriority = priority;
119                             selectedTempPath = path;
120                             break;
121                         }
122                     }
123                 }
124             }
125 
126             // Handle other sensor types (fan, voltage)
127             for (String sensor : new String[] { FAN, VOLTAGE }) {
128                 final String sensorPrefix = sensor;
129                 // Final to pass to anonymous class
130                 getSensorFilesFromPath(path, sensor, f -> {
131                     try {
132                         return f.getName().startsWith(sensorPrefix) && f.getName().endsWith(INPUT_SUFFIX)
133                                 && FileUtil.getIntFromFile(f.getCanonicalPath()) > 0;
134                     } catch (IOException e) {
135                         return false;
136                     }
137                 });
138             }
139 
140             i++;
141         }
142 
143         if (selectedTempPath != null) {
144             this.sensorsMap.put(TEMP, selectedTempPath + "/temp");
145         }
146     }
147 
148     /*
149      * Iterate over all thermal_zone* directories and look for sensor files, e.g., /sys/class/thermal/thermal_zone0/temp
150      */
151     private void populateSensorsMapFromThermalZone() {
152         getSensorFilesFromPath(THERMAL_ZONE_PATH, TEMP, f -> f.getName().equals(TYPE) || f.getName().equals(TEMP),
153                 files -> Stream.of(files).filter(f -> TYPE.equals(f.getName())).findFirst().map(File::getPath)
154                         .map(FileUtil::getStringFromFile).map(THERMAL_ZONE_TYPE_PRIORITY::indexOf)
155                         .filter((index) -> index >= 0).orElse(THERMAL_ZONE_TYPE_PRIORITY.size()));
156     }
157 
158     /**
159      * Find all sensor files in a specific path and adds them to the sensorsMap
160      *
161      * @param sensorPath       A string containing the sensor path
162      * @param sensor           A string containing the sensor
163      * @param sensorFileFilter A FileFilter for detecting valid sensor files
164      */
165     private void getSensorFilesFromPath(String sensorPath, String sensor, FileFilter sensorFileFilter) {
166         getSensorFilesFromPath(sensorPath, sensor, sensorFileFilter, (files) -> 0);
167     }
168 
169     /**
170      * Find all sensor files in a specific path and adds them to the sensorsMap
171      *
172      * @param sensorPath       A string containing the sensor path
173      * @param sensor           A string containing the sensor
174      * @param sensorFileFilter A FileFilter for detecting valid sensor files
175      * @param prioritizer      A callback to prioritize between multiple sensors
176      */
177     private void getSensorFilesFromPath(String sensorPath, String sensor, FileFilter sensorFileFilter,
178             ToIntFunction<File[]> prioritizer) {
179         String selectedPath = null;
180         int selectedPriority = Integer.MAX_VALUE;
181 
182         int i = 0;
183         while (Paths.get(sensorPath + i).toFile().isDirectory()) {
184             String path = sensorPath + i;
185             File dir = new File(path);
186             File[] matchingFiles = dir.listFiles(sensorFileFilter);
187 
188             if (matchingFiles != null && matchingFiles.length > 0) {
189                 int priority = prioritizer.applyAsInt(matchingFiles);
190 
191                 if (priority < selectedPriority) {
192                     selectedPriority = priority;
193                     selectedPath = path;
194                 }
195             }
196             i++;
197         }
198 
199         if (selectedPath != null) {
200             this.sensorsMap.put(sensor, String.format(Locale.ROOT, "%s/%s", selectedPath, sensor));
201         }
202     }
203 
204     @Override
205     public double queryCpuTemperature() {
206         if (IS_PI) {
207             return queryCpuTemperatureFromVcGenCmd();
208         }
209         String tempStr = this.sensorsMap.get(TEMP);
210         if (tempStr != null) {
211             long millidegrees = 0;
212             if (tempStr.contains(HWMON)) {
213                 // First attempt should be CPU temperature at index 1, if available
214                 millidegrees = FileUtil.getLongFromFile(String.format(Locale.ROOT, "%s1%s", tempStr, INPUT_SUFFIX));
215                 // Should return a single line of millidegrees Celsius
216                 if (millidegrees > 0) {
217                     return millidegrees / 1000d;
218                 }
219                 // If temp1_input doesn't exist, iterate over temp2..temp6_input
220                 // and average
221                 long sum = 0;
222                 int count = 0;
223                 for (int i = 2; i <= 6; i++) {
224                     millidegrees = FileUtil
225                             .getLongFromFile(String.format(Locale.ROOT, "%s%d%s", tempStr, i, INPUT_SUFFIX));
226                     if (millidegrees > 0) {
227                         sum += millidegrees;
228                         count++;
229                     }
230                 }
231                 if (count > 0) {
232                     return sum / (count * 1000d);
233                 }
234             } else if (tempStr.contains(THERMAL_ZONE)) {
235                 // If temp2..temp6_input doesn't exist, try thermal_zone0
236                 millidegrees = FileUtil.getLongFromFile(tempStr);
237                 // Should return a single line of millidegrees Celsius
238                 if (millidegrees > 0) {
239                     return millidegrees / 1000d;
240                 }
241             }
242         }
243         return 0d;
244     }
245 
246     /**
247      * Retrieves temperature from Raspberry Pi
248      *
249      * @return The temperature on a Pi, 0 otherwise
250      */
251     private static double queryCpuTemperatureFromVcGenCmd() {
252         String tempStr = ExecutingCommand.getFirstAnswer("vcgencmd measure_temp");
253         // temp=50.8'C
254         if (tempStr.startsWith("temp=")) {
255             return ParseUtil.parseDoubleOrDefault(tempStr.replaceAll("[^\\d|\\.]+", ""), 0d);
256         }
257         return 0d;
258     }
259 
260     @Override
261     public int[] queryFanSpeeds() {
262         if (!IS_PI) {
263             String fanStr = this.sensorsMap.get(FAN);
264             if (fanStr != null) {
265                 List<Integer> speeds = new ArrayList<>();
266                 int fan = 1;
267                 for (;;) {
268                     String fanPath = String.format(Locale.ROOT, "%s%d%s", fanStr, fan, INPUT_SUFFIX);
269                     if (!new File(fanPath).exists()) {
270                         // No file found, we've reached max fans
271                         break;
272                     }
273                     // Should return a single line of RPM
274                     speeds.add(FileUtil.getIntFromFile(fanPath));
275                     // Done reading data for current fan, read next fan
276                     fan++;
277                 }
278                 int[] fanSpeeds = new int[speeds.size()];
279                 for (int i = 0; i < speeds.size(); i++) {
280                     fanSpeeds[i] = speeds.get(i);
281                 }
282                 return fanSpeeds;
283             }
284         }
285         return new int[0];
286     }
287 
288     @Override
289     public double queryCpuVoltage() {
290         if (IS_PI) {
291             return queryCpuVoltageFromVcGenCmd();
292         }
293         String voltageStr = this.sensorsMap.get(VOLTAGE);
294         if (voltageStr != null) {
295             // Should return a single line of millivolt
296             return FileUtil.getIntFromFile(String.format(Locale.ROOT, "%s1%s", voltageStr, INPUT_SUFFIX)) / 1000d;
297         }
298         return 0d;
299     }
300 
301     /**
302      * Retrieves voltage from Raspberry Pi
303      *
304      * @return The temperature on a Pi, 0 otherwise
305      */
306     private static double queryCpuVoltageFromVcGenCmd() {
307         // For raspberry pi
308         String voltageStr = ExecutingCommand.getFirstAnswer("vcgencmd measure_volts core");
309         // volt=1.20V
310         if (voltageStr.startsWith("volt=")) {
311             return ParseUtil.parseDoubleOrDefault(voltageStr.replaceAll("[^\\d|\\.]+", ""), 0d);
312         }
313         return 0d;
314     }
315 }