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 + + + 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