View Javadoc
1   /*
2    * Copyright 2016-2024 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  
17  import oshi.annotation.concurrent.ThreadSafe;
18  import oshi.hardware.common.AbstractSensors;
19  import oshi.util.ExecutingCommand;
20  import oshi.util.FileUtil;
21  import oshi.util.ParseUtil;
22  import oshi.util.platform.linux.SysPath;
23  
24  /**
25   * Sensors from WMI or Open Hardware Monitor
26   */
27  @ThreadSafe
28  final class LinuxSensors extends AbstractSensors {
29  
30      // Possible sensor types. See sysfs documentation for others, e.g. current
31      private static final String TEMP = "temp";
32      private static final String FAN = "fan";
33      private static final String VOLTAGE = "in";
34      private static final String[] SENSORS = { TEMP, FAN, VOLTAGE };
35  
36      // Base HWMON path, adds 0, 1, etc. to end for various sensors
37      private static final String HWMON = "hwmon";
38      private static final String HWMON_PATH = SysPath.HWMON + HWMON;
39      // Base THERMAL_ZONE path, adds 0, 1, etc. to end for temperature sensors
40      private static final String THERMAL_ZONE = "thermal_zone";
41      private static final String THERMAL_ZONE_PATH = SysPath.THERMAL + THERMAL_ZONE;
42  
43      // Initial test to see if we are running on a Pi
44      private static final boolean IS_PI = queryCpuTemperatureFromVcGenCmd() > 0;
45  
46      // Map from sensor to path. Built by constructor, so thread safe
47      private final Map<String, String> sensorsMap = new HashMap<>();
48  
49      /**
50       * <p>
51       * Constructor for LinuxSensors.
52       * </p>
53       */
54      LinuxSensors() {
55          if (!IS_PI) {
56              populateSensorsMapFromHwmon();
57              // if no temperature sensor is found in hwmon, try thermal_zone
58              if (!this.sensorsMap.containsKey(TEMP)) {
59                  populateSensorsMapFromThermalZone();
60              }
61          }
62      }
63  
64      /*
65       * Iterate over all hwmon* directories and look for sensor files, e.g., /sys/class/hwmon/hwmon0/temp1_input
66       */
67      private void populateSensorsMapFromHwmon() {
68          for (String sensor : SENSORS) {
69              // Final to pass to anonymous class
70              final String sensorPrefix = sensor;
71              // Find any *_input files in that path
72              getSensorFilesFromPath(HWMON_PATH, sensor, f -> {
73                  try {
74                      return f.getName().startsWith(sensorPrefix) && f.getName().endsWith("_input")
75                              && FileUtil.getIntFromFile(f.getCanonicalPath()) > 0;
76                  } catch (IOException e) {
77                      return false;
78                  }
79              });
80          }
81      }
82  
83      /*
84       * Iterate over all thermal_zone* directories and look for sensor files, e.g., /sys/class/thermal/thermal_zone0/temp
85       */
86      private void populateSensorsMapFromThermalZone() {
87          getSensorFilesFromPath(THERMAL_ZONE_PATH, TEMP, f -> f.getName().equals(TEMP));
88      }
89  
90      /**
91       * Find all sensor files in a specific path and adds them to the hwmonMap
92       *
93       * @param sensorPath       A string containing the sensor path
94       * @param sensor           A string containing the sensor
95       * @param sensorFileFilter A FileFilter for detecting valid sensor files
96       */
97      private void getSensorFilesFromPath(String sensorPath, String sensor, FileFilter sensorFileFilter) {
98          int i = 0;
99          while (Paths.get(sensorPath + i).toFile().isDirectory()) {
100             String path = sensorPath + i;
101             File dir = new File(path);
102             File[] matchingFiles = dir.listFiles(sensorFileFilter);
103             if (matchingFiles != null && matchingFiles.length > 0) {
104                 this.sensorsMap.put(sensor, String.format(Locale.ROOT, "%s/%s", path, sensor));
105             }
106             i++;
107         }
108     }
109 
110     @Override
111     public double queryCpuTemperature() {
112         if (IS_PI) {
113             return queryCpuTemperatureFromVcGenCmd();
114         }
115         String tempStr = this.sensorsMap.get(TEMP);
116         if (tempStr != null) {
117             long millidegrees = 0;
118             if (tempStr.contains(HWMON)) {
119                 // First attempt should be CPU temperature at index 1, if available
120                 millidegrees = FileUtil.getLongFromFile(String.format(Locale.ROOT, "%s1_input", tempStr));
121                 // Should return a single line of millidegrees Celsius
122                 if (millidegrees > 0) {
123                     return millidegrees / 1000d;
124                 }
125                 // If temp1_input doesn't exist, iterate over temp2..temp6_input
126                 // and average
127                 long sum = 0;
128                 int count = 0;
129                 for (int i = 2; i <= 6; i++) {
130                     millidegrees = FileUtil.getLongFromFile(String.format(Locale.ROOT, "%s%d_input", tempStr, i));
131                     if (millidegrees > 0) {
132                         sum += millidegrees;
133                         count++;
134                     }
135                 }
136                 if (count > 0) {
137                     return sum / (count * 1000d);
138                 }
139             } else if (tempStr.contains(THERMAL_ZONE)) {
140                 // If temp2..temp6_input doesn't exist, try thermal_zone0
141                 millidegrees = FileUtil.getLongFromFile(tempStr);
142                 // Should return a single line of millidegrees Celsius
143                 if (millidegrees > 0) {
144                     return millidegrees / 1000d;
145                 }
146             }
147         }
148         return 0d;
149     }
150 
151     /**
152      * Retrieves temperature from Raspberry Pi
153      *
154      * @return The temperature on a Pi, 0 otherwise
155      */
156     private static double queryCpuTemperatureFromVcGenCmd() {
157         String tempStr = ExecutingCommand.getFirstAnswer("vcgencmd measure_temp");
158         // temp=50.8'C
159         if (tempStr.startsWith("temp=")) {
160             return ParseUtil.parseDoubleOrDefault(tempStr.replaceAll("[^\\d|\\.]+", ""), 0d);
161         }
162         return 0d;
163     }
164 
165     @Override
166     public int[] queryFanSpeeds() {
167         if (!IS_PI) {
168             String fanStr = this.sensorsMap.get(FAN);
169             if (fanStr != null) {
170                 List<Integer> speeds = new ArrayList<>();
171                 int fan = 1;
172                 for (;;) {
173                     String fanPath = String.format(Locale.ROOT, "%s%d_input", fanStr, fan);
174                     if (!new File(fanPath).exists()) {
175                         // No file found, we've reached max fans
176                         break;
177                     }
178                     // Should return a single line of RPM
179                     speeds.add(FileUtil.getIntFromFile(fanPath));
180                     // Done reading data for current fan, read next fan
181                     fan++;
182                 }
183                 int[] fanSpeeds = new int[speeds.size()];
184                 for (int i = 0; i < speeds.size(); i++) {
185                     fanSpeeds[i] = speeds.get(i);
186                 }
187                 return fanSpeeds;
188             }
189         }
190         return new int[0];
191     }
192 
193     @Override
194     public double queryCpuVoltage() {
195         if (IS_PI) {
196             return queryCpuVoltageFromVcGenCmd();
197         }
198         String voltageStr = this.sensorsMap.get(VOLTAGE);
199         if (voltageStr != null) {
200             // Should return a single line of millivolt
201             return FileUtil.getIntFromFile(String.format(Locale.ROOT, "%s1_input", voltageStr)) / 1000d;
202         }
203         return 0d;
204     }
205 
206     /**
207      * Retrieves voltage from Raspberry Pi
208      *
209      * @return The temperature on a Pi, 0 otherwise
210      */
211     private static double queryCpuVoltageFromVcGenCmd() {
212         // For raspberry pi
213         String voltageStr = ExecutingCommand.getFirstAnswer("vcgencmd measure_volts core");
214         // volt=1.20V
215         if (voltageStr.startsWith("volt=")) {
216             return ParseUtil.parseDoubleOrDefault(voltageStr.replaceAll("[^\\d|\\.]+", ""), 0d);
217         }
218         return 0d;
219     }
220 }