Daily Tips - If I have a Second Screen that Sits Idle 🎶

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:

  1. 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)

  1. 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

2 Likes

Very cool, thanks for sharing this tip, @stu .

I’m curious to see what enhanced graphs you plan to add.

I can see this app being especially useful for people who play hardware-demanding computer games.

1 Like

hi @adamschroeder ,

Thx for your curiosity about the charts I might add to this sys stat monitoring app, which really got me thinking!

I just suddenly thought, since you’ve been running those awesome Figure Friday challenges, why not turn this question into a new challenge on your list? I think we could create a new, really great community product from it.

1 Like

That could work, @stu .
Do you have a good dataset we can use?

hmm, I just recorded a one-min dataset on my laptop using below script that collects sensor data. however, since this project is designed to work with real-time hardware readings, would actually recommend that challengers gather the sensor data directly from their own devices.

from hardware_monitor import HardwareMonitor
import os
import json
import time

base_dir = os.path.dirname(os.path.abspath(__file__))
dll_file = os.path.join(base_dir, "LibreHardwareMonitorLib.dll")
monitor = HardwareMonitor(dll_file)


records = []
for _ in range(60):
    data = monitor.get_monitor_data()
    records.append(data)
    time.sleep(1)


with open("sensors_log.txt", "w") as f:
    json.dump(records, f, indent=2)
    print("Done.")

Gathering real-time sensor data allow them to simulate different scenarios, such as high load, thermal spikes, GPU-intensive tasks, etc. And design visualization approaches that best fit each situation. Using real data from their own hardware will give them a much more accurate and meaningful basis for tesing.

[
  {
    "cpu_temp": 54.0,
    "cpu_load": 13.689554214477539,
    "cpu_power": 7.142908096313477,
    "cpu_voltage": 0.719,
    "cpu_core_load": [
      15.605711936950684,
      10.788869857788086,
      39.891807556152344,
      37.22257614135742,
      40.13304138183594,
      43.4936408996582,
      5.123787879943848,
      1.41831636428833,
      2.8483450412750244,
      2.8010785579681396,
      27.702157974243164,
      44.43605422973633,
      0.0,
      0.0,
      0.0,
      0.0,
      2.3184776306152344,
      1.8863320350646973,
      0.0,
      2.524310350418091,
      44.43605422973633
    ],
    "cpu_clock_avg": 1119.63,
    "gpu_temp": 46.0,
    "gpu_load": 0.0,
    "gpu_mem_used": 1.1258087158203125,
    "gpu_power": 0.010069490410387516,
    "gpu_clock": 210.0,
    "gpu_fan_speed": null,
    "ram_used": 14.328521728515625,
    "ram_total": 33.61,
    "ram_free": 19.286136627197266,
    "disk_temp": null,
    "fan_speed": null
  },
  {
    "cpu_temp": 53.0,
    "cpu_load": 11.41698932647705,
    "cpu_power": 7.424137592315674,
    "cpu_voltage": 0.714,
    "cpu_core_load": [
      6.581169128417969,
      3.414398431777954,
      34.96672439575195,
      33.667816162109375,
      32.102394104003906,
      49.328887939453125,
      2.6257097721099854,
      4.8829498291015625,
      3.3443212509155273,
      2.684682607650757,
      21.485816955566406,
      28.13352394104004,
      1.4185428619384766,
      1.4140665531158447,
      1.4104366302490234,
      1.4127075672149658,
      0.0,
      0.0,
      1.4805257320404053,
      0.0,
      49.328887939453125
    ],
    "cpu_clock_avg": 1376.37,
    "gpu_temp": 46.0,
    "gpu_load": 0.0,
    "gpu_mem_used": 1.1470451354980469,
    "gpu_power": 0.05395229160785675,
    "gpu_clock": 210.0,
    "gpu_fan_speed": null,
    "ram_used": 14.357158660888672,
    "ram_total": 33.61,
    "ram_free": 19.25749969482422,
    "disk_temp": null,
    "fan_speed": null
  },

...  

]

hi @adamschroeder ,

Do you have any good idea to let me attach a txt to share with you?

Thanks @stu
Can you email me please?
adam@plot.ly

Done.

:handshake: :handshake: :handshake: