diff --git a/usr/lib/linuxmint/mintreport/app.py b/usr/lib/linuxmint/mintreport/app.py
index 5a427df..651721c 100755
--- a/usr/lib/linuxmint/mintreport/app.py
+++ b/usr/lib/linuxmint/mintreport/app.py
@@ -25,6 +25,8 @@
from pci import PCIListWidget
from usb import USBListWidget
from gpu import GPUListWidget
+from sensors import SensorsListWidget
+
setproctitle.setproctitle("mintreport")
_ = xapp.util.l10n("mintreport")
@@ -68,6 +70,7 @@ def on_command_line(self, app, command_line):
group.add_argument("--bios", action="store_const", dest="page", const="bios")
group.add_argument("--pci", action="store_const", dest="page", const="pci")
group.add_argument("--gpu", action="store_const", dest="page", const="gpu")
+ group.add_argument("--sensors", action="store_const", dest="page", const="sensors")
argv = command_line.get_arguments()[1:]
args, _ = parser.parse_known_args(argv)
if args.page is None:
@@ -409,7 +412,6 @@ def sort_by_date(model, a, b, *args):
self.builder.get_object("button_sysinfo_copy").connect("clicked", self.copy_inxi_info)
self.builder.get_object("button_sysinfo_upload").connect("clicked", self.upload_inxi_info)
-
# USB page
self.usb_widget = USBListWidget()
self.builder.get_object("box_usb_widget").pack_start(self.usb_widget, True, True, 0)
@@ -418,6 +420,10 @@ def sort_by_date(model, a, b, *args):
self.pci_widget = PCIListWidget()
self.builder.get_object("box_pci_widget").pack_start(self.pci_widget, True, True, 0)
+ # Sensors page
+ self.sensors_widget = SensorsListWidget()
+ self.builder.get_object("box_sensors_widget").pack_start(self.sensors_widget, True, True, 0)
+
# BIOS page
self.bios_widget = BIOSListWidget()
self.builder.get_object("box_bios_widget").add(self.bios_widget)
@@ -431,6 +437,7 @@ def sort_by_date(model, a, b, *args):
self.load_pci()
self.load_bios()
self.load_gpu()
+ self.load_sensors()
def show_page(self, page_name):
page_name = f"page_{page_name}"
@@ -587,6 +594,10 @@ def load_usb(self):
def load_pci(self):
self.pci_widget.load()
+ @xt.run_async
+ def load_sensors(self):
+ self.sensors_widget.load()
+
@xt.run_async
def load_bios(self):
self.bios_widget.load()
diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py
new file mode 100644
index 0000000..0aa6671
--- /dev/null
+++ b/usr/lib/linuxmint/mintreport/sensors.py
@@ -0,0 +1,288 @@
+import os
+import gi
+import xapp.util
+import re
+from enum import IntEnum
+
+gi.require_version("Gtk", "3.0")
+from gi.repository import Gtk, GLib, Pango
+
+_ = xapp.util.l10n("mintreport")
+
+SYS_HWMON = "/sys/class/hwmon"
+
+COL_NAME, COL_VALUE, COL_UNIT, COL_SENSITIVE, COL_ICON_NAME = range(5)
+
+class SensorType(IntEnum):
+ TEMP = 0
+ FAN = 1
+ PWM = 2
+ FREQ = 3
+ POWER = 4
+ VOLTAGE = 5
+ CURRENT = 6
+ ENERGY = 7
+
+
+SENSOR_SPECS = {
+ SensorType.TEMP: {
+ "prefix":"temp",
+ "suffix":"_input",
+ "format":lambda raw: f"{int(raw)/1000:.1f}",
+ "unit":"°C",
+ "icon":"xsi-temperature-symbolic"
+ },
+ SensorType.FAN: {
+ "prefix":"fan",
+ "suffix":"_input",
+ "format":lambda raw: raw.strip(),
+ "unit":_("RPM"),
+ "icon":"xsi-cog-symbolic"
+ },
+ SensorType.PWM: {
+ "prefix":"pwm",
+ "suffix":"", # no _input suffix for pwm
+ "format":lambda raw: f"{int(raw)*100/255:.0f}",
+ "unit":"%",
+ "icon":"xsi-cog-symbolic"
+ },
+ SensorType.FREQ: {
+ "prefix":"freq",
+ "suffix":"_input",
+ "format":lambda raw: f"{int(raw)/1_000_000_000:.3f}",
+ "unit":"GHz",
+ "icon":"xsi-cog-symbolic"
+ },
+ SensorType.VOLTAGE: {
+ "prefix":"in",
+ "suffix":"_input",
+ "format":lambda raw: f"{int(raw)/1000:.3f}",
+ "unit":"V",
+ "icon":"xsi-cog-symbolic"
+ },
+ SensorType.CURRENT: {
+ "prefix":"curr",
+ "suffix":"_input",
+ "format":lambda raw: f"{int(raw)/1000:.3f}",
+ "unit":"A",
+ "icon":"xsi-cog-symbolic"
+ },
+ SensorType.POWER: {
+ "prefix":"power",
+ "suffix":"_input",
+ "format":lambda raw: f"{int(raw)/1_000_000:.1f}",
+ "unit":"W",
+ "icon":"xsi-cog-symbolic"
+ },
+ SensorType.ENERGY: {
+ "prefix":"energy",
+ "suffix":"_input",
+ "format":lambda raw: f"{int(raw)/1_000_000:.3f}",
+ "unit":"J",
+ "icon":"xsi-cog-symbolic"
+ }
+}
+
+def sensor_spec_from_filename(filename):
+ for stype, spec in SENSOR_SPECS.items():
+ prefix = spec["prefix"]
+ suffix = spec["suffix"]
+ if filename.startswith(prefix) and filename.endswith(suffix):
+ return stype, spec
+ return None, None
+
+# Helper funcs to sort sensors in correct numerical order (ex in10 after in9)
+def natural_key(label):
+ # Split around any digit sequence
+ parts = re.split(r'(\d+)', label)
+ key = []
+ for part in parts:
+ if part.isdigit():
+ key.append(int(part))
+ else:
+ key.append(part.lower())
+ return key
+
+def sort_sensors(sensors):
+ # Natural sort within each sensor type
+ sensors.sort(key=lambda s: natural_key(s["label"]))
+ # Group by sensor type
+ sensors.sort(key=lambda s: s["type"])
+
+class SensorsListWidget(Gtk.ScrolledWindow):
+
+ def __init__(self):
+ super().__init__()
+
+ self.treestore = Gtk.TreeStore(str, str, str, bool, str)
+
+ self.treeview = Gtk.TreeView(model=self.treestore)
+ self.treeview.set_enable_tree_lines(True)
+ self.treeview.set_property("expand", True)
+ self.treeview.set_headers_clickable(True)
+
+ # --- Columns ---
+ # Name column with device icon
+ icon_renderer = Gtk.CellRendererPixbuf()
+ icon_renderer.set_property("xpad", 2)
+ icon_renderer.set_property("ypad", 2)
+ text_renderer = Gtk.CellRendererText()
+ text_renderer.set_property("ypad", 6)
+ column = Gtk.TreeViewColumn(_("Name"))
+ column.pack_start(icon_renderer, False)
+ column.pack_start(text_renderer, True)
+ column.add_attribute(icon_renderer, "icon-name", COL_ICON_NAME)
+ column.add_attribute(text_renderer, "text", COL_NAME)
+ column.add_attribute(text_renderer, "sensitive", COL_SENSITIVE)
+ text_renderer.set_property("ellipsize", Pango.EllipsizeMode.END)
+ column.set_sizing(Gtk.TreeViewColumnSizing.FIXED)
+ self.treeview.append_column(column)
+ column.set_expand(True)
+ column.set_resizable(True)
+
+ # Value column
+ renderer = Gtk.CellRendererText()
+ column = Gtk.TreeViewColumn(_("Value"), renderer, text=COL_VALUE)
+ column.set_expand(False)
+ self.treeview.append_column(column)
+
+ # Unit column
+ renderer = Gtk.CellRendererText()
+ column = Gtk.TreeViewColumn(_("Unit"), renderer, text=COL_UNIT)
+ column.set_expand(False)
+ self.treeview.append_column(column)
+
+ self.add(self.treeview)
+ self.set_shadow_type(Gtk.ShadowType.IN)
+
+ self.sensor_rows = {}
+
+ self.timeout_id = None
+ self.refresh_interval = 1 # seconds
+
+ self.connect("map", self._on_map)
+ self.connect("unmap", self._on_unmap)
+
+ def load(self):
+ # do nothing, we do everything in the _on_map() function
+ pass
+
+ def _on_map(self, *arg):
+ if self.timeout_id is None:
+ # Refresh existing tree or build it is does not exist yet
+ if self.sensor_rows:
+ self.refresh_values()
+ else:
+ self.build_tree()
+
+ self.timeout_id = GLib.timeout_add_seconds(
+ self.refresh_interval, self.refresh_values
+ )
+
+ def _on_unmap(self, *arg):
+ if self.timeout_id is not None:
+ GLib.source_remove(self.timeout_id)
+ self.timeout_id = None
+
+ def build_tree(self):
+ self.treestore.clear()
+ self.sensor_rows.clear()
+
+ if not os.path.isdir(SYS_HWMON):
+ return
+
+ # sort hwmon folders by natural order, as the listdir order is random
+ for hwmon in sorted(os.listdir(SYS_HWMON), key=natural_key):
+ hwmon_path = os.path.join(SYS_HWMON, hwmon)
+ device_path = os.path.join(hwmon_path, "device")
+
+ # Determine base path for sensors
+ base_path = None
+ if os.path.isdir(device_path):
+ # Use device/ only if it contains *_input files
+ # This is required as some modules put sensors files in the device folder (apple-smc for example)
+ inputs = [f for f in os.listdir(device_path) if f.endswith("_input")]
+ if inputs:
+ base_path = device_path
+ if base_path is None:
+ base_path = hwmon_path
+
+ # Root name
+ name_file = os.path.join(base_path, "name")
+ name = self._read_file(name_file)
+ name = name.strip() if name else hwmon
+
+ parent = self.treestore.append(
+ None, [name, "", "", True, "xsi-cpu-symbolic"]
+ )
+
+ device_without_sensors = True
+
+ # Process all *_input files in base_path
+ sensors = []
+ for fname in os.listdir(base_path):
+ stype, spec = sensor_spec_from_filename(fname)
+ if spec is None:
+ continue # that's not a sensor
+
+ fpath = os.path.join(base_path, fname)
+ raw = self._read_file(fpath)
+ if raw is None:
+ continue # unable to read sensor -> skip
+
+ # Label
+ labelname = fname.replace(spec["suffix"], "_label")
+ labelpath = os.path.join(base_path, labelname)
+ label = self._read_file(labelpath)
+ label = label.strip() if label else fname.replace("_input", "")
+
+ sensors.append({
+ "label": label,
+ "path": fpath,
+ "value": spec["format"](raw),
+ "unit": spec["unit"],
+ "icon": spec["icon"],
+ "type": stype,
+ })
+
+ sort_sensors(sensors)
+
+ # Add sorted sensors to treestore
+ for s in sensors:
+ itr = self.treestore.append(
+ parent,
+ [s["label"], s["value"], s["unit"], True, s["icon"]],
+ )
+
+ # Store TreeStore itr and sensor type by path for refresh
+ self.sensor_rows[s["path"]] = (itr, s["type"])
+ device_without_sensors = False
+
+ if device_without_sensors:
+ self.treestore.set_value(parent, COL_SENSITIVE, False)
+
+ self.treeview.expand_all()
+
+ def refresh_values(self):
+ self.treestore.freeze_notify()
+
+ for fpath, (itr, stype) in self.sensor_rows.items():
+ raw = self._read_file(fpath)
+ if raw is None:
+ continue
+
+ spec = SENSOR_SPECS[stype]
+ value = spec["format"](raw)
+
+ if value != self.treestore.get_value(itr, COL_VALUE):
+ self.treestore.set_value(itr, COL_VALUE, value)
+
+ self.treestore.thaw_notify()
+ return True
+
+ def _read_file(self, path):
+ try:
+ with open(path, "r") as f:
+ return f.read()
+ except Exception:
+ return None
diff --git a/usr/share/linuxmint/mintreport/mintreport.ui b/usr/share/linuxmint/mintreport/mintreport.ui
index cfa03e3..0f55f92 100644
--- a/usr/share/linuxmint/mintreport/mintreport.ui
+++ b/usr/share/linuxmint/mintreport/mintreport.ui
@@ -306,6 +306,34 @@
3
+
+
+
+ pense_sensors
+ Sensors
+ xsi-temperature-symbolic
+ 4
+
+
True
@@ -357,7 +385,7 @@
page_bios
BIOS
xsi-cpu-symbolic
- 4
+ 5
@@ -590,7 +618,7 @@
page_reports
System Reports
mintreport-symbolic
- 5
+ 6
@@ -869,7 +897,7 @@
page_crashes
Crash Reports
xsi-computer-fail-symbolic
- 6
+ 7