Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion usr/lib/linuxmint/mintreport/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -68,6 +70,7 @@
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:
Expand Down Expand Up @@ -346,7 +349,7 @@
vendor = clean_brand(read_dmi('sys_vendor'))
sku = read_dmi('product_sku')
product = f"{vendor} {read_dmi('product_name')} {version} {sku}"
product = " ".join(product.split()) # remove unecessary spaces

Check failure on line 352 in usr/lib/linuxmint/mintreport/app.py

View workflow job for this annotation

GitHub Actions / build / build (mint22, linuxmintd/mint22.3-amd64, Mint 22, true) / Mint 22

unecessary ==> unnecessary
infos.append((_("Computer"), product))

try:
Expand Down Expand Up @@ -405,7 +408,6 @@
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)
Expand All @@ -414,6 +416,10 @@
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)
Expand All @@ -427,6 +433,7 @@
self.load_pci()
self.load_bios()
self.load_gpu()
self.load_sensors()

def show_page(self, page_name):
page_name = f"page_{page_name}"
Expand Down Expand Up @@ -585,6 +592,10 @@
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()
Expand Down Expand Up @@ -773,7 +784,7 @@

# Archive the crash report - exclude the CoreDump as it can be very big (close to 1GB)
os.chdir(TMP_DIR)
subprocess.call(["tar", "caf", CRASH_ARCHIVE, "crash", "--exclude", "CoreDump"])

Check failure on line 787 in usr/lib/linuxmint/mintreport/app.py

View workflow job for this annotation

GitHub Actions / build / build (mint22, linuxmintd/mint22.3-amd64, Mint 22, true) / Mint 22

caf ==> calf

self.on_unpack_crash_report_finished()

Expand Down
269 changes: 269 additions & 0 deletions usr/lib/linuxmint/mintreport/sensors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
import os
import gi
import xapp.util
import xapp.SettingsWidgets as Xs
import re
from enum import IntEnum

gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib

_ = xapp.util.l10n("mintreport")

SYS_HWMON = "/sys/class/hwmon"

class SensorType(IntEnum):
TEMP = 0
FAN = 1
PWM = 2
FREQ = 3
POWER = 4
VOLTAGE = 5
CURRENT = 6
ENERGY = 7


SENSOR_SPECS = {
SensorType.TEMP: {
"pattern":re.compile(r"^(temp\d+)_input$"),
"format":lambda raw: f"{int(raw)/1000:.1f}",
"unit":"°C",
"icon":"xsi-temperature-symbolic"
},
SensorType.FAN: {
"pattern":re.compile(r"^(fan\d+)_input$"),
"format":lambda raw: raw.strip(),
"unit":_("RPM"),
"icon":"xsi-fan-symbolic"
},
SensorType.PWM: {
"pattern":re.compile(r"^(pwm\d+)$"), # no _input suffix for pwm type
"format":lambda raw: f"{int(raw)*100/255:.0f}",
"unit":"%",
"icon":"xsi-fan-symbolic"
},
SensorType.FREQ: {
"pattern":re.compile(r"^(freq\d+)_input$"),
"format":lambda raw: f"{int(raw)/1_000_000_000:.2f}",
"unit":"GHz",
"icon":"xsi-physics-wavelength-symbolic"
},
SensorType.VOLTAGE: {
"pattern":re.compile(r"^(in\d+)_input$"),
"format":lambda raw: f"{int(raw)/1000:.2f}",
"unit":"V",
"icon":"xsi-physics-volts-symbolic"
},
SensorType.CURRENT: {
"pattern":re.compile(r"^(curr\d+)_input$"),
"format":lambda raw: f"{int(raw)/1000:.2f}",
"unit":"A",
"icon":"xsi-physics-amps-symbolic"
},
SensorType.POWER: {
"pattern":re.compile(r"^(power\d+)_input$"),
"format":lambda raw: f"{int(raw)/1_000_000:.1f}",
"unit":"W",
"icon":"xsi-physics-watts-symbolic"
},
SensorType.ENERGY: {
"pattern":re.compile(r"^(energy\d+)_input$"),
"format":lambda raw: f"{int(raw)/1_000_000:.1f}",
"unit":"J",
"icon":"xsi-power-symbolic"
}
}

def sensor_spec_from_filename(filename):
for stype, spec in SENSOR_SPECS.items():
m = spec["pattern"].match(filename)
if m:
base = m.group(1)
return stype, spec, base
return None, 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.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)

self.page = Xs.SettingsPage()
self.page.set_spacing(24)
self.page.set_margin_left(24)
self.page.set_margin_right(24)
self.page.set_margin_top(12)
self.page.set_margin_bottom(12)
self.add(self.page)

self.sensor_rows = {} # fpath -> (value_label, stype)
self.value_size_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)
self.unit_size_group = Gtk.SizeGroup(Gtk.SizeGroupMode.HORIZONTAL)

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, *_):
if self.timeout_id is None:
# Refresh existing cards or build them if they don't exist yet
if self.sensor_rows:
self.refresh_values()
else:
self.build_cards()

self.timeout_id = GLib.timeout_add_seconds(
self.refresh_interval, self.refresh_values
)

def _on_unmap(self, *_):
if self.timeout_id is not None:
GLib.source_remove(self.timeout_id)
self.timeout_id = None

def build_cards(self):
for child in self.page.get_children():
self.page.remove(child)
self.sensor_rows.clear()

if not os.path.isdir(SYS_HWMON):
return

# First pass: collect all chips with their sensors
chips = []
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

# Process all sensor files in base_path
sensors = []
for fname in os.listdir(base_path):
stype, spec, base_name = 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
labelpath = os.path.join(base_path, f"{base_name}_label")
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,
})

if not sensors:
continue

sort_sensors(sensors)
chips.append((hwmon, name, sensors))

# Disambiguate chips that share the same name (e.g. two RAM sticks: spd5118)
name_counts = {}
for _, name, _ in chips:
name_counts[name] = name_counts.get(name, 0) + 1

# Second pass: build cards
for hwmon, name, sensors in chips:
if name_counts[name] > 1:
name = f"{name} ({hwmon})"

section = self.page.add_section(name)

for s in sensors:
row = Xs.SettingsWidget()
row.set_spacing(8)

icon = Gtk.Image.new_from_icon_name(s["icon"], Gtk.IconSize.MENU)
icon.set_pixel_size(16)
row.pack_start(icon, False, False, 0)

name_label = Gtk.Label(label=s["label"])
name_label.set_xalign(0)
row.pack_start(name_label, True, True, 0)

value_label = Gtk.Label(label=s["value"])
value_label.get_style_context().add_class("dim-label")
value_label.set_xalign(1.0)
unit_label = Gtk.Label(label=s["unit"])
unit_label.get_style_context().add_class("dim-label")
unit_label.set_xalign(0.0)
self.value_size_group.add_widget(value_label)
self.unit_size_group.add_widget(unit_label)
row.pack_end(unit_label, False, False, 0)
row.pack_end(value_label, False, False, 0)

section.add_row(row)
self.sensor_rows[s["path"]] = (value_label, s["type"])

self.page.show_all()

def refresh_values(self):
for fpath, (value_label, 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 != value_label.get_text():
value_label.set_text(value)

return True

def _read_file(self, path):
try:
with open(path, "r") as f:
return f.read()
except Exception:
return None
34 changes: 31 additions & 3 deletions usr/share/linuxmint/mintreport/mintreport.ui
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,34 @@
<property name="position">3</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box_sensors">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="box_sensors_widget">
<property name="visible">True</property>
<property name="can-focus">False</property>
<property name="orientation">vertical</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">True</property>
<property name="fill">True</property>
<property name="position">0</property>
</packing>
</child>
</object>
<packing>
<property name="name">pense_sensors</property>
<property name="title" translatable="yes">Sensors</property>
<property name="icon-name">xsi-temperature-symbolic</property>
<property name="position">4</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box_bios">
<property name="visible">True</property>
Expand Down Expand Up @@ -357,7 +385,7 @@
<property name="name">page_bios</property>
<property name="title" translatable="yes">BIOS</property>
<property name="icon-name">xsi-cpu-symbolic</property>
<property name="position">4</property>
<property name="position">5</property>
</packing>
</child>
<child>
Expand Down Expand Up @@ -590,7 +618,7 @@
<property name="name">page_reports</property>
<property name="title" translatable="yes">System Reports</property>
<property name="icon-name">mintreport-symbolic</property>
<property name="position">5</property>
<property name="position">6</property>
</packing>
</child>
<child>
Expand Down Expand Up @@ -869,7 +897,7 @@
<property name="name">page_crashes</property>
<property name="title" translatable="yes">Crash Reports</property>
<property name="icon-name">xsi-computer-fail-symbolic</property>
<property name="position">6</property>
<property name="position">7</property>
</packing>
</child>
</object>
Expand Down
Loading