From 9892e00754b184707fa6be2e88f8466735d88d29 Mon Sep 17 00:00:00 2001 From: Pila Date: Tue, 13 Jan 2026 22:05:19 +0100 Subject: [PATCH 01/10] Sensors pages, first draft --- usr/lib/linuxmint/mintreport/app.py | 13 +- usr/lib/linuxmint/mintreport/sensors.py | 176 +++++++++++++++++++ usr/share/linuxmint/mintreport/mintreport.ui | 34 +++- 3 files changed, 219 insertions(+), 4 deletions(-) create mode 100644 usr/lib/linuxmint/mintreport/sensors.py 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..468f6cf --- /dev/null +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -0,0 +1,176 @@ +import os +import gi +import xapp.util + +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, +) = range(5) + + +class SensorsListWidget(Gtk.ScrolledWindow): + ICONS = { + "root": "xsi-cpu-symbolic", + "temp": "xsi-temperature-symbolic", + "fan": "xsi-cpu-symbolic", + "in": "xsi-cpu-symbolic", + "power": "xsi-cpu-symbolic", + "other": "xsi-cpu-symbolic", + } + + def __init__(self): + super().__init__() + + # TreeStore with icon, name, value, unit + 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_headers_clickable(True) + + # First column: tree + icon + name + renderer = Gtk.CellRendererPixbuf() + column = Gtk.TreeViewColumn("", renderer, icon_name=COL_ICON) + column.set_expand(False) + self.treeview.append_column(column) + + renderer = Gtk.CellRendererText() + renderer.set_property("ellipsize", Pango.EllipsizeMode.END) + column = Gtk.TreeViewColumn(_("Sensor"), renderer, text=COL_NAME) + column.set_expand(True) + self.treeview.append_column(column) + + # 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.hwmon_parents = {} + self.sensor_rows = {} + + # ------------------------------------------------------------------ + def load(self): + self.build_tree() + self.refresh_values() + GLib.timeout_add_seconds(1, self.refresh_values) + + # ------------------------------------------------------------------ + def build_tree(self): + self.treestore.clear() + self.hwmon_parents.clear() + self.sensor_rows.clear() + + if not os.path.isdir(SYS_HWMON): + return + + for hwmon in sorted(os.listdir(SYS_HWMON)): + 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 + 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, "", "", False, self.ICONS["root"]] + ) + self.hwmon_parents[hwmon_path] = parent + + # Process all *_input files in base_path + for fname in sorted(os.listdir(base_path)): + if not fname.endswith("_input"): + continue + + fpath = os.path.join(base_path, fname) + raw = self._read_file(fpath) + if raw is None: + continue + + # Label + label_file = fpath.replace("_input", "_label") + label = self._read_file(label_file) + label = label.strip() if label else fname.replace("_input", "") + + unit, display, stype = self.format_sensor(fname, raw) + + itr = self.treestore.append( + parent, + [label, display, unit, True, self.ICONS.get(stype, self.ICONS["other"])], + ) + self.sensor_rows[fpath] = itr + + self.treeview.expand_all() + + # ------------------------------------------------------------------ + def refresh_values(self): + for fpath, itr in self.sensor_rows.items(): + raw = self._read_file(fpath) + if raw is None: + continue + + fname = os.path.basename(fpath) + _, display, _ = self.format_sensor(fname, raw) + self.treestore.set_value(itr, COL_VALUE, display) + + return True + + # ------------------------------------------------------------------ + def format_sensor(self, filename, raw): + raw = raw.strip() + stype = "other" + + if filename.startswith("temp"): + stype = "temp" + return "°C", f"{int(raw)/1000:.1f}", stype + + if filename.startswith("fan"): + stype = "fan" + return "RPM", raw, stype + + if filename.startswith("in"): + stype = "in" + return "V", f"{int(raw)/1000:.3f}", stype + + if filename.startswith("power"): + stype = "power" + return "W", f"{int(raw)/1_000_000:.2f}", stype + + return "", raw, stype + + # ------------------------------------------------------------------ + 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 + + + True + False + vertical + + + True + False + vertical + + + + + + True + True + 0 + + + + + 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 From eab4a2a582f9d4319cc3f495d8adbc42e77a92cf Mon Sep 17 00:00:00 2001 From: Pila Date: Wed, 14 Jan 2026 22:12:24 +0100 Subject: [PATCH 02/10] Code cleanup, add other sensor types --- usr/lib/linuxmint/mintreport/sensors.py | 118 ++++++++++++------------ 1 file changed, 57 insertions(+), 61 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 468f6cf..00824ae 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -9,45 +9,64 @@ SYS_HWMON = "/sys/class/hwmon" -( - COL_NAME, - COL_VALUE, - COL_UNIT, - COL_SENSITIVE, - COL_ICON, -) = range(5) +COL_NAME, COL_VALUE, COL_UNIT, COL_SENSITIVE, COL_ICON_NAME = range(5) +def format_sensor(filename, raw): + raw = raw.strip() + + if filename.startswith("temp"): + return f"{int(raw)/1000:.1f}", _("°C"), "xsi-temperature-symbolic" + + if filename.startswith("fan"): + return raw, _("RPM"), "xsi-cpu-symbolic" + + if filename.startswith("pwm"): + return f"{int(raw)*100/255:.0f}", _("%"), "xsi-cpu-symbolic" + + if filename.startswith("in"): + return f"{int(raw)/1000:.3f}", _("V"), "xsi-cpu-symbolic" + + if filename.startswith("curr"): + return f"{int(raw)/1000:.3f}", _("A"), "xsi-cpu-symbolic" + + if filename.startswith("power"): + return f"{int(raw)/1_000_000:.2f}", _("W"), "xsi-cpu-symbolic" + + if filename.startswith("energy"): + return f"{int(raw)/1_000_000:.2f}", _("J"), "xsi-cpu-symbolic" + + return raw, "", "xsi-cpu-symbolic" class SensorsListWidget(Gtk.ScrolledWindow): - ICONS = { - "root": "xsi-cpu-symbolic", - "temp": "xsi-temperature-symbolic", - "fan": "xsi-cpu-symbolic", - "in": "xsi-cpu-symbolic", - "power": "xsi-cpu-symbolic", - "other": "xsi-cpu-symbolic", - } def __init__(self): super().__init__() - # TreeStore with icon, name, value, unit 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) - # First column: tree + icon + name - renderer = Gtk.CellRendererPixbuf() - column = Gtk.TreeViewColumn("", renderer, icon_name=COL_ICON) - column.set_expand(False) + # --- 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) - - renderer = Gtk.CellRendererText() - renderer.set_property("ellipsize", Pango.EllipsizeMode.END) - column = Gtk.TreeViewColumn(_("Sensor"), renderer, text=COL_NAME) column.set_expand(True) - self.treeview.append_column(column) + column.set_resizable(True) # Value column renderer = Gtk.CellRendererText() @@ -64,19 +83,15 @@ def __init__(self): self.add(self.treeview) self.set_shadow_type(Gtk.ShadowType.IN) - self.hwmon_parents = {} self.sensor_rows = {} - # ------------------------------------------------------------------ def load(self): self.build_tree() self.refresh_values() GLib.timeout_add_seconds(1, self.refresh_values) - # ------------------------------------------------------------------ def build_tree(self): self.treestore.clear() - self.hwmon_parents.clear() self.sensor_rows.clear() if not os.path.isdir(SYS_HWMON): @@ -102,9 +117,10 @@ def build_tree(self): name = name.strip() if name else hwmon parent = self.treestore.append( - None, [name, "", "", False, self.ICONS["root"]] + None, [name, "", "", True, "xsi-cpu-symbolic"] ) - self.hwmon_parents[hwmon_path] = parent + + device_without_sensors = True; # Process all *_input files in base_path for fname in sorted(os.listdir(base_path)): @@ -116,22 +132,26 @@ def build_tree(self): if raw is None: continue + value, unit, icon_name = format_sensor(fname, raw) + # Label label_file = fpath.replace("_input", "_label") label = self._read_file(label_file) label = label.strip() if label else fname.replace("_input", "") - unit, display, stype = self.format_sensor(fname, raw) - itr = self.treestore.append( parent, - [label, display, unit, True, self.ICONS.get(stype, self.ICONS["other"])], + [label, value, unit, True, icon_name], ) self.sensor_rows[fpath] = itr + device_without_sensors = False; + + if (device_without_sensors): + self.treestore.set_value(parent, COL_SENSITIVE, False) + self.treeview.expand_all() - # ------------------------------------------------------------------ def refresh_values(self): for fpath, itr in self.sensor_rows.items(): raw = self._read_file(fpath) @@ -139,35 +159,11 @@ def refresh_values(self): continue fname = os.path.basename(fpath) - _, display, _ = self.format_sensor(fname, raw) - self.treestore.set_value(itr, COL_VALUE, display) + value, _, _ = format_sensor(fname, raw) + self.treestore.set_value(itr, COL_VALUE, value) return True - # ------------------------------------------------------------------ - def format_sensor(self, filename, raw): - raw = raw.strip() - stype = "other" - - if filename.startswith("temp"): - stype = "temp" - return "°C", f"{int(raw)/1000:.1f}", stype - - if filename.startswith("fan"): - stype = "fan" - return "RPM", raw, stype - - if filename.startswith("in"): - stype = "in" - return "V", f"{int(raw)/1000:.3f}", stype - - if filename.startswith("power"): - stype = "power" - return "W", f"{int(raw)/1_000_000:.2f}", stype - - return "", raw, stype - - # ------------------------------------------------------------------ def _read_file(self, path): try: with open(path, "r") as f: From 31ee8363caa0e09e5e6c7eea32a4a75b6b03a689 Mon Sep 17 00:00:00 2001 From: Pilatomic Date: Sat, 17 Jan 2026 22:25:27 +0100 Subject: [PATCH 03/10] Add freq value, fine tune decimal display --- usr/lib/linuxmint/mintreport/sensors.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 00824ae..b0ceab0 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -30,10 +30,13 @@ def format_sensor(filename, raw): return f"{int(raw)/1000:.3f}", _("A"), "xsi-cpu-symbolic" if filename.startswith("power"): - return f"{int(raw)/1_000_000:.2f}", _("W"), "xsi-cpu-symbolic" + return f"{int(raw)/1_000_000:.1f}", _("W"), "xsi-cpu-symbolic" + + if filename.startswith("freq"): + return f"{int(raw)/1_000_000_000:.3f}", _("GHz"), "xsi-cpu-symbolic" if filename.startswith("energy"): - return f"{int(raw)/1_000_000:.2f}", _("J"), "xsi-cpu-symbolic" + return f"{int(raw)/1_000_000:.3f}", _("J"), "xsi-cpu-symbolic" return raw, "", "xsi-cpu-symbolic" From 84a872f630815cf77d99758cfc34fef5bd1a4f67 Mon Sep 17 00:00:00 2001 From: Pilatomic Date: Sat, 17 Jan 2026 22:38:43 +0100 Subject: [PATCH 04/10] Use cog icon as placeholder for measures other than temperature --- usr/lib/linuxmint/mintreport/sensors.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index b0ceab0..1743125 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -18,25 +18,25 @@ def format_sensor(filename, raw): return f"{int(raw)/1000:.1f}", _("°C"), "xsi-temperature-symbolic" if filename.startswith("fan"): - return raw, _("RPM"), "xsi-cpu-symbolic" + return raw, _("RPM"), "xsi-cog-symbolic" if filename.startswith("pwm"): - return f"{int(raw)*100/255:.0f}", _("%"), "xsi-cpu-symbolic" + return f"{int(raw)*100/255:.0f}", _("%"), "xsi-cog-symbolic" if filename.startswith("in"): - return f"{int(raw)/1000:.3f}", _("V"), "xsi-cpu-symbolic" + return f"{int(raw)/1000:.3f}", _("V"), "xsi-cog-symbolic" if filename.startswith("curr"): - return f"{int(raw)/1000:.3f}", _("A"), "xsi-cpu-symbolic" + return f"{int(raw)/1000:.3f}", _("A"), "xsi-cog-symbolic" if filename.startswith("power"): - return f"{int(raw)/1_000_000:.1f}", _("W"), "xsi-cpu-symbolic" + return f"{int(raw)/1_000_000:.1f}", _("W"), "xsi-cog-symbolic" if filename.startswith("freq"): - return f"{int(raw)/1_000_000_000:.3f}", _("GHz"), "xsi-cpu-symbolic" + return f"{int(raw)/1_000_000_000:.3f}", _("GHz"), "xsi-cog-symbolic" if filename.startswith("energy"): - return f"{int(raw)/1_000_000:.3f}", _("J"), "xsi-cpu-symbolic" + return f"{int(raw)/1_000_000:.3f}", _("J"), "xsi-cog-symbolic" return raw, "", "xsi-cpu-symbolic" @@ -108,6 +108,7 @@ def build_tree(self): 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 From 0572c47260308ba8d1146078c6380df5a3ec2eb5 Mon Sep 17 00:00:00 2001 From: Pilatomic Date: Sat, 17 Jan 2026 22:41:18 +0100 Subject: [PATCH 05/10] Remove translations for non-translatable units --- usr/lib/linuxmint/mintreport/sensors.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 1743125..cf87517 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -15,28 +15,28 @@ def format_sensor(filename, raw): raw = raw.strip() if filename.startswith("temp"): - return f"{int(raw)/1000:.1f}", _("°C"), "xsi-temperature-symbolic" + return f"{int(raw)/1000:.1f}", "°C", "xsi-temperature-symbolic" if filename.startswith("fan"): return raw, _("RPM"), "xsi-cog-symbolic" if filename.startswith("pwm"): - return f"{int(raw)*100/255:.0f}", _("%"), "xsi-cog-symbolic" + return f"{int(raw)*100/255:.0f}", "%", "xsi-cog-symbolic" if filename.startswith("in"): - return f"{int(raw)/1000:.3f}", _("V"), "xsi-cog-symbolic" + return f"{int(raw)/1000:.3f}", "V", "xsi-cog-symbolic" if filename.startswith("curr"): - return f"{int(raw)/1000:.3f}", _("A"), "xsi-cog-symbolic" + return f"{int(raw)/1000:.3f}", "A", "xsi-cog-symbolic" if filename.startswith("power"): - return f"{int(raw)/1_000_000:.1f}", _("W"), "xsi-cog-symbolic" + return f"{int(raw)/1_000_000:.1f}", "W", "xsi-cog-symbolic" if filename.startswith("freq"): - return f"{int(raw)/1_000_000_000:.3f}", _("GHz"), "xsi-cog-symbolic" + return f"{int(raw)/1_000_000_000:.3f}", "GHz", "xsi-cog-symbolic" if filename.startswith("energy"): - return f"{int(raw)/1_000_000:.3f}", _("J"), "xsi-cog-symbolic" + return f"{int(raw)/1_000_000:.3f}", "J", "xsi-cog-symbolic" return raw, "", "xsi-cpu-symbolic" From 7a3825742ded6398ec2f592eebacde70a28783d9 Mon Sep 17 00:00:00 2001 From: Pila Date: Sun, 18 Jan 2026 15:57:27 +0100 Subject: [PATCH 06/10] Reduce CPU usage, only update row if value changed, and use freeze_notify / thaw_notify --- usr/lib/linuxmint/mintreport/sensors.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index cf87517..eea3f70 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -151,12 +151,13 @@ def build_tree(self): device_without_sensors = False; - if (device_without_sensors): + 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 in self.sensor_rows.items(): raw = self._read_file(fpath) if raw is None: @@ -164,8 +165,9 @@ def refresh_values(self): fname = os.path.basename(fpath) value, _, _ = format_sensor(fname, raw) - self.treestore.set_value(itr, COL_VALUE, value) - + 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): From 37f2334e1497036577673fe1a9c0b97df020cfe5 Mon Sep 17 00:00:00 2001 From: Pila Date: Sun, 18 Jan 2026 21:38:21 +0100 Subject: [PATCH 07/10] Stop refresh when not visible --- usr/lib/linuxmint/mintreport/sensors.py | 28 ++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index eea3f70..c6d2d6f 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -88,10 +88,32 @@ def __init__(self): 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): - self.build_tree() - self.refresh_values() - GLib.timeout_add_seconds(1, self.refresh_values) + # 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() From a38614bc582de827e31b5c86fdfb752949ae568a Mon Sep 17 00:00:00 2001 From: Pila Date: Mon, 19 Jan 2026 22:29:52 +0100 Subject: [PATCH 08/10] Move all sensors specifications to a dictionnary. Sort bny natural order (for ease to use & it fixes core order in coretemp) --- usr/lib/linuxmint/mintreport/sensors.py | 162 ++++++++++++++++++------ 1 file changed, 124 insertions(+), 38 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index c6d2d6f..15ff076 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -1,6 +1,8 @@ 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 @@ -11,34 +13,99 @@ COL_NAME, COL_VALUE, COL_UNIT, COL_SENSITIVE, COL_ICON_NAME = range(5) -def format_sensor(filename, raw): - raw = raw.strip() - - if filename.startswith("temp"): - return f"{int(raw)/1000:.1f}", "°C", "xsi-temperature-symbolic" - - if filename.startswith("fan"): - return raw, _("RPM"), "xsi-cog-symbolic" - - if filename.startswith("pwm"): - return f"{int(raw)*100/255:.0f}", "%", "xsi-cog-symbolic" - - if filename.startswith("in"): - return f"{int(raw)/1000:.3f}", "V", "xsi-cog-symbolic" - - if filename.startswith("curr"): - return f"{int(raw)/1000:.3f}", "A", "xsi-cog-symbolic" - - if filename.startswith("power"): - return f"{int(raw)/1_000_000:.1f}", "W", "xsi-cog-symbolic" - - if filename.startswith("freq"): - return f"{int(raw)/1_000_000_000:.3f}", "GHz", "xsi-cog-symbolic" - - if filename.startswith("energy"): - return f"{int(raw)/1_000_000:.3f}", "J", "xsi-cog-symbolic" - - return raw, "", "xsi-cpu-symbolic" +class SensorType(IntEnum): + TEMP = 0 + FAN = 1 + PWM = 2 + FREQ = 3 + VOLTAGE = 4 + CURRENT = 5 + POWER = 6 + ENERGY = 7 + OTHER = 99 + + +SENSOR_SPECS = { + SensorType.TEMP: { + "prefix":"temp", + "format":lambda raw: f"{int(raw)/1000:.1f}", + "unit":"°C", + "icon":"xsi-temperature-symbolic" + }, + SensorType.FAN: { + "prefix":"fan", + "format":lambda raw: raw.strip(), + "unit":_("RPM"), + "icon":"xsi-cog-symbolic" + }, + SensorType.PWM: { + "prefix":"pwm", + "format":lambda raw: f"{int(raw)*100/255:.0f}", + "unit":"%", + "icon":"xsi-cog-symbolic" + }, + SensorType.FREQ: { + "prefix":"freq", + "format":lambda raw: f"{int(raw)/1_000_000_000:.3f}", + "unit":"GHz", + "icon":"xsi-cog-symbolic" + }, + SensorType.VOLTAGE: { + "prefix":"in", + "format":lambda raw: f"{int(raw)/1000:.3f}", + "unit":"V", + "icon":"xsi-cog-symbolic" + }, + SensorType.CURRENT: { + "prefix":"curr", + "format":lambda raw: f"{int(raw)/1000:.3f}", + "unit":"A", + "icon":"xsi-cog-symbolic" + }, + SensorType.POWER: { + "prefix":"power", + "format":lambda raw: f"{int(raw)/1_000_000:.1f}", + "unit":"W", + "icon":"xsi-cog-symbolic" + }, + SensorType.ENERGY: { + "prefix":"energy", + "format":lambda raw: f"{int(raw)/1_000_000:.3f}", + "unit":"J", + "icon":"xsi-cog-symbolic" + }, + SensorType.OTHER: { + "prefix":"", + "format":lambda raw: raw.strip(), + "unit":"", + "icon":"xsi-cog-symbolic" + }, +} + +def sensor_spec_from_filename(filename): + for stype, spec in SENSOR_SPECS.items(): + prefix = spec["prefix"] + if prefix and filename.startswith(prefix): + return stype, spec + return SensorType.OTHER, SENSOR_SPECS[SensorType.OTHER] + +# 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): @@ -122,7 +189,7 @@ def build_tree(self): if not os.path.isdir(SYS_HWMON): return - for hwmon in sorted(os.listdir(SYS_HWMON)): + for hwmon in os.listdir(SYS_HWMON): hwmon_path = os.path.join(SYS_HWMON, hwmon) device_path = os.path.join(hwmon_path, "device") @@ -146,10 +213,11 @@ def build_tree(self): None, [name, "", "", True, "xsi-cpu-symbolic"] ) - device_without_sensors = True; + device_without_sensors = True # Process all *_input files in base_path - for fname in sorted(os.listdir(base_path)): + sensors = [] + for fname in os.listdir(base_path): if not fname.endswith("_input"): continue @@ -158,20 +226,35 @@ def build_tree(self): if raw is None: continue - value, unit, icon_name = format_sensor(fname, raw) + stype, spec = sensor_spec_from_filename(fname) + value = spec["format"](raw) # Label label_file = fpath.replace("_input", "_label") label = self._read_file(label_file) label = label.strip() if label else fname.replace("_input", "") + sensors.append({ + "label": label, + "path": fpath, + "value": value, + "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, - [label, value, unit, True, icon_name], + [s["label"], s["value"], s["unit"], True, s["icon"]], ) - self.sensor_rows[fpath] = itr - device_without_sensors = False; + # 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) @@ -180,15 +263,18 @@ def build_tree(self): def refresh_values(self): self.treestore.freeze_notify() - for fpath, itr in self.sensor_rows.items(): + + for fpath, (itr, stype) in self.sensor_rows.items(): raw = self._read_file(fpath) if raw is None: continue - fname = os.path.basename(fpath) - value, _, _ = format_sensor(fname, raw) + 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 From f669d6f81248ac1892702a184ded63ae1fc12bf8 Mon Sep 17 00:00:00 2001 From: Pila Date: Tue, 20 Jan 2026 17:46:19 +0100 Subject: [PATCH 09/10] Also sort devices --- usr/lib/linuxmint/mintreport/sensors.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 15ff076..0e7079c 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -189,7 +189,8 @@ def build_tree(self): if not os.path.isdir(SYS_HWMON): return - for hwmon in os.listdir(SYS_HWMON): + # 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") From 6fd1bb973105cd6a10adaf4a837ffbd01e22f070 Mon Sep 17 00:00:00 2001 From: Pila Date: Wed, 21 Jan 2026 19:37:53 +0100 Subject: [PATCH 10/10] Implement suffix to properly match pwm file name. Remove SensorType.OTHER, we implement all useful sensor types. Group POWER sensors close to FREQ --- usr/lib/linuxmint/mintreport/sensors.py | 45 +++++++++++++------------ 1 file changed, 23 insertions(+), 22 deletions(-) diff --git a/usr/lib/linuxmint/mintreport/sensors.py b/usr/lib/linuxmint/mintreport/sensors.py index 0e7079c..0aa6671 100644 --- a/usr/lib/linuxmint/mintreport/sensors.py +++ b/usr/lib/linuxmint/mintreport/sensors.py @@ -18,76 +18,78 @@ class SensorType(IntEnum): FAN = 1 PWM = 2 FREQ = 3 - VOLTAGE = 4 - CURRENT = 5 - POWER = 6 + POWER = 4 + VOLTAGE = 5 + CURRENT = 6 ENERGY = 7 - OTHER = 99 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" - }, - SensorType.OTHER: { - "prefix":"", - "format":lambda raw: raw.strip(), - "unit":"", - "icon":"xsi-cog-symbolic" - }, + } } def sensor_spec_from_filename(filename): for stype, spec in SENSOR_SPECS.items(): prefix = spec["prefix"] - if prefix and filename.startswith(prefix): + suffix = spec["suffix"] + if filename.startswith(prefix) and filename.endswith(suffix): return stype, spec - return SensorType.OTHER, SENSOR_SPECS[SensorType.OTHER] + return None, None # Helper funcs to sort sensors in correct numerical order (ex in10 after in9) def natural_key(label): @@ -219,26 +221,25 @@ def build_tree(self): # Process all *_input files in base_path sensors = [] for fname in os.listdir(base_path): - if not fname.endswith("_input"): - continue + 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 - - stype, spec = sensor_spec_from_filename(fname) - value = spec["format"](raw) + continue # unable to read sensor -> skip # Label - label_file = fpath.replace("_input", "_label") - label = self._read_file(label_file) + 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": value, + "value": spec["format"](raw), "unit": spec["unit"], "icon": spec["icon"], "type": stype,