Files
ReLibreHardwareMonitor/LibreHardwareMonitor.Windows.WinUI/ViewModels/MainWindowViewModel.cs
T
2026-06-01 18:45:00 -05:00

1239 lines
40 KiB
C#

// 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;
using System.Threading.Tasks;
using LibreHardwareMonitor.Hardware;
using LibreHardwareMonitor.Hardware.Storage;
using LibreHardwareMonitor.Windows.WinUI.Services;
using LibreHardwareMonitor.Windows.WinUI.Utilities;
using Microsoft.UI.Dispatching;
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 const int DefaultPlotTimeWindowIndex = 2;
private static readonly TimeSpan MaximumPlotPointRetention = TimeSpan.FromHours(24);
private static readonly TimeSpan MaximumSyntheticPlotPointRetention = TimeSpan.FromMinutes(5);
private static readonly TimeSpan?[] PlotTimeWindows =
[
null,
TimeSpan.FromMinutes(5),
TimeSpan.FromMinutes(10),
TimeSpan.FromMinutes(20),
TimeSpan.FromMinutes(30),
TimeSpan.FromMinutes(45),
TimeSpan.FromHours(1),
TimeSpan.FromMinutes(90),
TimeSpan.FromHours(2),
TimeSpan.FromHours(3),
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 DispatcherQueue _dispatcherQueue;
private readonly Logger _logger;
private readonly Dictionary<string, PlotSeriesViewModel> _plotSeriesByIdentifier = new();
private readonly RemoteWebServer _remoteWebServer;
private readonly StartupService _startupService = new();
private readonly WinUiStartupTrace? _startupTrace;
private AppThemeMode _themeMode;
private int _loggingIntervalIndex;
private PlotLocation _plotLocation;
private bool _plotStackedAxes;
private double _plotStrokeThickness;
private bool _plotTimeAxisZoomEnabled;
private int _plotTimeWindowIndex;
private bool _plotValueAxesZoomEnabled;
private SensorTreeItemViewModel? _selectedItem;
private int _sensorValuesTimeWindowIndex;
private bool _showHiddenSensors;
private bool _showMaxColumn;
private bool _showMinColumn;
private bool _showPlotAxisLabels;
private bool _showPlot;
private bool _showValueColumn;
private bool _throttleAtaUpdate;
private bool _isHardwareLoading = true;
private bool _isStarted;
private bool _isStarting;
private int _rootUpdateQueued;
private string _statusText = "";
private TemperatureUnit _temperatureUnit;
private int _updateIntervalIndex;
public MainWindowViewModel(AppSettings settings) : this(settings, null)
{
}
internal MainWindowViewModel(AppSettings settings, WinUiStartupTrace? startupTrace)
{
Settings = settings;
_startupTrace = startupTrace;
_startupTrace?.Mark("MainWindowViewModel.Constructor.Begin");
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
_hardwareMonitor = new HardwareMonitorService(settings);
_logger = new Logger(_hardwareMonitor.Computer);
_remoteWebServer = new RemoteWebServer(
() => RootItems.FirstOrDefault(),
_hardwareMonitor.Computer,
_hardwareMonitor.SensorReadLock,
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, 2);
_plotStrokeThickness = Math.Clamp(settings.GetValue("plotStroke", 1) + 1, 1, 4);
_plotStackedAxes = settings.GetValue("stackedAxes", true);
_showPlotAxisLabels = settings.GetValue("showAxesLabels", true);
_plotTimeAxisZoomEnabled = settings.GetValue("timeAxisEnableZoom", true);
_plotValueAxesZoomEnabled = settings.GetValue("yAxesEnableZoom", true);
int plotTimeWindowDefault = GetPlotTimeWindowIndex(settings.GetValue("plotPanel.MaxTimeSpan", 10.0f * 60));
_plotTimeWindowIndex = Math.Clamp(settings.GetValue("plotTimeWindow", plotTimeWindowDefault), 0, PlotTimeWindows.Length - 1);
_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);
_startupTrace?.Mark("MainWindowViewModel.Constructor.Complete");
}
public event EventHandler? PlotInvalidated;
public event EventHandler? GadgetSensorsChanged;
public event EventHandler? TraySensorsChanged;
public Visibility MaxColumnVisibility => ShowMaxColumn ? Visibility.Visible : Visibility.Collapsed;
public Visibility MinColumnVisibility => ShowMinColumn ? Visibility.Visible : Visibility.Collapsed;
public ObservableCollection<PlotSeriesViewModel> PlotSeries { get; } = [];
public bool IsPlotWindowVisible => ShowPlot && PlotLocation == PlotLocation.Window;
public Visibility PlotVisibility => ShowPlot && PlotLocation != PlotLocation.Window ? 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 ShowGadget
{
get => Settings.GetValue("gadgetMenuItem", false);
set => SetBooleanSetting("gadgetMenuItem", value);
}
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();
PlotInvalidated?.Invoke(this, EventArgs.Empty);
}
}
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 bool PlotStackedAxes
{
get => _plotStackedAxes;
set
{
if (!SetProperty(ref _plotStackedAxes, value))
return;
Settings.SetValue("stackedAxes", value);
PlotInvalidated?.Invoke(this, EventArgs.Empty);
}
}
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 TimeSpan? PlotTimeWindow => PlotTimeWindows[PlotTimeWindowIndex];
public bool PlotTimeAxisZoomEnabled
{
get => _plotTimeAxisZoomEnabled;
set
{
if (!SetProperty(ref _plotTimeAxisZoomEnabled, value))
return;
Settings.SetValue("timeAxisEnableZoom", value);
}
}
public int PlotTimeWindowIndex
{
get => _plotTimeWindowIndex;
set
{
value = Math.Clamp(value, 0, PlotTimeWindows.Length - 1);
if (!SetProperty(ref _plotTimeWindowIndex, value))
return;
Settings.SetValue("plotTimeWindow", value);
Settings.SetValue("plotPanel.MinTimeSpan", 0.0f);
Settings.SetValue("plotPanel.MaxTimeSpan", PlotTimeWindow.HasValue ? (float)PlotTimeWindow.Value.TotalSeconds : float.NaN);
OnPropertyChanged(nameof(PlotTimeWindow));
PlotInvalidated?.Invoke(this, EventArgs.Empty);
}
}
public bool PlotValueAxesZoomEnabled
{
get => _plotValueAxesZoomEnabled;
set
{
if (!SetProperty(ref _plotValueAxesZoomEnabled, value))
return;
Settings.SetValue("yAxesEnableZoom", value);
}
}
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 ShowPlotAxisLabels
{
get => _showPlotAxisLabels;
set
{
if (!SetProperty(ref _showPlotAxisLabels, value))
return;
Settings.SetValue("showAxesLabels", value);
PlotInvalidated?.Invoke(this, EventArgs.Empty);
}
}
public bool ShowPlot
{
get => _showPlot;
set
{
if (!SetProperty(ref _showPlot, value))
return;
Settings.SetValue("plotMenuItem", value);
OnPropertyChanged(nameof(PlotVisibility));
OnPropertyChanged(nameof(IsPlotWindowVisible));
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 bool IsHardwareInteractionEnabled => !IsHardwareLoading;
public bool IsHardwareLoading
{
get => _isHardwareLoading;
private set
{
if (!SetProperty(ref _isHardwareLoading, value))
return;
OnPropertyChanged(nameof(HardwareLoadingVisibility));
OnPropertyChanged(nameof(IsHardwareInteractionEnabled));
}
}
public Visibility HardwareLoadingVisibility => IsHardwareLoading ? Visibility.Visible : Visibility.Collapsed;
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 RefreshPlotSeries()
{
TrackPlotPoints();
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 bool IsSensorInGadget(SensorTreeItemViewModel item)
{
return item.Sensor != null && Settings.GetValue(GetSensorSettingName(item.Sensor, "gadget"), false);
}
public bool IsSensorInTray(SensorTreeItemViewModel item)
{
return item.Sensor != null && Settings.GetValue(GetSensorSettingName(item.Sensor, "tray"), false);
}
public IEnumerable<SensorTreeItemViewModel> GetGadgetSensorItems()
{
return RootItems.FirstOrDefault()?.EnumerateSensors().Where(IsSensorInGadget) ?? [];
}
public IEnumerable<SensorTreeItemViewModel> GetTraySensorItems()
{
return RootItems.FirstOrDefault()?.EnumerateSensors().Where(IsSensorInTray) ?? [];
}
public void SetSensorInGadget(SensorTreeItemViewModel item, bool value)
{
if (item.Sensor == null)
return;
SetSensorBooleanSetting(item.Sensor, "gadget", value);
GadgetSensorsChanged?.Invoke(this, EventArgs.Empty);
}
public void SetSensorInTray(SensorTreeItemViewModel item, bool value)
{
if (item.Sensor == null)
return;
SetSensorBooleanSetting(item.Sensor, "tray", value);
if (!value)
Settings.Remove(GetSensorSettingName(item.Sensor, "traycolor"));
TraySensorsChanged?.Invoke(this, EventArgs.Empty);
}
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
{
IsHardwareLoading = true;
_startupTrace?.Mark("MainWindowViewModel.StartAsync.Begin");
StatusText = "Initializing hardware sensors...";
await MeasureStartupAsync("MainWindowViewModel.HardwareMonitor.OpenAsync", () => _hardwareMonitor.OpenAsync(raiseTreeRebuilt: false));
MeasureStartup("MainWindowViewModel.UpdateRoot", UpdateRoot, GetRootDetail);
StatusText = "Reading sensor values...";
await MeasureStartupAsync("MainWindowViewModel.InitialSensorValueUpdate", () => RefreshSensorValuesAsync(trackPlotPoints: true, logSensors: false));
MeasureStartup("MainWindowViewModel.StartWebServerFromSettings", StartWebServerFromSettings);
MeasureStartup("MainWindowViewModel.UpdateStatus", UpdateStatus, GetRootDetail);
_isStarted = true;
_startupTrace?.Mark("MainWindowViewModel.StartAsync.Complete", GetRootDetail());
}
finally
{
IsHardwareLoading = false;
_startupTrace?.Mark("MainWindowViewModel.HardwareLoadingComplete", GetRootDetail());
_isStarting = false;
}
}
public async Task UpdateAsync()
{
if (!_isStarted)
return;
await RefreshSensorValuesAsync(trackPlotPoints: true, logSensors: LogSensors);
}
private async Task RefreshSensorValuesAsync(bool trackPlotPoints, bool logSensors)
{
await _hardwareMonitor.UpdateAsync();
SensorTreeItemViewModel? root = RootItems.FirstOrDefault();
root?.RefreshValues();
if (trackPlotPoints)
TrackPlotPoints();
if (logSensors)
_logger.Log();
UpdateStatus();
}
private void HardwareMonitor_TreeRebuilt(object? sender, EventArgs e)
{
if (_dispatcherQueue.HasThreadAccess)
{
UpdateRoot();
UpdateStatus();
return;
}
if (Interlocked.Exchange(ref _rootUpdateQueued, 1) == 1)
return;
if (!_dispatcherQueue.TryEnqueue(() =>
{
try
{
UpdateRoot();
UpdateStatus();
}
finally
{
Interlocked.Exchange(ref _rootUpdateQueued, 0);
}
}))
{
Interlocked.Exchange(ref _rootUpdateQueued, 0);
}
}
private void ApplySensorValuesTimeWindow()
{
TimeSpan timeWindow = SensorValuesTimeWindows[SensorValuesTimeWindowIndex];
SensorVisitor visitor = new(sensor => sensor.ValuesTimeWindow = timeWindow);
visitor.VisitComputer(_hardwareMonitor.Computer);
}
private string GetRootDetail()
{
int hardwareCount = _hardwareMonitor.Computer.Hardware.Count;
int sensorCount = RootItems.FirstOrDefault()?.EnumerateSensors().Count() ?? 0;
return $"{hardwareCount} hardware device(s), {sensorCount} sensor(s)";
}
private void MeasureStartup(string phase, Action action)
{
if (_startupTrace is not { IsComplete: false })
{
action();
return;
}
_startupTrace.Measure(phase, action);
}
private void MeasureStartup(string phase, Action action, Func<string> getDetail)
{
if (_startupTrace is not { IsComplete: false })
{
action();
return;
}
_startupTrace.Measure(phase, action, getDetail);
}
private T MeasureStartup<T>(string phase, Func<T> action)
{
if (_startupTrace is not { IsComplete: false })
return action();
return _startupTrace.Measure(phase, action);
}
private async Task MeasureStartupAsync(string phase, Func<Task> action)
{
if (_startupTrace is not { IsComplete: false })
{
await action();
return;
}
await _startupTrace.MeasureAsync(phase, action);
}
private void NotifyColumnVisibilityChanged()
{
OnPropertyChanged(nameof(ValueColumnVisibility));
OnPropertyChanged(nameof(MinColumnVisibility));
OnPropertyChanged(nameof(MaxColumnVisibility));
RootItems.FirstOrDefault()?.SetColumnVisibility(ShowValueColumn, ShowMinColumn, ShowMaxColumn);
}
private void NotifyPlotLayoutChanged()
{
OnPropertyChanged(nameof(PlotVisibility));
OnPropertyChanged(nameof(IsPlotWindowVisible));
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 SetSensorBooleanSetting(ISensor sensor, string suffix, bool value)
{
string settingName = GetSensorSettingName(sensor, suffix);
if (value)
Settings.SetValue(settingName, true);
else
Settings.Remove(settingName);
}
private static string GetSensorSettingName(ISensor sensor, string suffix)
{
return new Identifier(sensor.Identifier, suffix).ToString();
}
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.UtcNow;
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);
if (!_plotSeriesByIdentifier.TryGetValue(identifier, out PlotSeriesViewModel? series))
{
series = new PlotSeriesViewModel(
identifier,
sensor.Hardware.Name,
sensor.Name,
sensor.SensorType,
SensorFormatter.GetPlotUnit(sensor.SensorType, TemperatureUnit),
GetPlotColor(sensorItem));
_plotSeriesByIdentifier[identifier] = series;
PlotSeries.Add(series);
}
else
{
series.UpdateMetadata(sensor.Hardware.Name, sensor.Name, sensor.SensorType, SensorFormatter.GetPlotUnit(sensor.SensorType, TemperatureUnit));
// Only honor an explicit user pen color for an existing series; keep the auto-assigned color stable.
// (Recomputing it from the live series count made existing lines shift/collide colors every tick.)
if (sensorItem.PenColor.HasValue)
series.Color = sensorItem.PenColor.Value;
}
List<PlotPointViewModel> points = [];
foreach (SensorValue sensorValue in sensor.Values.OrderBy(value => value.Time))
{
double? displayedValue = SensorFormatter.GetPlotValue(sensor, sensorValue.Value, TemperatureUnit);
if (displayedValue is not { } pointValue || !double.IsFinite(pointValue))
continue;
points.Add(new PlotPointViewModel(sensorValue.Time, pointValue));
}
DateTime? latestHistoryTimestamp = points.Count > 0 ? points[^1].Timestamp : null;
foreach (PlotPointViewModel existingPoint in series.Points)
{
if (now - existingPoint.Timestamp > MaximumSyntheticPlotPointRetention)
continue;
if (!latestHistoryTimestamp.HasValue || existingPoint.Timestamp > latestHistoryTimestamp.Value)
points.Add(existingPoint);
}
double? currentValue = SensorFormatter.GetPlotValue(sensor, TemperatureUnit);
if (currentValue is { } currentPointValue && double.IsFinite(currentPointValue))
points.Add(new PlotPointViewModel(now, currentPointValue));
DateTime cutoff = now - MaximumPlotPointRetention;
points = points
.Where(point => point.Timestamp >= cutoff)
.GroupBy(point => point.Timestamp.Ticks)
.Select(group => group.Last())
.OrderBy(point => point.Timestamp)
.ToList();
if (points.Count == 0 && currentValue is { } fallbackPointValue && double.IsFinite(fallbackPointValue))
{
points.Add(new PlotPointViewModel(now, fallbackPointValue));
}
series.ReplacePoints(points);
}
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()
{
MeasureStartup("MainWindowViewModel.RootItems.Clear", RootItems.Clear);
SensorTreeItemViewModel root = MeasureStartup("MainWindowViewModel.GetHardwareRoot", () => _hardwareMonitor.Root);
MeasureStartup("MainWindowViewModel.ConfigureRoot", () => root.Configure(TemperatureUnit, ShowHiddenSensors, ShowValueColumn, ShowMinColumn, ShowMaxColumn), GetRootDetail);
MeasureStartup("MainWindowViewModel.RootItems.Add", () => RootItems.Add(root), GetRootDetail);
MeasureStartup("MainWindowViewModel.ApplySensorValuesTimeWindow", ApplySensorValuesTimeWindow, GetRootDetail);
}
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 int GetPlotTimeWindowIndex(float maxSeconds)
{
if (!float.IsFinite(maxSeconds))
return 0;
int bestIndex = DefaultPlotTimeWindowIndex;
double bestDistance = double.MaxValue;
for (int i = 1; i < PlotTimeWindows.Length; i++)
{
TimeSpan? timeWindow = PlotTimeWindows[i];
if (!timeWindow.HasValue)
continue;
double distance = Math.Abs(timeWindow.Value.TotalSeconds - maxSeconds);
if (distance < bestDistance)
{
bestDistance = distance;
bestIndex = i;
}
}
return bestIndex;
}
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";
}
}