WIP - have working UI for base functions

This commit is contained in:
2026-05-30 14:01:22 -05:00
parent 95e97cf96b
commit 45bf81ebfa
24 changed files with 4420 additions and 4 deletions
@@ -0,0 +1,13 @@
<Application
x:Class="LibreHardwareMonitor.Windows.WinUI.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Microsoft.UI.Xaml.Controls">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<controls:XamlControlsResources />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>
@@ -0,0 +1,79 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.UI.Xaml;
using IOPath = System.IO.Path;
namespace LibreHardwareMonitor.Windows.WinUI;
public partial class App : Application
{
private bool _launchStarted;
private Window? _window;
public App()
{
UnhandledException += App_UnhandledException;
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
InitializeComponent();
}
protected override void OnLaunched(LaunchActivatedEventArgs args)
{
LaunchMainWindow();
}
private void LaunchMainWindow()
{
if (_launchStarted)
return;
_launchStarted = true;
try
{
MainWindow mainWindow = new();
_window = mainWindow;
_window.Activate();
mainWindow.StartMonitoringAfterActivation();
}
catch (Exception ex)
{
WriteExceptionLog("Window launch failed", ex);
throw;
}
}
private static void App_UnhandledException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs args)
{
WriteExceptionLog("WinUI unhandled exception", args.Exception);
}
private static void CurrentDomain_UnhandledException(object sender, System.UnhandledExceptionEventArgs args)
{
if (args.ExceptionObject is Exception exception)
WriteExceptionLog("AppDomain unhandled exception", exception);
else
WriteMessageLog($"AppDomain unhandled exception: {args.ExceptionObject}");
}
private static void TaskScheduler_UnobservedTaskException(object? sender, UnobservedTaskExceptionEventArgs args)
{
WriteExceptionLog("Unobserved task exception", args.Exception);
}
private static void WriteExceptionLog(string context, Exception exception)
{
WriteMessageLog($"{context}{Environment.NewLine}{exception}");
}
private static void WriteMessageLog(string message)
{
string logPath = IOPath.Combine(AppContext.BaseDirectory, "LibreHardwareMonitor.Windows.WinUI.startup.log");
File.AppendAllText(logPath, $"{DateTimeOffset.Now:u} {message}{Environment.NewLine}{Environment.NewLine}");
}
}
@@ -0,0 +1,35 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net10.0-windows10.0.19041.0</TargetFramework>
<TargetPlatformMinVersion>10.0.19041.0</TargetPlatformMinVersion>
<Platforms>x64;x86;ARM64</Platforms>
<RuntimeIdentifiers>win-x64;win-x86;win-arm64</RuntimeIdentifiers>
<AssemblyName>LibreHardwareMonitor.Windows.WinUI</AssemblyName>
<AssemblyTitle>Libre Hardware Monitor</AssemblyTitle>
<RootNamespace>LibreHardwareMonitor.Windows.WinUI</RootNamespace>
<ApplicationIcon>..\LibreHardwareMonitor.Windows.Forms\Resources\icon.ico</ApplicationIcon>
<ApplicationManifest>app.manifest</ApplicationManifest>
<UseWinUI>true</UseWinUI>
<WindowsPackageType>None</WindowsPackageType>
<WindowsAppSDKSelfContained>true</WindowsAppSDKSelfContained>
<OutputPath>..\bin\$(Configuration)\</OutputPath>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.WindowsAppSDK" Version="2.0.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LibreHardwareMonitorLib\LibreHardwareMonitorLib.csproj" />
</ItemGroup>
<ItemGroup>
<Content Include="..\LibreHardwareMonitor.Windows.Forms\Resources\icon.ico" Link="Resources\icon.ico" CopyToOutputDirectory="PreserveNewest" />
<EmbeddedResource Include="..\LibreHardwareMonitor.Windows.Forms\Resources\PawnIO_setup.exe" Link="Resources\PawnIO_setup.exe" />
<EmbeddedResource Include="..\LibreHardwareMonitor.Windows.Forms\Resources\*.png" LinkBase="Resources" />
<EmbeddedResource Include="..\LibreHardwareMonitor.Windows.Forms\Resources\Web\**" LinkBase="Resources\Web" />
</ItemGroup>
</Project>
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,234 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Text;
using System.Xml;
using LibreHardwareMonitor.Hardware;
using Windows.UI;
namespace LibreHardwareMonitor.Windows.WinUI.Services;
public sealed class AppSettings : ISettings
{
private readonly IDictionary<string, string> _settings = new Dictionary<string, string>();
private AppSettings(string fileName)
{
FileName = fileName;
}
public string FileName { get; }
public static AppSettings LoadDefault()
{
string fileName = Path.ChangeExtension(Environment.ProcessPath, ".config")
?? Path.Combine(AppContext.BaseDirectory, "LibreHardwareMonitor.Windows.WinUI.config");
AppSettings settings = new(fileName);
settings.Load();
return settings;
}
public bool Contains(string name)
{
return _settings.ContainsKey(name);
}
public string GetValue(string name, string value)
{
return _settings.TryGetValue(name, out string? result) ? result : value;
}
public int GetValue(string name, int value)
{
return _settings.TryGetValue(name, out string? result) && int.TryParse(result, out int parsedValue)
? parsedValue
: value;
}
public float GetValue(string name, float value)
{
return _settings.TryGetValue(name, out string? result)
&& float.TryParse(result, NumberStyles.Float, CultureInfo.InvariantCulture, out float parsedValue)
? parsedValue
: value;
}
public double GetValue(string name, double value)
{
return _settings.TryGetValue(name, out string? result)
&& double.TryParse(result, NumberStyles.Float, CultureInfo.InvariantCulture, out double parsedValue)
? parsedValue
: value;
}
public bool GetValue(string name, bool value)
{
return _settings.TryGetValue(name, out string? result) ? result == "true" : value;
}
public Color GetValue(string name, Color value)
{
if (_settings.TryGetValue(name, out string? result)
&& uint.TryParse(result, NumberStyles.HexNumber, CultureInfo.InvariantCulture, out uint parsedValue))
{
return Color.FromArgb(
(byte)((parsedValue >> 24) & 0xff),
(byte)((parsedValue >> 16) & 0xff),
(byte)((parsedValue >> 8) & 0xff),
(byte)(parsedValue & 0xff));
}
return value;
}
public void SetValue(string name, string value)
{
_settings[name] = value;
}
public void SetValue(string name, int value)
{
_settings[name] = value.ToString(CultureInfo.InvariantCulture);
}
public void SetValue(string name, float value)
{
_settings[name] = value.ToString(CultureInfo.InvariantCulture);
}
public void SetValue(string name, double value)
{
_settings[name] = value.ToString(CultureInfo.InvariantCulture);
}
public void SetValue(string name, bool value)
{
_settings[name] = value ? "true" : "false";
}
public void SetValue(string name, Color value)
{
uint argb = (uint)((value.A << 24) | (value.R << 16) | (value.G << 8) | value.B);
_settings[name] = argb.ToString("X8", CultureInfo.InvariantCulture);
}
public void Remove(string name)
{
_settings.Remove(name);
}
public void Load()
{
XmlDocument doc = new();
try
{
doc.Load(FileName);
}
catch
{
if (!TryLoadBackup(doc))
return;
}
XmlNodeList list = doc.GetElementsByTagName("appSettings");
foreach (XmlNode node in list)
{
if (node.ParentNode?.Name != "configuration" || node.ParentNode.ParentNode is not XmlDocument)
continue;
foreach (XmlNode child in node.ChildNodes)
{
if (child.Name != "add")
continue;
XmlAttribute? keyAttribute = child.Attributes?["key"];
XmlAttribute? valueAttribute = child.Attributes?["value"];
if (keyAttribute?.Value != null && valueAttribute?.Value != null)
_settings[keyAttribute.Value] = valueAttribute.Value;
}
}
}
public void Save()
{
Directory.CreateDirectory(Path.GetDirectoryName(FileName) ?? AppContext.BaseDirectory);
XmlDocument doc = new();
doc.AppendChild(doc.CreateXmlDeclaration("1.0", "utf-8", null));
XmlElement configuration = doc.CreateElement("configuration");
doc.AppendChild(configuration);
XmlElement appSettings = doc.CreateElement("appSettings");
configuration.AppendChild(appSettings);
foreach (KeyValuePair<string, string> setting in _settings)
{
XmlElement add = doc.CreateElement("add");
add.SetAttribute("key", setting.Key);
add.SetAttribute("value", setting.Value);
appSettings.AppendChild(add);
}
byte[] file;
using (MemoryStream memory = new())
{
using (StreamWriter writer = new(memory, Encoding.UTF8, leaveOpen: true))
doc.Save(writer);
file = memory.ToArray();
}
string backupFileName = FileName + ".backup";
if (File.Exists(FileName))
{
TryDelete(backupFileName);
TryMove(FileName, backupFileName);
}
using (FileStream stream = new(FileName, FileMode.Create, FileAccess.Write))
stream.Write(file, 0, file.Length);
TryDelete(backupFileName);
}
private bool TryLoadBackup(XmlDocument doc)
{
TryDelete(FileName);
string backupFileName = FileName + ".backup";
try
{
doc.Load(backupFileName);
return true;
}
catch
{
TryDelete(backupFileName);
return false;
}
}
private static void TryDelete(string fileName)
{
try
{
File.Delete(fileName);
}
catch
{
}
}
private static void TryMove(string sourceFileName, string destinationFileName)
{
try
{
File.Move(sourceFileName, destinationFileName);
}
catch
{
}
}
}
@@ -0,0 +1,197 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System;
using System.Linq;
using System.Threading.Tasks;
using LibreHardwareMonitor.Hardware;
using LibreHardwareMonitor.Hardware.Storage;
using LibreHardwareMonitor.Windows.WinUI.ViewModels;
namespace LibreHardwareMonitor.Windows.WinUI.Services;
public sealed class HardwareMonitorService : IDisposable
{
private readonly object _updateLock = new();
private readonly UpdateVisitor _updateVisitor = new();
private bool _isOpen;
public HardwareMonitorService(AppSettings settings)
{
Settings = settings;
Computer = new Computer(settings);
Computer.HardwareAdded += HardwareChanged;
Computer.HardwareRemoved += HardwareChanged;
ApplyHardwareFlagsFromSettings();
}
public event EventHandler? TreeRebuilt;
public Computer Computer { get; }
public AppSettings Settings { get; }
public SensorTreeItemViewModel Root { get; private set; } = SensorTreeItemViewModel.CreateRoot(Environment.MachineName);
public bool IsMotherboardEnabled
{
get => Computer.IsMotherboardEnabled;
set => SetHardwareEnabled("mainboardMenuItem", value, v => Computer.IsMotherboardEnabled = v);
}
public bool IsCpuEnabled
{
get => Computer.IsCpuEnabled;
set => SetHardwareEnabled("cpuMenuItem", value, v => Computer.IsCpuEnabled = v);
}
public bool IsMemoryEnabled
{
get => Computer.IsMemoryEnabled;
set => SetHardwareEnabled("ramMenuItem", value, v => Computer.IsMemoryEnabled = v);
}
public bool IsGpuEnabled
{
get => Computer.IsGpuEnabled;
set => SetHardwareEnabled("gpuMenuItem", value, v => Computer.IsGpuEnabled = v);
}
public bool IsPowerMonitorEnabled
{
get => Computer.IsPowerMonitorEnabled;
set => SetHardwareEnabled("powerMonitorMenuItem", value, v => Computer.IsPowerMonitorEnabled = v);
}
public bool IsControllerEnabled
{
get => Computer.IsControllerEnabled;
set => SetHardwareEnabled("fanControllerMenuItem", value, v => Computer.IsControllerEnabled = v);
}
public bool IsStorageEnabled
{
get => Computer.IsStorageEnabled;
set => SetHardwareEnabled("hddMenuItem", value, v => Computer.IsStorageEnabled = v);
}
public bool IsNetworkEnabled
{
get => Computer.IsNetworkEnabled;
set => SetHardwareEnabled("nicMenuItem", value, v => Computer.IsNetworkEnabled = v);
}
public bool IsPsuEnabled
{
get => Computer.IsPsuEnabled;
set => SetHardwareEnabled("psuMenuItem", value, v => Computer.IsPsuEnabled = v);
}
public bool IsBatteryEnabled
{
get => Computer.IsBatteryEnabled;
set => SetHardwareEnabled("batteryMenuItem", value, v => Computer.IsBatteryEnabled = v);
}
public bool ForceDriveWakeup
{
get => Settings.GetValue("forceDriveWakeupItem", false);
set
{
Settings.SetValue("forceDriveWakeupItem", value);
foreach (StorageDevice storageDevice in Computer.Hardware.OfType<StorageDevice>())
storageDevice.ForceWakeup = value;
}
}
public void Open(bool raiseTreeRebuilt = true)
{
if (_isOpen)
return;
Computer.Open();
_isOpen = true;
RebuildTree(raiseTreeRebuilt);
}
public void Reset()
{
if (_isOpen)
Computer.Close();
Root = SensorTreeItemViewModel.CreateRoot(Environment.MachineName);
Computer.Open();
RebuildTree();
}
public Task UpdateAsync()
{
return Task.Run(() =>
{
lock (_updateLock)
Computer.Accept(_updateVisitor);
});
}
public void ResetMinMax()
{
SensorVisitor visitor = new(sensor => sensor.ResetMin());
visitor.VisitComputer(Computer);
visitor = new SensorVisitor(sensor => sensor.ResetMax());
visitor.VisitComputer(Computer);
}
public void ClearSensorValues()
{
SensorVisitor visitor = new(sensor => sensor.ClearValues());
visitor.VisitComputer(Computer);
}
public void RebuildTree(bool raiseTreeRebuilt = true)
{
SensorTreeItemViewModel root = SensorTreeItemViewModel.CreateRoot(Environment.MachineName);
foreach (IHardware hardware in Computer.Hardware.OrderBy(hardware => hardware.HardwareType).ThenBy(hardware => hardware.Name, StringComparer.OrdinalIgnoreCase))
root.Children.Add(SensorTreeItemViewModel.FromHardware(hardware, Settings));
Root = root;
if (raiseTreeRebuilt)
TreeRebuilt?.Invoke(this, EventArgs.Empty);
}
public void Dispose()
{
Computer.HardwareAdded -= HardwareChanged;
Computer.HardwareRemoved -= HardwareChanged;
Computer.Close();
}
private void ApplyHardwareFlagsFromSettings()
{
Computer.IsMotherboardEnabled = Settings.GetValue("mainboardMenuItem", true);
Computer.IsCpuEnabled = Settings.GetValue("cpuMenuItem", true);
Computer.IsMemoryEnabled = Settings.GetValue("ramMenuItem", true);
Computer.IsGpuEnabled = Settings.GetValue("gpuMenuItem", true);
Computer.IsPowerMonitorEnabled = Settings.GetValue("powerMonitorMenuItem", true);
Computer.IsControllerEnabled = Settings.GetValue("fanControllerMenuItem", true);
Computer.IsStorageEnabled = Settings.GetValue("hddMenuItem", true);
Computer.IsNetworkEnabled = Settings.GetValue("nicMenuItem", true);
Computer.IsPsuEnabled = Settings.GetValue("psuMenuItem", true);
Computer.IsBatteryEnabled = Settings.GetValue("batteryMenuItem", true);
ForceDriveWakeup = Settings.GetValue("forceDriveWakeupItem", false);
}
private void SetHardwareEnabled(string settingName, bool value, Action<bool> setter)
{
setter(value);
Settings.SetValue(settingName, value);
if (_isOpen)
RebuildTree();
}
private void HardwareChanged(IHardware hardware)
{
if (_isOpen)
RebuildTree();
}
}
@@ -0,0 +1,201 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using LibreHardwareMonitor.Hardware;
namespace LibreHardwareMonitor.Windows.WinUI.Services;
public sealed class Logger
{
private const string FileNameFormat = "LibreHardwareMonitorLog-{0:yyyy-MM-dd}{1}.csv";
private readonly IComputer _computer;
private DateTime _day = DateTime.MinValue;
private string _fileName = "";
private string[]? _identifiers;
private DateTime _lastLoggedTime = DateTime.MinValue;
private ISensor?[]? _sensors;
public Logger(IComputer computer)
{
_computer = computer;
_computer.HardwareAdded += HardwareAdded;
_computer.HardwareRemoved += HardwareRemoved;
}
public LoggerFileRotation FileRotationMethod { get; set; } = LoggerFileRotation.PerSession;
public TimeSpan LoggingInterval { get; set; } = TimeSpan.FromSeconds(1);
public void Log()
{
DateTime now = DateTime.Now;
if (_lastLoggedTime + LoggingInterval - TimeSpan.FromMilliseconds(500) > now)
return;
switch (FileRotationMethod)
{
case LoggerFileRotation.PerSession:
if (!File.Exists(_fileName) || now - _lastLoggedTime > LoggingInterval + TimeSpan.FromMilliseconds(100))
{
uint sessionNumber = 1;
do
{
_fileName = GetFileName(DateTime.Now, sessionNumber);
sessionNumber++;
} while (File.Exists(_fileName));
CreateNewLogFile();
}
break;
case LoggerFileRotation.Daily:
if (_day != now.Date || !File.Exists(_fileName))
{
_day = now.Date;
_fileName = GetFileName(_day);
if (!OpenExistingLogFile())
CreateNewLogFile();
}
break;
}
try
{
using StreamWriter writer = new(new FileStream(_fileName, FileMode.Append, FileAccess.Write, FileShare.ReadWrite));
writer.Write(now.ToString("G", CultureInfo.InvariantCulture));
writer.Write(",");
if (_sensors != null)
{
for (int i = 0; i < _sensors.Length; i++)
{
float? value = _sensors[i]?.Value;
if (value.HasValue)
writer.Write(value.Value.ToString("R", CultureInfo.InvariantCulture));
writer.Write(i < _sensors.Length - 1 ? "," : Environment.NewLine);
}
}
}
catch (IOException)
{
}
_lastLoggedTime = now;
}
private static string GetFileName(DateTime date, uint sessionNumber = 0)
{
return Path.Combine(AppContext.BaseDirectory, string.Format(FileNameFormat, date, sessionNumber == 0 ? "" : "-" + sessionNumber));
}
private void CreateNewLogFile()
{
IList<ISensor> sensors = new List<ISensor>();
SensorVisitor visitor = new(sensor => sensors.Add(sensor));
visitor.VisitComputer(_computer);
_sensors = sensors.Cast<ISensor?>().ToArray();
_identifiers = sensors.Select(sensor => sensor.Identifier.ToString()).ToArray();
using StreamWriter writer = new(_fileName, false);
writer.Write(",");
writer.WriteLine(string.Join(",", _identifiers));
writer.Write("Time,");
writer.WriteLine(string.Join(",", sensors.Select(sensor => $"\"{sensor.Name}\"")));
}
private bool OpenExistingLogFile()
{
if (!File.Exists(_fileName))
return false;
try
{
using StreamReader reader = new(_fileName);
string? line = reader.ReadLine();
if (string.IsNullOrEmpty(line))
return false;
_identifiers = line.Split(',').Skip(1).ToArray();
}
catch
{
_identifiers = null;
return false;
}
if (_identifiers.Length == 0)
{
_identifiers = null;
return false;
}
_sensors = new ISensor?[_identifiers.Length];
SensorVisitor visitor = new(sensor =>
{
for (int i = 0; i < _identifiers.Length; i++)
{
if (sensor.Identifier.ToString() == _identifiers[i])
_sensors[i] = sensor;
}
});
visitor.VisitComputer(_computer);
return true;
}
private void HardwareAdded(IHardware hardware)
{
foreach (ISensor sensor in hardware.Sensors)
SensorAdded(sensor);
hardware.SensorAdded += SensorAdded;
hardware.SensorRemoved += SensorRemoved;
foreach (IHardware subHardware in hardware.SubHardware)
HardwareAdded(subHardware);
}
private void HardwareRemoved(IHardware hardware)
{
hardware.SensorAdded -= SensorAdded;
hardware.SensorRemoved -= SensorRemoved;
foreach (ISensor sensor in hardware.Sensors)
SensorRemoved(sensor);
foreach (IHardware subHardware in hardware.SubHardware)
HardwareRemoved(subHardware);
}
private void SensorAdded(ISensor sensor)
{
if (_sensors == null || _identifiers == null)
return;
for (int i = 0; i < _sensors.Length; i++)
{
if (sensor.Identifier.ToString() == _identifiers[i])
_sensors[i] = sensor;
}
}
private void SensorRemoved(ISensor sensor)
{
if (_sensors == null)
return;
for (int i = 0; i < _sensors.Length; i++)
{
if (sensor == _sensors[i])
_sensors[i] = null;
}
}
}
@@ -0,0 +1,11 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
namespace LibreHardwareMonitor.Windows.WinUI.Services;
public enum LoggerFileRotation
{
PerSession = 0,
Daily = 1
}
@@ -0,0 +1,719 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using LibreHardwareMonitor.Hardware;
using LibreHardwareMonitor.Windows.WinUI.Utilities;
using LibreHardwareMonitor.Windows.WinUI.ViewModels;
namespace LibreHardwareMonitor.Windows.WinUI.Services;
public sealed class RemoteWebServer : IDisposable
{
private readonly IComputer _computer;
private readonly Func<SensorTreeItemViewModel?> _rootProvider;
private readonly Version _version = typeof(RemoteWebServer).Assembly.GetName().Version ?? new Version(0, 0);
private CancellationTokenSource? _cts;
private HttpListener? _listener;
private Task? _listenerTask;
public RemoteWebServer(
Func<SensorTreeItemViewModel?> rootProvider,
IComputer computer,
string listenerIp,
int listenerPort,
bool authEnabled,
string userName,
string passwordSha256)
{
_rootProvider = rootProvider;
_computer = computer;
ListenerIp = listenerIp;
ListenerPort = listenerPort;
AuthEnabled = authEnabled;
UserName = userName;
PasswordSHA256 = passwordSha256;
try
{
_listener = new HttpListener { IgnoreWriteExceptions = true };
}
catch (PlatformNotSupportedException)
{
_listener = null;
}
}
public bool AuthEnabled { get; set; }
public bool IsRunning => _listener?.IsListening == true;
public string ListenerIp { get; set; }
public int ListenerPort { get; set; }
public string PasswordSHA256 { get; private set; }
public bool PlatformNotSupported => _listener == null;
public string UserName { get; set; }
public void Dispose()
{
Quit();
}
public void SetPassword(string plainPassword)
{
PasswordSHA256 = ComputeSHA256(plainPassword);
}
public bool Start()
{
if (PlatformNotSupported || _listener == null)
return false;
try
{
if (_listener.IsListening)
return true;
string listenerIp = ResolveListenerIp();
string prefix = $"http://{listenerIp}:{ListenerPort}/";
_listener.Prefixes.Clear();
_listener.Prefixes.Add(prefix);
_listener.Realm = "Libre Hardware Monitor";
_listener.AuthenticationSchemes = AuthEnabled ? AuthenticationSchemes.Basic : AuthenticationSchemes.Anonymous;
_listener.Start();
_cts = new CancellationTokenSource();
_listenerTask = Task.Run(() => ProcessRequestsAsync(_cts.Token));
return true;
}
catch
{
return false;
}
}
public bool Stop()
{
if (PlatformNotSupported || _listener == null)
return false;
try
{
_cts?.Cancel();
_listenerTask?.Wait(TimeSpan.FromSeconds(5));
_listener.Stop();
_cts?.Dispose();
_cts = null;
_listenerTask = null;
return true;
}
catch
{
return false;
}
}
public void Quit()
{
if (PlatformNotSupported)
return;
Stop();
try
{
_listener?.Abort();
}
catch
{
}
}
private async Task ProcessRequestsAsync(CancellationToken cancellationToken)
{
if (_listener == null)
return;
while (_listener.IsListening && !cancellationToken.IsCancellationRequested)
{
try
{
HttpListenerContext context = await _listener.GetContextAsync();
_ = Task.Run(() => HandleContextAsync(context), cancellationToken);
}
catch (ObjectDisposedException)
{
break;
}
catch (OperationCanceledException)
{
break;
}
catch (HttpListenerException)
{
if (!_listener.IsListening)
break;
}
catch
{
}
}
}
private async Task HandleContextAsync(HttpListenerContext context)
{
try
{
if (!IsAuthenticated(context))
{
context.Response.StatusCode = 401;
await SendResponseAsync(context.Response, "<HTML><BODY><H4>401 Unauthorized</H4>Authorization required.</BODY></HTML>", "text/html");
return;
}
HttpListenerRequest request = context.Request;
if (request.HttpMethod == "POST")
{
await HandlePostRequestAsync(context.Response, request);
return;
}
string requestedFile = request.RawUrl?.TrimStart('/') ?? "";
if (requestedFile.Length == 0)
requestedFile = "index.html";
if (requestedFile.Equals("data.json", StringComparison.OrdinalIgnoreCase))
{
await SendJsonAsync(context.Response, request);
return;
}
if (requestedFile.StartsWith("metrics", StringComparison.OrdinalIgnoreCase))
{
await SendPrometheusAsync(context.Response, request);
return;
}
if (requestedFile.StartsWith("Sensor", StringComparison.OrdinalIgnoreCase))
{
Dictionary<string, object?> result = [];
HandleSensorRequest(request, result);
await SendJsonSensorAsync(context.Response, result);
return;
}
if (requestedFile.StartsWith("ResetAllMinMax", StringComparison.OrdinalIgnoreCase))
{
_computer.Accept(new SensorVisitor(sensor =>
{
sensor.ResetMin();
sensor.ResetMax();
}));
await SendJsonAsync(context.Response, request);
return;
}
if (requestedFile.StartsWith("images_icon/", StringComparison.OrdinalIgnoreCase))
{
await ServeResourceFileAsync(context.Response, requestedFile["images_icon/".Length..]);
return;
}
await ServeResourceFileAsync(context.Response, PathForWebResource(requestedFile));
}
catch
{
try
{
context.Response.StatusCode = 500;
context.Response.Close();
}
catch
{
}
}
}
private bool IsAuthenticated(HttpListenerContext context)
{
if (!AuthEnabled)
return true;
try
{
if (context.User?.Identity is HttpListenerBasicIdentity identity)
return identity.Name == UserName && ComputeSHA256(identity.Password) == PasswordSHA256;
}
catch
{
}
return false;
}
private async Task HandlePostRequestAsync(HttpListenerResponse response, HttpListenerRequest request)
{
Dictionary<string, object?> result = new() { ["result"] = "ok" };
try
{
if (request.Url?.Segments.Length == 2 && request.Url.Segments[1].TrimEnd('/').Equals("Sensor", StringComparison.OrdinalIgnoreCase))
HandleSensorRequest(request, result);
else
throw new ArgumentException("Empty URL, possible values: ['Sensor']");
}
catch (Exception ex)
{
result["result"] = "fail";
result["message"] = ex.ToString();
}
await SendJsonSensorAsync(response, result);
}
private void HandleSensorRequest(HttpListenerRequest request, Dictionary<string, object?> result)
{
IDictionary<string, string> query = ParseQuery(request.Url?.Query);
if (!query.TryGetValue("action", out string? action))
throw new ArgumentNullException(nameof(action), "No action provided");
if (!query.TryGetValue("id", out string? id))
throw new ArgumentNullException(nameof(id), "No id provided");
SensorTreeItemViewModel sensorNode = FindSensor(id)
?? throw new ArgumentException("Unknown id " + id + " specified");
ISensor sensor = sensorNode.Sensor!;
if (action == "ResetMinMax")
{
sensor.ResetMin();
sensor.ResetMax();
action = "Get";
}
switch (action)
{
case "Set" when query.TryGetValue("value", out string? value):
SetSensorControlValue(sensor, value);
result["result"] = "ok";
break;
case "Set":
throw new ArgumentNullException("value", "No value provided");
case "Get":
result["result"] = "ok";
result["value"] = sensor.Value;
result["min"] = sensor.Min;
result["max"] = sensor.Max;
result["format"] = SensorFormatter.GetFormatString(sensor);
break;
default:
throw new ArgumentException("Unknown action type " + action);
}
}
private SensorTreeItemViewModel? FindSensor(string id)
{
return _rootProvider()?.EnumerateSensors()
.FirstOrDefault(sensorItem => sensorItem.Sensor?.Identifier.ToString() == id);
}
private static void SetSensorControlValue(ISensor sensor, string value)
{
if (sensor.Control == null)
throw new ArgumentException("Specified sensor '" + sensor.Identifier + "' can not be set");
if (value == "null")
sensor.Control.SetDefault();
else
sensor.Control.SetSoftware(float.Parse(value, CultureInfo.InvariantCulture));
}
private async Task SendJsonAsync(HttpListenerResponse response, HttpListenerRequest request)
{
Dictionary<string, object?> json = new()
{
["id"] = 0,
["Version"] = $"{_version.Major}.{_version.Minor}.{_version.Build}",
["Text"] = "Sensor",
["Min"] = "Min",
["Value"] = "Value",
["Max"] = "Max",
["ImageURL"] = ""
};
int nodeIndex = 1;
SensorTreeItemViewModel? root = _rootProvider();
json["Children"] = root == null ? Array.Empty<object>() : new List<object> { GenerateJsonForNode(root, ref nodeIndex) };
JsonSerializerOptions options = new()
{
NumberHandling = JsonNumberHandling.AllowNamedFloatingPointLiterals
};
byte[] buffer = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(json, options));
bool acceptGzip = request.Headers["Accept-Encoding"]?.IndexOf("gzip", StringComparison.OrdinalIgnoreCase) >= 0;
response.AddHeader("Cache-Control", "no-cache");
response.AddHeader("Access-Control-Allow-Origin", "*");
response.ContentType = "application/json";
if (acceptGzip)
{
response.AddHeader("Content-Encoding", "gzip");
using MemoryStream memory = new();
using (GZipStream zip = new(memory, CompressionMode.Compress, true))
await zip.WriteAsync(buffer);
buffer = memory.ToArray();
}
response.ContentLength64 = buffer.Length;
await response.OutputStream.WriteAsync(buffer);
response.Close();
}
private Dictionary<string, object?> GenerateJsonForNode(SensorTreeItemViewModel node, ref int nodeIndex)
{
Dictionary<string, object?> jsonNode = new()
{
["id"] = nodeIndex++,
["Text"] = node.Text,
["Min"] = "",
["Value"] = "",
["Max"] = "",
["ImageURL"] = GetImageUrl(node)
};
if (node.Sensor != null)
{
jsonNode["SensorId"] = node.Sensor.Identifier.ToString();
jsonNode["Type"] = node.Sensor.SensorType.ToString();
jsonNode["Min"] = node.Min;
jsonNode["Value"] = node.Value;
jsonNode["Max"] = node.Max;
jsonNode["RawMin"] = node.Sensor.Min;
jsonNode["RawValue"] = node.Sensor.Value;
jsonNode["RawMax"] = node.Sensor.Max;
jsonNode["ImageURL"] = "images/transparent.png";
}
else if (node.Hardware != null)
{
jsonNode["HardwareId"] = node.Hardware.Identifier.ToString();
}
List<object> children = [];
foreach (SensorTreeItemViewModel child in node.Children)
children.Add(GenerateJsonForNode(child, ref nodeIndex));
jsonNode["Children"] = children;
return jsonNode;
}
private async Task SendPrometheusAsync(HttpListenerResponse response, HttpListenerRequest request)
{
Dictionary<string, int> settings = ParsePrometheusSettings(request);
string content = GeneratePrometheusResponse(_rootProvider(), settings);
response.AddHeader("Cache-Control", "no-cache");
response.AddHeader("Access-Control-Allow-Origin", "*");
response.AddHeader("X-archivelength", settings["archivelength"].ToString(CultureInfo.InvariantCulture));
response.AddHeader("X-timestamps", settings["timestamps"].ToString(CultureInfo.InvariantCulture));
response.AddHeader("X-lastvalue", settings["lastvalue"].ToString(CultureInfo.InvariantCulture));
await SendResponseAsync(response, content, "text/plain");
}
private static Dictionary<string, int> ParsePrometheusSettings(HttpListenerRequest request)
{
Dictionary<string, string> query = new(ParseQuery(request.Url?.Query), StringComparer.OrdinalIgnoreCase);
int archive = ClampQueryValue(query, "archivelength", 0, 0, 10);
int timestamps = ClampQueryValue(query, "timestamps", 0, 0, 1);
int lastValue = ClampQueryValue(query, "lastvalue", 1, 0, 1);
if (archive == 0 && lastValue == 0)
{
archive = 1;
timestamps = 1;
}
if (archive > 0)
timestamps = 1;
return new Dictionary<string, int>
{
["archivelength"] = archive,
["timestamps"] = timestamps,
["lastvalue"] = lastValue
};
}
private static int ClampQueryValue(IDictionary<string, string> query, string key, int fallback, int min, int max)
{
if (!query.TryGetValue(key, out string? rawValue) || !int.TryParse(rawValue, NumberStyles.Integer, CultureInfo.InvariantCulture, out int value))
return fallback;
return Math.Clamp(value, min, max);
}
private static string GeneratePrometheusResponse(SensorTreeItemViewModel? root, Dictionary<string, int> settings)
{
if (root == null)
return "";
StringBuilder builder = new();
string host = root.Text;
string lastTagName = "";
foreach (SensorTreeItemViewModel hardware in root.Enumerate().Where(item => item.Hardware != null))
{
string hardwareType = hardware.Hardware!.HardwareType.ToString();
string hardwareName = hardware.Hardware.Name;
string hardwareId = hardware.Hardware.Identifier.ToString();
string hardwareAlias = $"{hardwareName} ({hardwareId})";
foreach (SensorTreeItemViewModel sensorItem in hardware.EnumerateSensors())
{
ISensor sensor = sensorItem.Sensor!;
(string suffix, double factor) = GetPrometheusUnit(sensor.SensorType);
string tagName = $"lhm_{hardwareType}_{sensor.SensorType}{suffix}".ToLowerInvariant();
string sensorName = sensor.Name.Replace("#", "");
string sensorId = sensor.Identifier.ToString().StartsWith(hardwareId, StringComparison.Ordinal)
? sensor.Identifier.ToString()[hardwareId.Length..]
: sensor.Identifier.ToString();
string sensorAlias = $"{sensorName} ({sensorId})";
string tagLine = $$"""{{tagName}} {"sensorName"="{{sensorName}}", "sensorAlias"="{{sensorAlias}}", "hardwareName"="{{hardwareName}}", "hardwareAlias"="{{hardwareAlias}}", "sensorId"="{{sensorId}}", "hardwareId"="{{hardwareId}}", "host"="{{host}}"}""";
if (lastTagName != tagName)
{
builder.Append("# TYPE ").Append(tagName).AppendLine(" gauge");
lastTagName = tagName;
}
int counter = 0;
foreach (SensorValue value in sensor.Values.Reverse())
{
if (counter++ > settings["archivelength"])
break;
if (float.IsNaN(value.Value))
{
builder.Append("# HELP ").Append(tagLine).AppendLine(" has an invalid value and was skipped.");
continue;
}
if (counter == 1 && settings["lastvalue"] == 0)
continue;
builder.Append(tagLine).Append(' ').Append((value.Value * factor).ToString(CultureInfo.InvariantCulture));
if (settings["timestamps"] == 1)
builder.Append(' ').Append(((DateTimeOffset)value.Time).ToUnixTimeMilliseconds());
builder.AppendLine();
}
}
}
return builder.ToString();
}
private static (string Suffix, double Factor) GetPrometheusUnit(SensorType sensorType)
{
return sensorType switch
{
SensorType.Clock => ("_hertz", 1000000),
SensorType.Conductivity => ("_seconds_per_centimeter", 0.000001),
SensorType.Control => ("_percent", 1),
SensorType.Current => ("_amperes", 1),
SensorType.Data => ("_bytes", 1000000000),
SensorType.Energy => ("_watthour", 0.001),
SensorType.Fan => ("_rpm", 1),
SensorType.Flow => ("_liters_per_hour", 1),
SensorType.Frequency => ("_hertz", 1),
SensorType.Humidity => ("_percent", 1),
SensorType.Level => ("_percent", 1),
SensorType.Load => ("_percent", 1),
SensorType.Noise => ("_decibels", 1),
SensorType.Power => ("_watts", 1),
SensorType.SmallData => ("_bytes", 1024 * 1024),
SensorType.Temperature => ("_celsius", 1),
SensorType.Throughput => ("_bytes_per_second", 1),
SensorType.TimeSpan => ("_seconds", 1),
SensorType.Timing => ("_seconds", 0.000000001),
SensorType.Voltage => ("_volts", 1),
_ => ("", 1)
};
}
private async Task SendJsonSensorAsync(HttpListenerResponse response, Dictionary<string, object?> sensorData)
{
response.AddHeader("Cache-Control", "no-cache");
response.AddHeader("Access-Control-Allow-Origin", "*");
await SendResponseAsync(response, JsonSerializer.Serialize(sensorData), "application/json");
}
private static async Task SendResponseAsync(HttpListenerResponse response, string content, string contentType)
{
byte[] buffer = Encoding.UTF8.GetBytes(content);
response.ContentType = contentType;
response.ContentLength64 = buffer.Length;
await response.OutputStream.WriteAsync(buffer);
response.Close();
}
private async Task ServeResourceFileAsync(HttpListenerResponse response, string relativePath)
{
string normalizedPath = relativePath.Replace('/', '.').Replace('\\', '.').Replace("custom-theme", "custom_theme");
Assembly assembly = Assembly.GetExecutingAssembly();
string? resourceName = assembly.GetManifestResourceNames()
.FirstOrDefault(name => name.EndsWith(".Resources." + normalizedPath, StringComparison.OrdinalIgnoreCase)
|| name.EndsWith(".Resources.Web." + normalizedPath, StringComparison.OrdinalIgnoreCase));
if (resourceName == null)
{
response.StatusCode = 404;
response.Close();
return;
}
await using Stream? stream = assembly.GetManifestResourceStream(resourceName);
if (stream == null)
{
response.StatusCode = 404;
response.Close();
return;
}
response.ContentType = GetContentType(Path.GetExtension(relativePath));
response.ContentLength64 = stream.Length;
await stream.CopyToAsync(response.OutputStream);
response.Close();
}
private string ResolveListenerIp()
{
if (ListenerIp is "+" or "*" or "0.0.0.0")
return ListenerIp;
try
{
IPHostEntry host = Dns.GetHostEntry(Dns.GetHostName());
if (host.AddressList.Any(ip => ip.ToString() == ListenerIp))
return ListenerIp;
}
catch
{
}
ListenerIp = "+";
return ListenerIp;
}
private static string PathForWebResource(string requestedFile)
{
return "Web." + requestedFile.TrimStart('/').Replace('/', '.');
}
private static string GetImageUrl(SensorTreeItemViewModel node)
{
if (node.Hardware != null)
return "images_icon/" + GetHardwareImageFile(node.Hardware.HardwareType);
if (node.Kind == SensorTreeItemKind.SensorType)
return "images_icon/" + GetTypeImageFile(node.Text);
if (node.Kind == SensorTreeItemKind.Root)
return "images_icon/computer.png";
return "images/transparent.png";
}
private static string GetHardwareImageFile(HardwareType hardwareType)
{
return hardwareType switch
{
HardwareType.Cpu => "cpu.png",
HardwareType.GpuNvidia => "nvidia.png",
HardwareType.GpuAmd => "ati.png",
HardwareType.GpuIntel => "intel.png",
HardwareType.Storage => "hdd.png",
HardwareType.Motherboard => "mainboard.png",
HardwareType.SuperIO => "chip.png",
HardwareType.Memory => "ram.png",
HardwareType.Cooler => "fan.png",
HardwareType.Network => "nic.png",
HardwareType.Psu => "power-supply.png",
HardwareType.Battery => "battery.png",
HardwareType.PowerMonitor => "powermonitor.png",
_ => "cpu.png"
};
}
private static string GetTypeImageFile(string sensorTypeText)
{
return sensorTypeText switch
{
"Voltages" or "Currents" or "Conductivity" => "voltage.png",
"Clocks" or "Timings" or "Frequencies" => "clock.png",
"Load" => "load.png",
"Temperatures" => "temperature.png",
"Fans" => "fan.png",
"Flows" or "Humidity" => "flow.png",
"Controls" => "control.png",
"Levels" => "level.png",
"Powers" => "power.png",
"Noise" => "loudspeaker.png",
"Throughput" => "throughput.png",
"Data" or "Small Data" => "data.png",
"Energy" or "Time Spans" => "time.png",
_ => "power.png"
};
}
private static string GetContentType(string extension)
{
return extension.ToLowerInvariant() switch
{
".css" => "text/css",
".gif" => "image/gif",
".htm" or ".html" => "text/html",
".ico" => "image/x-icon",
".jpg" or ".jpeg" => "image/jpeg",
".js" => "application/javascript",
".png" => "image/png",
".txt" => "text/plain",
_ => "application/octet-stream"
};
}
private static IDictionary<string, string> ParseQuery(string? query)
{
Dictionary<string, string> result = new(StringComparer.OrdinalIgnoreCase);
if (string.IsNullOrWhiteSpace(query))
return result;
foreach (string part in query.TrimStart('?').Split('&', StringSplitOptions.RemoveEmptyEntries))
{
string[] split = part.Split('=', 2);
string key = WebUtility.UrlDecode(split[0]);
string value = split.Length > 1 ? WebUtility.UrlDecode(split[1]) : "";
result[key] = value;
}
return result;
}
public static string ComputeSHA256(string text)
{
using SHA256 hash = SHA256.Create();
return string.Concat(hash.ComputeHash(Encoding.UTF8.GetBytes(text)).Select(item => item.ToString("x2", CultureInfo.InvariantCulture)));
}
}
@@ -0,0 +1,38 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System;
using System.IO;
using Microsoft.Win32;
namespace LibreHardwareMonitor.Windows.WinUI.Services;
public sealed class StartupService
{
private const string RegistryPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
private const string RegistryValueName = nameof(LibreHardwareMonitor);
public bool IsAvailable => OperatingSystem.IsWindows();
public bool Startup
{
get
{
using RegistryKey? registryKey = Registry.CurrentUser.OpenSubKey(RegistryPath);
string? value = registryKey?.GetValue(RegistryValueName) as string;
return string.Equals(value, Environment.ProcessPath, StringComparison.OrdinalIgnoreCase);
}
set
{
if (!IsAvailable)
throw new InvalidOperationException("Windows startup registration is not available on this platform.");
using RegistryKey? registryKey = Registry.CurrentUser.CreateSubKey(RegistryPath);
if (value)
registryKey?.SetValue(RegistryValueName, Environment.ProcessPath ?? Path.Combine(AppContext.BaseDirectory, "LibreHardwareMonitor.Windows.WinUI.exe"));
else
registryKey?.DeleteValue(RegistryValueName, false);
}
}
}
@@ -0,0 +1,30 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using LibreHardwareMonitor.Hardware;
namespace LibreHardwareMonitor.Windows.WinUI.Services;
public sealed class UpdateVisitor : IVisitor
{
public void VisitComputer(IComputer computer)
{
computer.Traverse(this);
}
public void VisitHardware(IHardware hardware)
{
hardware.Update();
foreach (IHardware subHardware in hardware.SubHardware)
subHardware.Accept(this);
}
public void VisitSensor(ISensor sensor)
{
}
public void VisitParameter(IParameter parameter)
{
}
}
@@ -0,0 +1,144 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System;
using System.Globalization;
using System.Text;
using LibreHardwareMonitor.Hardware;
using LibreHardwareMonitor.Windows.WinUI.ViewModels;
namespace LibreHardwareMonitor.Windows.WinUI.Utilities;
public static class SensorFormatter
{
public static string GetFormatString(ISensor sensor)
{
return sensor.SensorType switch
{
SensorType.Voltage => "{0:F3} V",
SensorType.Current => "{0:F3} A",
SensorType.Clock => "{0:F1} MHz",
SensorType.Load => "{0:F1} %",
SensorType.Temperature => "{0:F1} \u00B0C",
SensorType.Fan => "{0:F0} RPM",
SensorType.Flow => "{0:F1} L/h",
SensorType.Control => "{0:F1} %",
SensorType.Level => "{0:F1} %",
SensorType.Power => "{0:F1} W",
SensorType.Data => "{0:F1} GB",
SensorType.SmallData => "{0:F1} MB",
SensorType.Factor => "{0:F3}",
SensorType.Frequency => "{0:F1} Hz",
SensorType.Throughput => "{0:F1} B/s",
SensorType.TimeSpan => "{0:g}",
SensorType.Timing => "{0:F3} ns",
SensorType.Energy => "{0:F0} mWh",
SensorType.Noise => "{0:F0} dBA",
SensorType.Conductivity => "{0:F1} \u00B5S/cm",
SensorType.Humidity => "{0:F0} %",
_ => "{0:F1}"
};
}
public static string FormatValue(ISensor sensor, float? value, TemperatureUnit temperatureUnit)
{
if (!value.HasValue)
return "-";
return sensor.SensorType switch
{
SensorType.Voltage => $"{value:F3} V",
SensorType.Current => $"{value:F3} A",
SensorType.Clock => $"{value:F1} MHz",
SensorType.Load => $"{value:F1} %",
SensorType.Temperature when temperatureUnit == TemperatureUnit.Fahrenheit => $"{CelsiusToFahrenheit(value):F1} \u00B0F",
SensorType.Temperature => $"{value:F1} \u00B0C",
SensorType.Fan => $"{value:F0} RPM",
SensorType.Flow => $"{value:F1} L/h",
SensorType.Control => $"{value:F1} %",
SensorType.Level => $"{value:F1} %",
SensorType.Power => $"{value:F1} W",
SensorType.Data => $"{value:F1} GB",
SensorType.SmallData => $"{value:F1} MB",
SensorType.Factor => $"{value:F3}",
SensorType.Frequency => $"{value:F1} Hz",
SensorType.Throughput => FormatThroughput(sensor, value.Value),
SensorType.TimeSpan => string.Format(CultureInfo.CurrentCulture, "{0:g}", TimeSpan.FromSeconds(value.Value)),
SensorType.Timing => $"{value:F3} ns",
SensorType.Energy => $"{value:F0} mWh",
SensorType.Noise => $"{value:F0} dBA",
SensorType.Conductivity => $"{value:F1} \u00B5S/cm",
SensorType.Humidity => $"{value:F0} %",
_ => value.Value.ToString("F1", CultureInfo.CurrentCulture)
};
}
public static double? GetPlotValue(ISensor sensor, TemperatureUnit temperatureUnit)
{
float? value = sensor.Value;
if (!value.HasValue)
return null;
if (sensor.SensorType == SensorType.Temperature && temperatureUnit == TemperatureUnit.Fahrenheit)
return CelsiusToFahrenheit(value.Value);
return value.Value;
}
public static string GetToolTip(ISensor sensor, TemperatureUnit temperatureUnit)
{
StringBuilder builder = new();
if (sensor is ICriticalSensorLimits criticalSensorLimits)
OptionallyAppendRange(builder, sensor, temperatureUnit, criticalSensorLimits.CriticalLowLimit, criticalSensorLimits.CriticalHighLimit, "critical");
if (sensor is ISensorLimits sensorLimits)
OptionallyAppendRange(builder, sensor, temperatureUnit, sensorLimits.LowLimit, sensorLimits.HighLimit, "normal");
return builder.ToString();
}
private static void OptionallyAppendRange(StringBuilder builder, ISensor sensor, TemperatureUnit temperatureUnit, float? min, float? max, string kind)
{
if (min.HasValue)
{
builder.AppendLine(max.HasValue
? $"{CultureInfo.CurrentUICulture.TextInfo.ToTitleCase(kind)} range: {FormatValue(sensor, min, temperatureUnit)} to {FormatValue(sensor, max, temperatureUnit)}."
: $"Minimal {kind} value: {FormatValue(sensor, min, temperatureUnit)}.");
}
else if (max.HasValue)
{
builder.AppendLine($"Maximal {kind} value: {FormatValue(sensor, max, temperatureUnit)}.");
}
}
private static string FormatThroughput(ISensor sensor, float value)
{
if (sensor.Name == "Connection Speed")
{
return value switch
{
100000000 => "100Mbps",
1000000000 => "1Gbps",
_ when value < 1024 => $"{value:F0} bps",
_ when value < 1048576 => $"{value / 1024:F1} Kbps",
_ when value < 1073741824 => $"{value / 1048576:F1} Mbps",
_ => $"{value / 1073741824:F1} Gbps"
};
}
const int oneMegabyte = 1048576;
return value < oneMegabyte ? $"{value / 1024:F1} KB/s" : $"{value / oneMegabyte:F1} MB/s";
}
private static float? CelsiusToFahrenheit(float? valueInCelsius)
{
return valueInCelsius * 1.8f + 32;
}
private static float CelsiusToFahrenheit(float valueInCelsius)
{
return valueInCelsius * 1.8f + 32;
}
}
@@ -0,0 +1,89 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using LibreHardwareMonitor.Hardware;
namespace LibreHardwareMonitor.Windows.WinUI.Utilities;
public static class SensorTypeDisplay
{
public static string GetText(SensorType sensorType)
{
return sensorType switch
{
SensorType.Voltage => "Voltages",
SensorType.Current => "Currents",
SensorType.Clock => "Clocks",
SensorType.Load => "Load",
SensorType.Temperature => "Temperatures",
SensorType.Fan => "Fans",
SensorType.Flow => "Flows",
SensorType.Control => "Controls",
SensorType.Level => "Levels",
SensorType.Power => "Powers",
SensorType.Data => "Data",
SensorType.SmallData => "Data",
SensorType.Factor => "Factors",
SensorType.Frequency => "Frequencies",
SensorType.Throughput => "Throughput",
SensorType.TimeSpan => "Times",
SensorType.Timing => "Timings",
SensorType.Energy => "Capacities",
SensorType.Noise => "Noise Levels",
SensorType.Conductivity => "Conductivities",
SensorType.Humidity => "Humidity Levels",
_ => sensorType.ToString()
};
}
public static string GetGlyph(SensorType sensorType)
{
return sensorType switch
{
SensorType.Voltage => "\uE945",
SensorType.Current => "\uE945",
SensorType.Clock => "\uE916",
SensorType.Load => "\uE9D9",
SensorType.Temperature => "\uE9CA",
SensorType.Fan => "\uE9F3",
SensorType.Flow => "\uE9D5",
SensorType.Control => "\uE713",
SensorType.Level => "\uE9D2",
SensorType.Power => "\uE7E8",
SensorType.Data => "\uE8AB",
SensorType.SmallData => "\uE8AB",
SensorType.Factor => "\uE9D2",
SensorType.Frequency => "\uE916",
SensorType.Throughput => "\uE9D5",
SensorType.TimeSpan => "\uE121",
SensorType.Timing => "\uE121",
SensorType.Energy => "\uEBAA",
SensorType.Noise => "\uE767",
SensorType.Conductivity => "\uE945",
SensorType.Humidity => "\uE9CA",
_ => "\uE9D9"
};
}
public static string GetHardwareGlyph(HardwareType hardwareType)
{
return hardwareType switch
{
HardwareType.Motherboard => "\uE950",
HardwareType.SuperIO => "\uE950",
HardwareType.Cpu => "\uE950",
HardwareType.Memory => "\uE8AB",
HardwareType.GpuNvidia => "\uE7F4",
HardwareType.GpuAmd => "\uE7F4",
HardwareType.GpuIntel => "\uE7F4",
HardwareType.Storage => "\uEDA2",
HardwareType.Network => "\uE968",
HardwareType.Cooler => "\uE9F3",
HardwareType.EmbeddedController => "\uE950",
HardwareType.Psu => "\uE7E8",
HardwareType.Battery => "\uEBAA",
_ => "\uE950"
};
}
}
@@ -0,0 +1,13 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
namespace LibreHardwareMonitor.Windows.WinUI.ViewModels;
public enum AppThemeMode
{
Auto,
Light,
Dark,
Black
}
@@ -0,0 +1,878 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using LibreHardwareMonitor.Hardware;
using LibreHardwareMonitor.Hardware.Storage;
using LibreHardwareMonitor.Windows.WinUI.Services;
using LibreHardwareMonitor.Windows.WinUI.Utilities;
using Microsoft.UI.Xaml;
using Windows.UI;
namespace LibreHardwareMonitor.Windows.WinUI.ViewModels;
public sealed class MainWindowViewModel : ViewModelBase, IDisposable
{
private static readonly TimeSpan[] LoggingIntervals =
[
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10),
TimeSpan.FromSeconds(30),
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(2),
TimeSpan.FromMinutes(5),
TimeSpan.FromMinutes(10),
TimeSpan.FromMinutes(30),
TimeSpan.FromHours(1),
TimeSpan.FromHours(2),
TimeSpan.FromHours(6)
];
private static readonly TimeSpan[] UpdateIntervals =
[
TimeSpan.FromMilliseconds(250),
TimeSpan.FromMilliseconds(500),
TimeSpan.FromSeconds(1),
TimeSpan.FromSeconds(2),
TimeSpan.FromSeconds(5),
TimeSpan.FromSeconds(10)
];
private static readonly TimeSpan[] SensorValuesTimeWindows =
[
TimeSpan.FromSeconds(30),
TimeSpan.FromMinutes(1),
TimeSpan.FromMinutes(2),
TimeSpan.FromMinutes(5),
TimeSpan.FromMinutes(10),
TimeSpan.FromMinutes(30),
TimeSpan.FromHours(1),
TimeSpan.FromHours(2),
TimeSpan.FromHours(6),
TimeSpan.FromHours(12),
TimeSpan.FromHours(24)
];
private static readonly Color[] PlotColors =
[
Color.FromArgb(255, 0x00, 0x78, 0xD4),
Color.FromArgb(255, 0xE8, 0x11, 0x23),
Color.FromArgb(255, 0x10, 0x7C, 0x10),
Color.FromArgb(255, 0xF7, 0x63, 0x0C),
Color.FromArgb(255, 0x88, 0x17, 0x98),
Color.FromArgb(255, 0x00, 0xB7, 0xC3),
Color.FromArgb(255, 0x49, 0x8B, 0x00),
Color.FromArgb(255, 0xA8, 0x00, 0x00)
];
private readonly HardwareMonitorService _hardwareMonitor;
private readonly Logger _logger;
private readonly Dictionary<string, PlotSeriesViewModel> _plotSeriesByIdentifier = new();
private readonly RemoteWebServer _remoteWebServer;
private readonly StartupService _startupService = new();
private AppThemeMode _themeMode;
private int _loggingIntervalIndex;
private PlotLocation _plotLocation;
private double _plotStrokeThickness;
private SensorTreeItemViewModel? _selectedItem;
private int _sensorValuesTimeWindowIndex;
private bool _showHiddenSensors;
private bool _showMaxColumn;
private bool _showMinColumn;
private bool _showPlot;
private bool _showValueColumn;
private bool _throttleAtaUpdate;
private bool _isStarted;
private bool _isStarting;
private string _statusText = "";
private TemperatureUnit _temperatureUnit;
private int _updateIntervalIndex;
public MainWindowViewModel(AppSettings settings)
{
Settings = settings;
_hardwareMonitor = new HardwareMonitorService(settings);
_logger = new Logger(_hardwareMonitor.Computer);
_remoteWebServer = new RemoteWebServer(
() => RootItems.FirstOrDefault(),
_hardwareMonitor.Computer,
settings.GetValue("listenerIp", "?"),
settings.GetValue("listenerPort", 8085),
settings.GetValue("authenticationEnabled", false),
settings.GetValue("authenticationUserName", ""),
settings.GetValue("authenticationPassword", ""));
_hardwareMonitor.TreeRebuilt += HardwareMonitor_TreeRebuilt;
_themeMode = ParseThemeMode(settings.GetValue("theme", "auto"));
_showHiddenSensors = settings.GetValue("hiddenMenuItem", false);
_showValueColumn = settings.GetValue("valueMenuItem", true);
_showMinColumn = settings.GetValue("minMenuItem", false);
_showMaxColumn = settings.GetValue("maxMenuItem", true);
_showPlot = settings.GetValue("plotMenuItem", false);
_plotLocation = (PlotLocation)Math.Clamp(settings.GetValue("plotLocation", 0), 0, 1);
_plotStrokeThickness = Math.Clamp(settings.GetValue("plotStroke", 1) + 1, 1, 4);
_temperatureUnit = (TemperatureUnit)Math.Clamp(settings.GetValue("TemperatureUnit", 0), 0, 1);
_updateIntervalIndex = Math.Clamp(settings.GetValue("updateIntervalMenuItem", 2), 0, UpdateIntervals.Length - 1);
_loggingIntervalIndex = Math.Clamp(settings.GetValue("loggingInterval", 0), 0, LoggingIntervals.Length - 1);
_sensorValuesTimeWindowIndex = Math.Clamp(settings.GetValue("sensorValuesTimeWindow", 10), 0, SensorValuesTimeWindows.Length - 1);
_throttleAtaUpdate = settings.GetValue("throttleAtaUpdateMenuItem", false);
StorageDevice.ThrottleInterval = _throttleAtaUpdate ? TimeSpan.FromSeconds(30) : TimeSpan.Zero;
_logger.LoggingInterval = LoggingIntervals[_loggingIntervalIndex];
_logger.FileRotationMethod = (LoggerFileRotation)Math.Clamp(settings.GetValue("logger.fileRotation", 0), 0, 1);
}
public event EventHandler? PlotInvalidated;
public Visibility MaxColumnVisibility => ShowMaxColumn ? Visibility.Visible : Visibility.Collapsed;
public Visibility MinColumnVisibility => ShowMinColumn ? Visibility.Visible : Visibility.Collapsed;
public ObservableCollection<PlotSeriesViewModel> PlotSeries { get; } = [];
public Visibility PlotVisibility => ShowPlot ? Visibility.Visible : Visibility.Collapsed;
public ObservableCollection<SensorTreeItemViewModel> RootItems { get; } = [];
public SensorTreeItemViewModel? SelectedItem
{
get => _selectedItem;
set => SetProperty(ref _selectedItem, value);
}
public AppSettings Settings { get; }
public string StatusText
{
get => _statusText;
private set => SetProperty(ref _statusText, value);
}
public AppThemeMode ThemeMode
{
get => _themeMode;
set
{
if (!SetProperty(ref _themeMode, value))
return;
Settings.SetValue("theme", FormatThemeMode(value));
}
}
public TimeSpan UpdateInterval => UpdateIntervals[UpdateIntervalIndex];
public Visibility ValueColumnVisibility => ShowValueColumn ? Visibility.Visible : Visibility.Collapsed;
public bool AuthWebServer
{
get => _remoteWebServer.AuthEnabled;
set
{
if (_remoteWebServer.AuthEnabled == value)
return;
_remoteWebServer.AuthEnabled = value;
Settings.SetValue("authenticationEnabled", value);
RestartWebServerIfRunning();
OnPropertyChanged();
}
}
public string AuthWebServerUserName
{
get => _remoteWebServer.UserName;
set
{
if (_remoteWebServer.UserName == value)
return;
_remoteWebServer.UserName = value;
Settings.SetValue("authenticationUserName", value);
RestartWebServerIfRunning();
OnPropertyChanged();
}
}
public LoggerFileRotation FileRotationMethod
{
get => _logger.FileRotationMethod;
set
{
if (_logger.FileRotationMethod == value)
return;
_logger.FileRotationMethod = value;
Settings.SetValue("logger.fileRotation", (int)value);
OnPropertyChanged();
}
}
public bool ForceDriveWakeup
{
get => _hardwareMonitor.ForceDriveWakeup;
set
{
if (_hardwareMonitor.ForceDriveWakeup == value)
return;
_hardwareMonitor.ForceDriveWakeup = value;
OnPropertyChanged();
}
}
public bool IsBatteryEnabled
{
get => _hardwareMonitor.IsBatteryEnabled;
set => SetHardwareFlag(value, _hardwareMonitor.IsBatteryEnabled, v => _hardwareMonitor.IsBatteryEnabled = v);
}
public bool IsControllerEnabled
{
get => _hardwareMonitor.IsControllerEnabled;
set => SetHardwareFlag(value, _hardwareMonitor.IsControllerEnabled, v => _hardwareMonitor.IsControllerEnabled = v);
}
public bool IsCpuEnabled
{
get => _hardwareMonitor.IsCpuEnabled;
set => SetHardwareFlag(value, _hardwareMonitor.IsCpuEnabled, v => _hardwareMonitor.IsCpuEnabled = v);
}
public bool IsGpuEnabled
{
get => _hardwareMonitor.IsGpuEnabled;
set => SetHardwareFlag(value, _hardwareMonitor.IsGpuEnabled, v => _hardwareMonitor.IsGpuEnabled = v);
}
public bool IsMemoryEnabled
{
get => _hardwareMonitor.IsMemoryEnabled;
set => SetHardwareFlag(value, _hardwareMonitor.IsMemoryEnabled, v => _hardwareMonitor.IsMemoryEnabled = v);
}
public bool IsMotherboardEnabled
{
get => _hardwareMonitor.IsMotherboardEnabled;
set => SetHardwareFlag(value, _hardwareMonitor.IsMotherboardEnabled, v => _hardwareMonitor.IsMotherboardEnabled = v);
}
public bool IsNetworkEnabled
{
get => _hardwareMonitor.IsNetworkEnabled;
set => SetHardwareFlag(value, _hardwareMonitor.IsNetworkEnabled, v => _hardwareMonitor.IsNetworkEnabled = v);
}
public bool IsPowerMonitorEnabled
{
get => _hardwareMonitor.IsPowerMonitorEnabled;
set => SetHardwareFlag(value, _hardwareMonitor.IsPowerMonitorEnabled, v => _hardwareMonitor.IsPowerMonitorEnabled = v);
}
public bool IsPsuEnabled
{
get => _hardwareMonitor.IsPsuEnabled;
set => SetHardwareFlag(value, _hardwareMonitor.IsPsuEnabled, v => _hardwareMonitor.IsPsuEnabled = v);
}
public bool IsStorageEnabled
{
get => _hardwareMonitor.IsStorageEnabled;
set => SetHardwareFlag(value, _hardwareMonitor.IsStorageEnabled, v => _hardwareMonitor.IsStorageEnabled = v);
}
public int LoggingIntervalIndex
{
get => _loggingIntervalIndex;
set
{
value = Math.Clamp(value, 0, LoggingIntervals.Length - 1);
if (!SetProperty(ref _loggingIntervalIndex, value))
return;
Settings.SetValue("loggingInterval", value);
_logger.LoggingInterval = LoggingIntervals[value];
}
}
public string ListenerIp
{
get => _remoteWebServer.ListenerIp;
set
{
if (_remoteWebServer.ListenerIp == value)
return;
_remoteWebServer.ListenerIp = value;
Settings.SetValue("listenerIp", value);
RestartWebServerIfRunning();
OnPropertyChanged();
OnPropertyChanged(nameof(WebServerUrl));
}
}
public int ListenerPort
{
get => _remoteWebServer.ListenerPort;
set
{
value = Math.Clamp(value, 1, 65535);
if (_remoteWebServer.ListenerPort == value)
return;
_remoteWebServer.ListenerPort = value;
Settings.SetValue("listenerPort", value);
RestartWebServerIfRunning();
OnPropertyChanged();
OnPropertyChanged(nameof(WebServerUrl));
}
}
public bool LogSensors
{
get => Settings.GetValue("logSensorsMenuItem", false);
set
{
if (LogSensors == value)
return;
Settings.SetValue("logSensorsMenuItem", value);
OnPropertyChanged();
}
}
public bool RunWebServer
{
get => !IsWebServerUnavailable && Settings.GetValue("runWebServerMenuItem", false);
set
{
if (IsWebServerUnavailable || RunWebServer == value)
return;
if (value)
{
if (!_remoteWebServer.Start())
{
Settings.SetValue("runWebServerMenuItem", false);
StatusText = "Remote web server failed to start.";
OnPropertyChanged();
return;
}
}
else
{
_remoteWebServer.Stop();
}
Settings.SetValue("runWebServerMenuItem", value);
OnPropertyChanged();
UpdateStatus();
}
}
public int SensorValuesTimeWindowIndex
{
get => _sensorValuesTimeWindowIndex;
set
{
value = Math.Clamp(value, 0, SensorValuesTimeWindows.Length - 1);
if (!SetProperty(ref _sensorValuesTimeWindowIndex, value))
return;
Settings.SetValue("sensorValuesTimeWindow", value);
ApplySensorValuesTimeWindow();
}
}
public bool MinimizeOnClose
{
get => Settings.GetValue("minCloseMenuItem", false);
set => SetBooleanSetting("minCloseMenuItem", value);
}
public bool MinimizeToTray
{
get => Settings.GetValue("minTrayMenuItem", true);
set => SetBooleanSetting("minTrayMenuItem", value);
}
public PlotLocation PlotLocation
{
get => _plotLocation;
set
{
if (!SetProperty(ref _plotLocation, value))
return;
Settings.SetValue("plotLocation", (int)value);
NotifyPlotLayoutChanged();
}
}
public int PlotGridColumn => PlotLocation == PlotLocation.Right ? 1 : 0;
public int PlotGridColumnSpan => PlotLocation == PlotLocation.Bottom ? 2 : 1;
public int PlotGridRow => PlotLocation == PlotLocation.Bottom ? 1 : 0;
public int PlotGridRowSpan => PlotLocation == PlotLocation.Right ? 2 : 1;
public double PlotStrokeThickness
{
get => _plotStrokeThickness;
set
{
value = Math.Clamp(value, 1, 4);
if (!SetProperty(ref _plotStrokeThickness, value))
return;
Settings.SetValue("plotStroke", (int)value - 1);
PlotInvalidated?.Invoke(this, EventArgs.Empty);
}
}
public bool ShowHiddenSensors
{
get => _showHiddenSensors;
set
{
if (!SetProperty(ref _showHiddenSensors, value))
return;
Settings.SetValue("hiddenMenuItem", value);
RootItems.FirstOrDefault()?.SetShowHiddenSensors(value);
}
}
public bool ShowMaxColumn
{
get => _showMaxColumn;
set
{
if (!SetProperty(ref _showMaxColumn, value))
return;
Settings.SetValue("maxMenuItem", value);
NotifyColumnVisibilityChanged();
}
}
public bool ShowMinColumn
{
get => _showMinColumn;
set
{
if (!SetProperty(ref _showMinColumn, value))
return;
Settings.SetValue("minMenuItem", value);
NotifyColumnVisibilityChanged();
}
}
public bool ShowPlot
{
get => _showPlot;
set
{
if (!SetProperty(ref _showPlot, value))
return;
Settings.SetValue("plotMenuItem", value);
OnPropertyChanged(nameof(PlotVisibility));
NotifyPlotLayoutChanged();
PlotInvalidated?.Invoke(this, EventArgs.Empty);
}
}
public bool ShowValueColumn
{
get => _showValueColumn;
set
{
if (!SetProperty(ref _showValueColumn, value))
return;
Settings.SetValue("valueMenuItem", value);
NotifyColumnVisibilityChanged();
}
}
public bool StartMinimized
{
get => Settings.GetValue("startMinMenuItem", false);
set => SetBooleanSetting("startMinMenuItem", value);
}
public bool ThrottleAtaUpdate
{
get => _throttleAtaUpdate;
set
{
if (!SetProperty(ref _throttleAtaUpdate, value))
return;
Settings.SetValue("throttleAtaUpdateMenuItem", value);
StorageDevice.ThrottleInterval = value ? TimeSpan.FromSeconds(30) : TimeSpan.Zero;
}
}
public bool IsWebServerUnavailable => _remoteWebServer.PlatformNotSupported;
public string WebServerUrl
{
get
{
string host = ListenerIp is "?" or "+" or "*" or "0.0.0.0" ? "localhost" : ListenerIp;
return $"http://{host}:{ListenerPort}/";
}
}
public bool RunOnStartup
{
get => _startupService.IsAvailable && _startupService.Startup;
set
{
if (!_startupService.IsAvailable || RunOnStartup == value)
return;
try
{
_startupService.Startup = value;
OnPropertyChanged();
}
catch (InvalidOperationException)
{
OnPropertyChanged();
}
}
}
public int SensorGridColumnSpan => ShowPlot && PlotLocation == PlotLocation.Right ? 1 : 2;
public int SensorGridRowSpan => ShowPlot && PlotLocation == PlotLocation.Bottom ? 1 : 2;
public TemperatureUnit TemperatureUnit
{
get => _temperatureUnit;
set
{
if (!SetProperty(ref _temperatureUnit, value))
return;
Settings.SetValue("TemperatureUnit", (int)value);
RootItems.FirstOrDefault()?.SetTemperatureUnit(value);
PlotInvalidated?.Invoke(this, EventArgs.Empty);
}
}
public int UpdateIntervalIndex
{
get => _updateIntervalIndex;
set
{
value = Math.Clamp(value, 0, UpdateIntervals.Length - 1);
if (!SetProperty(ref _updateIntervalIndex, value))
return;
Settings.SetValue("updateIntervalMenuItem", value);
OnPropertyChanged(nameof(UpdateInterval));
}
}
public void Dispose()
{
_hardwareMonitor.TreeRebuilt -= HardwareMonitor_TreeRebuilt;
Settings.SetValue("listenerIp", ListenerIp);
Settings.SetValue("listenerPort", ListenerPort);
Settings.SetValue("authenticationEnabled", AuthWebServer);
Settings.SetValue("authenticationUserName", AuthWebServerUserName);
Settings.SetValue("authenticationPassword", _remoteWebServer.PasswordSHA256);
_remoteWebServer.Quit();
_hardwareMonitor.Dispose();
Save();
}
public string GetReport()
{
return _hardwareMonitor.Computer.GetReport();
}
public void ResetHardware()
{
_hardwareMonitor.Reset();
UpdateStatus();
}
public void ResetMinMax()
{
_hardwareMonitor.ResetMinMax();
RootItems.FirstOrDefault()?.RefreshValues();
}
public void ResetPlot()
{
_hardwareMonitor.ClearSensorValues();
_plotSeriesByIdentifier.Clear();
PlotSeries.Clear();
PlotInvalidated?.Invoke(this, EventArgs.Empty);
}
public void Save()
{
Settings.Save();
}
public void SetStatusText(string statusText)
{
StatusText = statusText;
}
public void SetAllExpanded(bool isExpanded)
{
RootItems.FirstOrDefault()?.SetAllExpanded(isExpanded);
}
public void SetAuthPassword(string plainPassword)
{
if (string.IsNullOrWhiteSpace(plainPassword))
return;
_remoteWebServer.SetPassword(plainPassword);
Settings.SetValue("authenticationPassword", _remoteWebServer.PasswordSHA256);
RestartWebServerIfRunning();
}
public void SetSensorPenColor(SensorTreeItemViewModel item, Color? color)
{
if (item.Sensor == null)
return;
item.PenColor = color;
string identifier = item.Sensor.Identifier.ToString();
if (_plotSeriesByIdentifier.TryGetValue(identifier, out PlotSeriesViewModel? series))
series.Color = GetPlotColor(item);
PlotInvalidated?.Invoke(this, EventArgs.Empty);
}
public async Task StartAsync()
{
if (_isStarted || _isStarting)
return;
_isStarting = true;
try
{
StatusText = "Initializing hardware sensors...";
await Task.Run(() => _hardwareMonitor.Open(raiseTreeRebuilt: false));
UpdateRoot();
ApplySensorValuesTimeWindow();
StartWebServerFromSettings();
UpdateStatus();
_isStarted = true;
}
finally
{
_isStarting = false;
}
}
public async Task UpdateAsync()
{
if (!_isStarted)
return;
await _hardwareMonitor.UpdateAsync();
SensorTreeItemViewModel? root = RootItems.FirstOrDefault();
root?.RefreshValues();
TrackPlotPoints();
if (LogSensors)
_logger.Log();
UpdateStatus();
}
private void HardwareMonitor_TreeRebuilt(object? sender, EventArgs e)
{
UpdateRoot();
}
private void ApplySensorValuesTimeWindow()
{
TimeSpan timeWindow = SensorValuesTimeWindows[SensorValuesTimeWindowIndex];
SensorVisitor visitor = new(sensor => sensor.ValuesTimeWindow = timeWindow);
visitor.VisitComputer(_hardwareMonitor.Computer);
}
private void NotifyColumnVisibilityChanged()
{
OnPropertyChanged(nameof(ValueColumnVisibility));
OnPropertyChanged(nameof(MinColumnVisibility));
OnPropertyChanged(nameof(MaxColumnVisibility));
RootItems.FirstOrDefault()?.SetColumnVisibility(ShowValueColumn, ShowMinColumn, ShowMaxColumn);
}
private void NotifyPlotLayoutChanged()
{
OnPropertyChanged(nameof(PlotGridRow));
OnPropertyChanged(nameof(PlotGridColumn));
OnPropertyChanged(nameof(PlotGridRowSpan));
OnPropertyChanged(nameof(PlotGridColumnSpan));
OnPropertyChanged(nameof(SensorGridRowSpan));
OnPropertyChanged(nameof(SensorGridColumnSpan));
}
private void SetBooleanSetting(string settingName, bool value)
{
if (Settings.GetValue(settingName, false) == value)
return;
Settings.SetValue(settingName, value);
OnPropertyChanged();
}
private void SetHardwareFlag(bool value, bool currentValue, Action<bool> setValue)
{
if (currentValue == value)
return;
setValue(value);
UpdateRoot();
OnPropertyChanged();
}
private void TrackPlotPoints()
{
SensorTreeItemViewModel? root = RootItems.FirstOrDefault();
if (root == null)
return;
DateTime now = DateTime.Now;
HashSet<string> selectedIdentifiers = new();
foreach (SensorTreeItemViewModel sensorItem in root.EnumerateSensors().Where(sensorItem => sensorItem.Plot && sensorItem.Sensor != null))
{
ISensor sensor = sensorItem.Sensor!;
string identifier = sensor.Identifier.ToString();
selectedIdentifiers.Add(identifier);
double? value = SensorFormatter.GetPlotValue(sensor, TemperatureUnit);
if (!value.HasValue)
continue;
if (!_plotSeriesByIdentifier.TryGetValue(identifier, out PlotSeriesViewModel? series))
{
series = new PlotSeriesViewModel(identifier, sensor.Name, GetPlotColor(sensorItem));
_plotSeriesByIdentifier[identifier] = series;
PlotSeries.Add(series);
}
else
{
series.Color = GetPlotColor(sensorItem);
}
series.Points.Add(new PlotPointViewModel(now, value.Value));
while (series.Points.Count > 0 && now - series.Points[0].Timestamp > TimeSpan.FromHours(24))
series.Points.RemoveAt(0);
}
foreach (string identifier in _plotSeriesByIdentifier.Keys.Where(identifier => !selectedIdentifiers.Contains(identifier)).ToArray())
{
PlotSeriesViewModel series = _plotSeriesByIdentifier[identifier];
_plotSeriesByIdentifier.Remove(identifier);
PlotSeries.Remove(series);
}
}
private void UpdateRoot()
{
RootItems.Clear();
SensorTreeItemViewModel root = _hardwareMonitor.Root;
root.Configure(TemperatureUnit, ShowHiddenSensors, ShowValueColumn, ShowMinColumn, ShowMaxColumn);
RootItems.Add(root);
ApplySensorValuesTimeWindow();
}
private void UpdateStatus()
{
int hardwareCount = _hardwareMonitor.Computer.Hardware.Count;
int sensorCount = RootItems.FirstOrDefault()?.EnumerateSensors().Count() ?? 0;
string webServerStatus = RunWebServer ? $", web server {WebServerUrl}" : "";
StatusText = $"{hardwareCount} hardware devices, {sensorCount} sensors, update every {FormatInterval(UpdateInterval)}{webServerStatus}";
}
private Color GetPlotColor(SensorTreeItemViewModel sensorItem)
{
return sensorItem.PenColor ?? PlotColors[_plotSeriesByIdentifier.Count % PlotColors.Length];
}
private void RestartWebServerIfRunning()
{
if (!RunWebServer)
return;
_remoteWebServer.Stop();
if (!_remoteWebServer.Start())
{
Settings.SetValue("runWebServerMenuItem", false);
StatusText = "Remote web server failed to restart.";
}
OnPropertyChanged(nameof(RunWebServer));
UpdateStatus();
}
private void StartWebServerFromSettings()
{
if (IsWebServerUnavailable || !Settings.GetValue("runWebServerMenuItem", false))
return;
if (!_remoteWebServer.Start())
{
Settings.SetValue("runWebServerMenuItem", false);
StatusText = "Remote web server failed to start.";
}
OnPropertyChanged(nameof(RunWebServer));
}
private static AppThemeMode ParseThemeMode(string value)
{
return value.ToLowerInvariant() switch
{
"light" => AppThemeMode.Light,
"dark" => AppThemeMode.Dark,
"black" => AppThemeMode.Black,
_ => AppThemeMode.Auto
};
}
private static string FormatThemeMode(AppThemeMode themeMode)
{
return themeMode switch
{
AppThemeMode.Light => "light",
AppThemeMode.Dark => "dark",
AppThemeMode.Black => "black",
_ => "auto"
};
}
private static string FormatInterval(TimeSpan interval)
{
if (interval.TotalMilliseconds < 1000)
return $"{interval.TotalMilliseconds:F0}ms";
if (interval.TotalSeconds < 60)
return $"{interval.TotalSeconds:F0}s";
if (interval.TotalMinutes < 60)
return $"{interval.TotalMinutes:F0}min";
return $"{interval.TotalHours:F0}h";
}
}
@@ -0,0 +1,11 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
namespace LibreHardwareMonitor.Windows.WinUI.ViewModels;
public enum PlotLocation
{
Bottom = 0,
Right = 1
}
@@ -0,0 +1,20 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System;
namespace LibreHardwareMonitor.Windows.WinUI.ViewModels;
public sealed class PlotPointViewModel
{
public PlotPointViewModel(DateTime timestamp, double value)
{
Timestamp = timestamp;
Value = value;
}
public DateTime Timestamp { get; }
public double Value { get; }
}
@@ -0,0 +1,26 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System.Collections.ObjectModel;
using Windows.UI;
namespace LibreHardwareMonitor.Windows.WinUI.ViewModels;
public sealed class PlotSeriesViewModel
{
public PlotSeriesViewModel(string sensorIdentifier, string name, Color color)
{
SensorIdentifier = sensorIdentifier;
Name = name;
Color = color;
}
public Color Color { get; set; }
public string Name { get; }
public ObservableCollection<PlotPointViewModel> Points { get; } = [];
public string SensorIdentifier { get; }
}
@@ -0,0 +1,13 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
namespace LibreHardwareMonitor.Windows.WinUI.ViewModels;
public enum SensorTreeItemKind
{
Root,
Hardware,
SensorType,
Sensor
}
@@ -0,0 +1,347 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Text;
using LibreHardwareMonitor.Hardware;
using LibreHardwareMonitor.Windows.WinUI.Services;
using LibreHardwareMonitor.Windows.WinUI.Utilities;
using Microsoft.UI.Xaml;
using Windows.UI;
namespace LibreHardwareMonitor.Windows.WinUI.ViewModels;
public sealed class SensorTreeItemViewModel : ViewModelBase
{
private readonly AppSettings? _settings;
private string? _expandedSettingName;
private bool _isExpanded = true;
private bool _isVisible = true;
private bool _plot;
private bool _showHiddenSensors;
private Visibility _maxColumnVisibility = Visibility.Visible;
private Visibility _minColumnVisibility = Visibility.Collapsed;
private Visibility _rowVisibility = Visibility.Visible;
private Visibility _valueColumnVisibility = Visibility.Visible;
private Color? _penColor;
private TemperatureUnit _temperatureUnit;
private SensorTreeItemViewModel(SensorTreeItemKind kind, AppSettings? settings)
{
Kind = kind;
_settings = settings;
}
public ObservableCollection<SensorTreeItemViewModel> Children { get; } = [];
public bool CanHide => Sensor != null;
public bool CanRename => Sensor != null || Hardware != null;
public IHardware? Hardware { get; private init; }
public string IconGlyph { get; private init; } = "\uE950";
public bool IsExpanded
{
get => _isExpanded;
set
{
if (SetProperty(ref _isExpanded, value) && _expandedSettingName != null)
_settings?.SetValue(_expandedSettingName, value);
}
}
public bool IsVisible
{
get => _isVisible;
set
{
if (!SetProperty(ref _isVisible, value))
return;
if (Sensor != null)
_settings?.SetValue(new Identifier(Sensor.Identifier, "hidden").ToString(), !value);
UpdateVisibilityState();
}
}
public SensorTreeItemKind Kind { get; }
public string Max => Sensor == null ? "" : SensorFormatter.FormatValue(Sensor, Sensor.Max, TemperatureUnit);
public Visibility MaxColumnVisibility
{
get => _maxColumnVisibility;
private set => SetProperty(ref _maxColumnVisibility, value);
}
public string Min => Sensor == null ? "" : SensorFormatter.FormatValue(Sensor, Sensor.Min, TemperatureUnit);
public Visibility MinColumnVisibility
{
get => _minColumnVisibility;
private set => SetProperty(ref _minColumnVisibility, value);
}
public bool Plot
{
get => _plot;
set
{
if (Sensor == null || !SetProperty(ref _plot, value))
return;
_settings?.SetValue(new Identifier(Sensor.Identifier, "plot").ToString(), value);
}
}
public Color? PenColor
{
get => _penColor;
set
{
if (Sensor == null || Nullable.Equals(_penColor, value))
return;
_penColor = value;
string settingName = new Identifier(Sensor.Identifier, "penColor").ToString();
if (value.HasValue)
_settings?.SetValue(settingName, value.Value);
else
_settings?.Remove(settingName);
OnPropertyChanged();
}
}
public Visibility PlotCheckVisibility => Sensor == null ? Visibility.Collapsed : Visibility.Visible;
public Visibility RowVisibility
{
get => _rowVisibility;
private set => SetProperty(ref _rowVisibility, value);
}
public ISensor? Sensor { get; private init; }
public string Text
{
get
{
if (Sensor != null)
return Sensor.Name;
if (Hardware != null)
return Hardware.Name;
return _text;
}
set
{
if (Sensor != null)
Sensor.Name = value;
else if (Hardware != null)
Hardware.Name = value;
else
_text = value;
OnPropertyChanged();
}
}
public string ToolTip
{
get
{
if (Sensor != null)
return SensorFormatter.GetToolTip(Sensor, TemperatureUnit);
if (Hardware?.Properties.Count > 0)
{
StringBuilder builder = new();
builder.AppendLine("Hardware properties:");
foreach (KeyValuePair<string, string> property in Hardware.Properties)
builder.AppendFormat(" - {0}: {1}{2}", property.Key, property.Value, Environment.NewLine);
return builder.ToString();
}
return "";
}
}
public TemperatureUnit TemperatureUnit
{
get => _temperatureUnit;
private set
{
if (!SetProperty(ref _temperatureUnit, value))
return;
RefreshValues();
}
}
public string Value => Sensor == null ? "" : SensorFormatter.FormatValue(Sensor, Sensor.Value, TemperatureUnit);
public Visibility ValueColumnVisibility
{
get => _valueColumnVisibility;
private set => SetProperty(ref _valueColumnVisibility, value);
}
private string _text = "";
public static SensorTreeItemViewModel CreateRoot(string text)
{
return new SensorTreeItemViewModel(SensorTreeItemKind.Root, null)
{
_text = text,
IconGlyph = "\uE7F4",
_isExpanded = true
};
}
public static SensorTreeItemViewModel FromHardware(IHardware hardware, AppSettings settings)
{
string expandedSettingName = new Identifier(hardware.Identifier, "expanded").ToString();
SensorTreeItemViewModel item = new(SensorTreeItemKind.Hardware, settings)
{
Hardware = hardware,
IconGlyph = SensorTypeDisplay.GetHardwareGlyph(hardware.HardwareType),
_expandedSettingName = expandedSettingName,
_isExpanded = settings.GetValue(expandedSettingName, true)
};
foreach (SensorType sensorType in Enum.GetValues<SensorType>())
{
ISensor[] sensors = hardware.Sensors
.Where(sensor => sensor.SensorType == sensorType)
.OrderBy(sensor => sensor.Index)
.ToArray();
if (sensors.Length == 0)
continue;
item.Children.Add(FromSensorType(hardware.Identifier, sensorType, sensors, settings));
}
foreach (IHardware subHardware in hardware.SubHardware.OrderBy(subHardware => subHardware.HardwareType).ThenBy(subHardware => subHardware.Name, StringComparer.OrdinalIgnoreCase))
item.Children.Add(FromHardware(subHardware, settings));
item.UpdateVisibilityState();
return item;
}
public void Configure(TemperatureUnit temperatureUnit, bool showHiddenSensors, bool showValueColumn, bool showMinColumn, bool showMaxColumn)
{
SetTemperatureUnit(temperatureUnit);
SetShowHiddenSensors(showHiddenSensors);
SetColumnVisibility(showValueColumn, showMinColumn, showMaxColumn);
}
public IEnumerable<SensorTreeItemViewModel> Enumerate()
{
yield return this;
foreach (SensorTreeItemViewModel child in Children)
{
foreach (SensorTreeItemViewModel node in child.Enumerate())
yield return node;
}
}
public IEnumerable<SensorTreeItemViewModel> EnumerateSensors()
{
return Enumerate().Where(item => item.Sensor != null);
}
public void RefreshValues()
{
OnPropertyChanged(nameof(Value));
OnPropertyChanged(nameof(Min));
OnPropertyChanged(nameof(Max));
OnPropertyChanged(nameof(ToolTip));
foreach (SensorTreeItemViewModel child in Children)
child.RefreshValues();
}
public void SetAllExpanded(bool isExpanded)
{
IsExpanded = isExpanded;
foreach (SensorTreeItemViewModel child in Children)
child.SetAllExpanded(isExpanded);
}
public void SetColumnVisibility(bool showValueColumn, bool showMinColumn, bool showMaxColumn)
{
ValueColumnVisibility = showValueColumn ? Visibility.Visible : Visibility.Collapsed;
MinColumnVisibility = showMinColumn ? Visibility.Visible : Visibility.Collapsed;
MaxColumnVisibility = showMaxColumn ? Visibility.Visible : Visibility.Collapsed;
foreach (SensorTreeItemViewModel child in Children)
child.SetColumnVisibility(showValueColumn, showMinColumn, showMaxColumn);
}
public void SetShowHiddenSensors(bool showHiddenSensors)
{
_showHiddenSensors = showHiddenSensors;
UpdateVisibilityState();
foreach (SensorTreeItemViewModel child in Children)
child.SetShowHiddenSensors(showHiddenSensors);
}
public void SetTemperatureUnit(TemperatureUnit temperatureUnit)
{
TemperatureUnit = temperatureUnit;
foreach (SensorTreeItemViewModel child in Children)
child.SetTemperatureUnit(temperatureUnit);
}
private static SensorTreeItemViewModel FromSensor(ISensor sensor, AppSettings settings)
{
string hiddenSettingName = new Identifier(sensor.Identifier, "hidden").ToString();
string plotSettingName = new Identifier(sensor.Identifier, "plot").ToString();
string penColorSettingName = new Identifier(sensor.Identifier, "penColor").ToString();
return new SensorTreeItemViewModel(SensorTreeItemKind.Sensor, settings)
{
Sensor = sensor,
IconGlyph = SensorTypeDisplay.GetGlyph(sensor.SensorType),
_isVisible = !settings.GetValue(hiddenSettingName, sensor.IsDefaultHidden),
_plot = settings.GetValue(plotSettingName, false),
_penColor = settings.Contains(penColorSettingName) ? settings.GetValue(penColorSettingName, Color.FromArgb(255, 0, 0, 0)) : null
};
}
private static SensorTreeItemViewModel FromSensorType(Identifier parentIdentifier, SensorType sensorType, IEnumerable<ISensor> sensors, AppSettings settings)
{
string expandedSettingName = new Identifier(parentIdentifier, sensorType.ToString(), ".expanded").ToString();
SensorTreeItemViewModel item = new(SensorTreeItemKind.SensorType, settings)
{
_text = SensorTypeDisplay.GetText(sensorType),
IconGlyph = SensorTypeDisplay.GetGlyph(sensorType),
_expandedSettingName = expandedSettingName,
_isExpanded = settings.GetValue(expandedSettingName, true)
};
foreach (ISensor sensor in sensors)
item.Children.Add(FromSensor(sensor, settings));
item.UpdateVisibilityState();
return item;
}
private void UpdateVisibilityState()
{
bool visible = Kind switch
{
SensorTreeItemKind.Sensor => _showHiddenSensors || IsVisible,
SensorTreeItemKind.SensorType => _showHiddenSensors || Children.Any(child => child.IsVisible),
_ => true
};
RowVisibility = visible ? Visibility.Visible : Visibility.Collapsed;
}
}
@@ -0,0 +1,11 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
namespace LibreHardwareMonitor.Windows.WinUI.ViewModels;
public enum TemperatureUnit
{
Celsius = 0,
Fahrenheit = 1
}
@@ -0,0 +1,29 @@
// This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
// If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Copyright (C) LibreHardwareMonitor and Contributors.
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
namespace LibreHardwareMonitor.Windows.WinUI.ViewModels;
public abstract class ViewModelBase : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected bool SetProperty<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
OnPropertyChanged(propertyName);
return true;
}
protected void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1"
xmlns:asmv1="urn:schemas-microsoft-com:asm.v1"
xmlns:asmv2="urn:schemas-microsoft-com:asm.v2"
xmlns:asmv3="urn:schemas-microsoft-com:asm.v3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
<security>
<requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
<requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
</requestedPrivileges>
</security>
</trustInfo>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>
</application>
</compatibility>
<asmv3:application>
<asmv3:windowsSettings>
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware>
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
</asmv3:windowsSettings>
</asmv3:application>
</asmv1:assembly>