hi there,
If you happen to be like me, have a second screen that sits idle most of the time, you might want to make it more useful by showing something informative—like system stats. A while ago, I spent a little time starting a project to monitor my computer’s hardware in real-time and display it on my second screen. The goal was to have a simple dashboard showing important metrics like CPU temperature, load, RAM usage, and more.
The Tech Behind It
The core of this project is based on LibreHardwareMonitor, an open-source library that lets you monitor your system’s hardware on Windows. It’s built on .NET, which makes it a perfect fit for Windows-based systems. You can find the full project on GitHub here: LibreHardwareMonitor GitHub.
I wrote two pieces of Python code to bring this to life:
- Dash Dashboard: The first script creates a dashboard using Dash and Dash Mantine Components (a beautiful UI library). This displays real-time system metrics like CPU temperature, load, and RAM usage in a graphical format, with gauges and charts updated every second.
import os
from dash import Dash, html, dcc, Input, Output, State
import dash_mantine_components as dmc
import plotly.io as pio
from hardware_monitor import HardwareMonitor
base_dir = os.path.dirname(os.path.abspath(__file__))
dll_file = os.path.join(base_dir, "LibreHardwareMonitorLib.dll")
monitor = HardwareMonitor(dll_file)
dmc.add_figure_templates(default="mantine_dark")
app = Dash(__name__)
app.layout = dmc.MantineProvider(
forceColorScheme="dark",
children=dmc.Container(
fluid=True,
h=100,
children=[
dmc.Title("Hardware Monitor Dashboard", order=1, mb="md"),
dmc.SimpleGrid(
cols=2,
spacing="lg",
children=[
dmc.SimpleGrid(
cols=1,
# spacing="md",
children=[
dmc.Card(
# padding="lg",
children=[
dmc.Text("CPU Temperature (°C):", fw=700),
dmc.SemiCircleProgress(
id="cpu-temp-gauge",
value=0,
# size=120,
# thickness=12,
),
],
),
dmc.Card(
# padding="lg",
children=[
dmc.Text("CPU Load (%):", fw=700),
dmc.SemiCircleProgress(
id="cpu-load-gauge",
value=0,
# size=120,
thickness=12,
),
],
),
dmc.Card(
# padding="lg",
children=[
dmc.Text("RAM Usage (%):", fw=700),
dmc.SemiCircleProgress(
id="ram-gauge",
value=0,
# size=120,
thickness=12,
),
],
),
],
),
dmc.SimpleGrid(
cols=1,
spacing="md",
mt="xl",
children=[
dmc.Card(
padding="md",
children=[
dmc.Text("CPU Temperature Over Time", fw=700),
dcc.Graph(
id="cpu-temp-graph",
config={"displayModeBar": False},
figure={
"data": [
{
"x": [],
"y": [],
"type": "line",
"name": "CPU Temp",
}
],
"layout": {
"margin": {"t": 20, "b": 40},
"yaxis": {"title": "°C"},
"template": pio.templates[
"mantine_dark"
],
},
},
),
],
),
dmc.Card(
padding="md",
children=[
dmc.Text("CPU Load Over Time", fw=700),
dcc.Graph(
id="cpu-load-graph",
config={"displayModeBar": False},
figure={
"data": [
{
"x": [],
"y": [],
"type": "line",
"name": "CPU Load",
}
],
"layout": {
"margin": {"t": 20, "b": 40},
"yaxis": {"title": "%"},
"template": pio.templates[
"mantine_dark"
],
},
},
),
],
),
dmc.Card(
padding="md",
children=[
dmc.Text("RAM Usage Over Time", fw=700),
dcc.Graph(
id="ram-graph",
config={"displayModeBar": False},
figure={
"data": [
{
"x": [],
"y": [],
"type": "line",
"name": "RAM Usage",
}
],
"layout": {
"margin": {"t": 20, "b": 40},
"yaxis": {"title": "%"},
"template": pio.templates[
"mantine_dark"
],
},
},
),
],
),
],
),
],
),
dcc.Interval(id="interval", interval=1000, n_intervals=0),
],
),
)
@app.callback(
[
Output("cpu-temp-gauge", "value"),
Output("cpu-load-gauge", "value"),
Output("ram-gauge", "value"),
Output("cpu-temp-graph", "extendData"),
Output("cpu-load-graph", "extendData"),
Output("ram-graph", "extendData"),
],
Input("interval", "n_intervals"),
)
def update_main_gauges(n):
data = monitor.get_monitor_data()
cpu_temp = data.get("cpu_temp", 0) or 0
cpu_load = data.get("cpu_load", 0) or 0
ram_used = data.get("ram_used", 0) or 0
ram_total = data.get("ram_total", 1) or 1
ram_pct = (ram_used / ram_total) * 100
return (
cpu_temp,
cpu_load,
ram_pct,
({"y": [[cpu_temp]], "x": [[n]]}, [0], 60),
({"y": [[cpu_load]], "x": [[n]]}, [0], 60),
({"y": [[ram_pct]], "x": [[n]]}, [0], 60),
)
if __name__ == "__main__":
app.run(debug=True)
- Hardware Monitor Class: The second script is responsible for pulling data from LibreHardwareMonitorLib.dll. It collects information like CPU temperature, CPU load, GPU stats, RAM usage, and more. I created a class that interacts with this DLL, processes the data, and makes it available for the Dash dashboard.
import os
import time
import sys
import clr
from typing import Optional, Dict, Any, List
clr.AddReference("System")
from System import Decimal
def log(msg: str) -> None:
"""Print message with a timestamp."""
print(f"[{time.strftime('%Y-%m-%d %H:%M:%S')}] {msg}")
def decimal_to_float(val: Any) -> Optional[float]:
"""Convert a System.Decimal or other numeric type to a float safely."""
if val is None:
return None
try:
return float(val)
except Exception:
return None
class HardwareMonitor:
def __init__(self, dll_file: str) -> None:
"""Initialize the hardware monitor and load the DLL."""
try:
clr.AddReference(dll_file)
except Exception as e:
log(f"[ERROR] Failed to load DLL: {e}")
raise
# Import hardware monitoring classes from the DLL
from LibreHardwareMonitor.Hardware import Computer, SensorType, HardwareType
self.SensorType = SensorType
self.HardwareType = HardwareType
# Enable all hardware components to monitor
self.computer = Computer()
self.computer.IsCpuEnabled = True
self.computer.IsGpuEnabled = True
self.computer.IsMemoryEnabled = True
self.computer.IsStorageEnabled = True
self.computer.IsMotherboardEnabled = True
self.computer.IsBatteryEnabled = True
self.computer.IsControllerEnabled = True
try:
self.computer.Open()
except Exception as e:
log(f"[ERROR] Failed to initialize hardware monitor: {e}")
raise
def get_monitor_data(self) -> Dict[str, Any]:
"""
Collect and return hardware sensor data in a dictionary.
Data includes CPU, GPU, RAM, Storage temps, loads, clocks, etc.
"""
data: Dict[str, Any] = {
"cpu_temp": None,
"cpu_load": None,
"cpu_power": None,
"cpu_voltage": None,
"cpu_core_load": [],
"cpu_clock_avg": None,
"gpu_temp": None,
"gpu_load": None,
"gpu_mem_used": None,
"gpu_power": None,
"gpu_clock": None,
"gpu_fan_speed": None,
"ram_used": None,
"ram_total": None,
"ram_free": None,
"disk_temp": None,
"fan_speed": None,
}
voltages = []
for hardware in self.computer.Hardware:
try:
hardware.Update()
except Exception as e:
log(f"[WARN] Hardware update failed for {hardware.Name}: {e}")
continue
hw_type = hardware.HardwareType.ToString().upper()
# Process CPU sensors
if hw_type == "CPU":
cpu_core_clocks = []
for sensor in hardware.Sensors:
s_type = sensor.SensorType
s_name = sensor.Name.upper()
s_val = decimal_to_float(sensor.Value)
if s_type == self.SensorType.Temperature:
if s_val is not None and data["cpu_temp"] is None:
data["cpu_temp"] = s_val
elif s_type == self.SensorType.Load:
if "CPU TOTAL" in s_name:
data["cpu_load"] = s_val
elif "CORE" in s_name or "THREAD" in s_name:
if s_val is not None:
data["cpu_core_load"].append(s_val)
elif s_type == self.SensorType.Clock and "CORE" in s_name:
if s_val is not None:
cpu_core_clocks.append(s_val)
elif s_type == self.SensorType.Power and "PACKAGE" in s_name:
data["cpu_power"] = s_val
elif s_type == self.SensorType.Voltage and "CORE" in s_name:
if s_val is not None:
voltages.append(s_val)
if cpu_core_clocks:
data["cpu_clock_avg"] = round(
sum(cpu_core_clocks) / len(cpu_core_clocks), 2
)
if voltages:
data["cpu_voltage"] = round(sum(voltages) / len(voltages), 3)
# Process GPU sensors
elif hw_type in ("GPUNVIDIA", "GPUAMD", "GPUINTEL"):
for sensor in hardware.Sensors:
s_type = sensor.SensorType
s_name = sensor.Name.upper()
s_val = decimal_to_float(sensor.Value)
if s_type == self.SensorType.Temperature and "CORE" in s_name:
if data["gpu_temp"] is None:
data["gpu_temp"] = s_val
elif s_type == self.SensorType.Load and "GPU CORE" in s_name:
data["gpu_load"] = s_val
elif s_type == self.SensorType.Power and "GPU" in s_name:
data["gpu_power"] = s_val
elif s_type == self.SensorType.Clock and "CORE" in s_name:
data["gpu_clock"] = s_val
elif s_type == self.SensorType.Fan and "GPU" in s_name:
data["gpu_fan_speed"] = s_val
elif (
s_type == self.SensorType.SmallData and "MEMORY USED" in s_name
):
data["gpu_mem_used"] = (
s_val / 1024 if s_val is not None else None
)
elif s_type == self.SensorType.Load and "MEMORY" in s_name:
if data["gpu_mem_used"] is None:
data["gpu_mem_used"] = s_val
# Process RAM sensors
elif hw_type == "MEMORY":
for sensor in hardware.Sensors:
s_type = sensor.SensorType
s_name = sensor.Name.upper()
s_val = decimal_to_float(sensor.Value)
if s_type == self.SensorType.Data:
if "USED" in s_name:
data["ram_used"] = s_val
elif "AVAILABLE" in s_name:
data["ram_free"] = s_val
if data["ram_used"] is not None and data["ram_free"] is not None:
data["ram_total"] = round(data["ram_used"] + data["ram_free"], 2)
# Process Storage (disk) sensors
elif hw_type == "STORAGE":
for sensor in hardware.Sensors:
if sensor.SensorType == self.SensorType.Temperature:
data["disk_temp"] = decimal_to_float(sensor.Value)
# Process Motherboard fan sensors
elif hw_type in ("MOTHERBOARD", "SUPERIO", "EMBEDDEDCONTROLLER"):
for sensor in hardware.Sensors:
if (
sensor.SensorType == self.SensorType.Fan
and "CPU" in sensor.Name.upper()
):
data["fan_speed"] = decimal_to_float(sensor.Value)
return data
def main(dll_path: Optional[str] = None):
"""
Main entry point.
Accepts optional path to the DLL file.
"""
if dll_path is None:
base_dir = os.path.dirname(os.path.abspath(__file__))
dll_path = os.path.join(base_dir, "LibreHardwareMonitorLib.dll")
if not os.path.exists(dll_path):
log(f"[ERROR] DLL not found: {dll_path}")
sys.exit(1)
try:
monitor = HardwareMonitor(dll_path)
except Exception as e:
log(f"[ERROR] Failed to initialize hardware monitor: {e}")
sys.exit(1)
log("Starting hardware monitoring. Press Ctrl+C to stop.")
while True:
try:
metrics = monitor.get_monitor_data()
log("[SUMMARY]")
for k, v in metrics.items():
log(f" {k}: {v}")
time.sleep(10)
except KeyboardInterrupt:
log("Monitoring stopped by user.")
break
except Exception as e:
log(f"[ERROR] Failed to read data: {e}")
time.sleep(10)
if __name__ == "__main__":
# Allow DLL path argument: python script.py path/to/LibreHardwareMonitorLib.dll
dll_arg = sys.argv[1] if len(sys.argv) > 1 else None
main(dll_arg)
How It Works
-
The Dash application (first script) creates a web interface displaying the metrics from your system in real-time.
-
The HardwareMonitor class (second script) uses CLR to interact with the LibreHardwareMonitorLib.dll and fetch the hardware data. It runs as a background service, pulling stats every second and sending them to the dashboard.
Administrator Permissions
Keep in mind that both scripts require administrator privileges to access hardware data. This is because the system monitoring tools need elevated permissions to interact with low-level hardware sensors. On Windows, you’ll need to run these scripts as an administrator to avoid access issues.
Next Steps
I have a few ideas for improving this setup in the future:
-
Enhanced Graphs: I plan to add more detailed and interactive charts to make the dashboard more insightful.
-
Image Carousel: To make the interface more visually appealing, I’d like to include an image carousel that displays dynamic content alongside the system stats, so it’s not just a sea of numbers.
-
Standalone App: I’m considering turning this into a standalone application, like one of my previous projects Daily Tips - Double-click to open Dash handshake, so that it can run independently and automatically start when you boot up your computer.
The goal is to create a sleek, interactive tool that’s both functional and fun to use.
Hope you like this. XD
Keywords: Hardware Monitor, System Monitoring, Second Screen, CPU, RAM, GPU
