7 Commits

Author SHA1 Message Date
afa7bd987a Update LibreHardwareMonitor
All checks were successful
Deploy to Gitea Releases / deploy-to-gitea-releases (push) Successful in 3m10s
- Some nullability updates
2026-02-27 13:46:06 -05:00
4cd7e74923 Minor change to test upgrade
All checks were successful
Deploy to Gitea Releases / deploy-to-gitea-releases (push) Successful in 52s
2026-01-27 21:25:59 -05:00
d65c556656 Try stopping service task before upgrade/uninstall
All checks were successful
Deploy to Gitea Releases / deploy-to-gitea-releases (push) Successful in 54s
2026-01-27 21:20:35 -05:00
fe3941d8bd Fix update source
All checks were successful
Deploy to Gitea Releases / deploy-to-gitea-releases (push) Successful in 54s
2026-01-27 20:40:39 -05:00
89d750fbe6 Add some logging and handle update error
All checks were successful
Deploy to Gitea Releases / deploy-to-gitea-releases (push) Successful in 54s
2026-01-27 20:30:48 -05:00
2e14ff032b Adjust logging and add debugger option
All checks were successful
Deploy to Gitea Releases / deploy-to-gitea-releases (push) Successful in 1m12s
2026-01-27 19:59:51 -05:00
13ae4c74bc Add separate logs
All checks were successful
Deploy to Gitea Releases / deploy-to-gitea-releases (push) Successful in 52s
2026-01-27 19:23:11 -05:00
18 changed files with 179 additions and 77 deletions

View File

@@ -31,6 +31,6 @@ jobs:
run: |
export PATH="$PATH:/root/.dotnet/tools"
dotnet tool install -g vpk
#vpk [win] download gitea --channel win-x64 --repoUrl https://gitea.kaczorzoo.net/ckaczor/HardwareMonitorStatusWindow
vpk [win] download gitea --channel win-x64 --repoUrl https://gitea.kaczorzoo.net/ckaczor/HardwareMonitorStatusWindow
vpk [win] pack --channel win-x64 -u HardwareMonitorStatusWindow -v ${{ steps.version.outputs.version }} -p publish --packTitle "Hardware Monitor Status Window" --shortcuts StartMenuRoot --framework net10.0-x64-desktop
vpk [win] upload gitea --channel win-x64 --repoUrl https://gitea.kaczorzoo.net/ckaczor/HardwareMonitorStatusWindow --publish --releaseName "${{ steps.version.outputs.version }}" --token ${{ secrets.VPK_TOKEN }}

View File

@@ -1,3 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/UserDictionary/Words/=dragdrop/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Kaczor/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Kaczor/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=Velopack/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

View File

@@ -1,4 +1,5 @@
using LibreHardwareMonitor.Hardware;
using Serilog;
namespace HardwareMonitorStatusWindow.Service;
@@ -12,6 +13,8 @@ public class HardwareMonitorService : IHardwareMonitorService
static HardwareMonitorService()
{
Log.Information("Creating computer");
Computer = new Computer
{
IsCpuEnabled = true,
@@ -25,6 +28,8 @@ public class HardwareMonitorService : IHardwareMonitorService
IsPsuEnabled = true
};
Log.Information("Opening computer");
Computer.Open();
HardwareUpdateVisitor = new HardwareUpdateVisitor();
@@ -32,8 +37,12 @@ public class HardwareMonitorService : IHardwareMonitorService
public IEnumerable<Hardware> GetHardware()
{
Log.Information("Updating computer");
Computer.Accept(HardwareUpdateVisitor);
Log.Information("Creating hardware entries");
var hardwareEntries = Computer.Hardware.Select(Hardware.Create);
return hardwareEntries;

View File

@@ -12,13 +12,13 @@ internal class Program
{
private static async Task Main(string[] args)
{
Log.Logger = new LoggerConfiguration().WriteTo.File("log.txt").CreateLogger();
Log.Logger = new LoggerConfiguration().WriteTo.File("HardwareMonitorService.log").CreateLogger();
Log.Logger.Information("Start");
Log.Information("Start");
if (args.Contains("--install", StringComparer.InvariantCultureIgnoreCase))
{
Log.Logger.Information("Starting install...");
Log.Information("Starting install...");
try
{
@@ -48,14 +48,14 @@ internal class Program
}
catch (Exception exception)
{
Log.Logger.Error(exception, "Install");
Log.Error(exception, "");
}
Log.Logger.Information("Install complete");
Log.Information("Install complete");
}
else if (args.Contains("--uninstall", StringComparer.InvariantCultureIgnoreCase))
{
Log.Logger.Information("Starting uninstall...");
Log.Information("Starting uninstall...");
try
{
@@ -69,43 +69,51 @@ internal class Program
}
catch (Exception exception)
{
Log.Logger.Error(exception, "Uninstall");
Log.Error(exception, "");
}
Log.Logger.Information("Uninstall complete");
Log.Information("Uninstall complete");
}
else
{
Log.Logger.Information("Starting");
Log.Information("Starting");
try
{
while (true)
{
Log.Information("Creating PipeSecurity");
var pipeSecurity = new PipeSecurity();
pipeSecurity.AddAccessRule(new PipeAccessRule(new SecurityIdentifier(WellKnownSidType.AuthenticatedUserSid, null), PipeAccessRights.ReadWrite, AccessControlType.Allow));
Log.Information("Creating NamedPipe");
var pipeWithSecurity = NamedPipeServerStreamAcl.Create(HardwareMonitorService.PipeName, PipeDirection.InOut, 1, PipeTransmissionMode.Byte, PipeOptions.Asynchronous, 0, 0, pipeSecurity);
Log.Information("Creating PipeServer");
var pipeServer = new PipeServer<IHardwareMonitorService>(new HardwarePipeSerializer(), pipeWithSecurity, () => new HardwareMonitorService());
//var pipeServer = new PipeServer<IHardwareMonitorService>(
// new HardwarePipeSerializer(),
// HardwareMonitorService.PipeName,
// () => new HardwareMonitorService());
Log.Information("Waiting for connection");
await pipeServer.WaitForConnectionAsync().ConfigureAwait(false);
Log.Information("Waiting for remote pipe to close");
await pipeServer.WaitForRemotePipeCloseAsync().ConfigureAwait(false);
Log.Information("Disposing pipe server");
pipeServer.Dispose();
}
}
catch (Exception exception)
{
Log.Logger.Error(exception, "");
Log.Error(exception, "");
}
}
Log.Logger.Information("Closing");
Log.Information("Closing");
}
}

View File

@@ -11,9 +11,9 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.5" />
<PackageReference Include="LibreHardwareMonitorLib" Version="0.9.6" />
<PackageReference Include="PipeMethodCalls" Version="4.0.3" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="TaskScheduler" Version="2.12.2" />
</ItemGroup>

View File

@@ -8,7 +8,7 @@ namespace HardwareMonitorStatusWindow.StatusWindow;
public partial class App
{
private List<IDisposable> _windowSourceList;
private List<IDisposable>? _windowSourceList;
protected override void OnStartup(StartupEventArgs e)
{
@@ -30,7 +30,7 @@ public partial class App
protected override void OnExit(ExitEventArgs e)
{
_windowSourceList.ForEach(ws => ws.Dispose());
_windowSourceList?.ForEach(ws => ws.Dispose());
base.OnExit(e);
}

View File

@@ -6,15 +6,16 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
using Serilog;
namespace HardwareMonitorStatusWindow.StatusWindow;
internal static class Data
{
private static PipeClient<IHardwareMonitorService> _pipeClient;
private static IEnumerable<Hardware> _hardware;
private static PipeClient<IHardwareMonitorService>? _pipeClient;
private static IEnumerable<Hardware>? _hardware;
internal static ObservableCollection<SensorEntry> SensorEntries { get; set; }
internal static ObservableCollection<SensorEntry> SensorEntries { get; set; } = [];
internal static async Task LoadComputer()
{
@@ -25,7 +26,7 @@ internal static class Data
}
catch (Exception exception)
{
Log.Error(exception, "");
}
}
@@ -36,14 +37,14 @@ internal static class Data
internal static void CloseComputer()
{
_pipeClient.Dispose();
_pipeClient?.Dispose();
}
internal static IList<Hardware> ComputerHardware => _hardware.ToList();
internal static IList<Hardware> ComputerHardware => _hardware?.ToList() ?? [];
internal static void Load()
{
SensorEntries = JsonSerializer.Deserialize<ObservableCollection<SensorEntry>>(Settings.Default.Sensors);
SensorEntries = JsonSerializer.Deserialize<ObservableCollection<SensorEntry>>(Settings.Default.Sensors) ?? [];
}
internal static void Save()

View File

@@ -7,15 +7,18 @@ namespace HardwareMonitorStatusWindow.StatusWindow;
internal class DataErrorDictionary : Dictionary<string, List<string>>
{
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
private void OnErrorsChanged(string propertyName)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(propertyName));
}
public IEnumerable GetErrors(string propertyName)
public IEnumerable? GetErrors(string? propertyName)
{
if (propertyName == null)
return null;
return TryGetValue(propertyName, out var value) ? value : null;
}

View File

@@ -1,5 +1,8 @@
using System;
using HardwareMonitorStatusWindow.Service;
using Microsoft.Win32.TaskScheduler;
using Serilog;
using System;
using System.Diagnostics;
using Velopack;
namespace HardwareMonitorStatusWindow.StatusWindow;
@@ -9,18 +12,42 @@ internal class Program
[STAThread]
public static void Main(string[] args)
{
Log.Logger = new LoggerConfiguration().WriteTo.File("log.txt").CreateLogger();
if (args.Contains("--debug"))
Debugger.Launch();
Log.Logger.Information("Start");
Log.Logger = new LoggerConfiguration().WriteTo.File("HardwareMonitorStatusWindow.log").CreateLogger();
// var loggerFactory = new LoggerFactory().AddSerilog(Log.Logger);
Log.Information("Start");
VelopackApp.Build().Run(); // loggerFactory.CreateLogger("Install")
var stopServiceHook = new VelopackHook(_ =>
{
try
{
using var taskService = new TaskService();
Log.Information("Checking for task name: {name}", HardwareMonitorService.ScheduledTaskName);
var existingTask = taskService.FindTask(HardwareMonitorService.ScheduledTaskName);
Log.Information("Task: {existingTask}", existingTask);
Log.Information("Stopping task");
existingTask?.Stop();
}
catch (Exception exception)
{
// Ignored
Log.Error(exception, "");
}
});
VelopackApp.Build().OnBeforeUpdateFastCallback(stopServiceHook).OnBeforeUninstallFastCallback(stopServiceHook).SetLogger(new SerilogVelopackLogger()).Run();
var app = new App();
app.InitializeComponent();
app.Run();
Log.Logger.Information("End");
Log.Information("End");
}
}

View File

@@ -11,15 +11,14 @@ namespace HardwareMonitorStatusWindow.StatusWindow;
public class SensorEntry : INotifyDataErrorInfo, INotifyPropertyChanged
{
private readonly DataErrorDictionary _dataErrorDictionary;
private readonly DataErrorDictionary _dataErrorDictionary = new();
public SensorEntry()
{
_dataErrorDictionary = new DataErrorDictionary();
_dataErrorDictionary.ErrorsChanged += DataErrorDictionaryErrorsChanged;
}
public string Label
public string? Label
{
get;
set
@@ -31,7 +30,7 @@ public class SensorEntry : INotifyDataErrorInfo, INotifyPropertyChanged
}
}
public string HardwareId
public string? HardwareId
{
get;
set
@@ -41,7 +40,7 @@ public class SensorEntry : INotifyDataErrorInfo, INotifyPropertyChanged
}
}
public string SensorId
public string? SensorId
{
get;
set
@@ -52,7 +51,7 @@ public class SensorEntry : INotifyDataErrorInfo, INotifyPropertyChanged
}
[JsonIgnore]
public Hardware? Hardware => Data.ComputerHardware.FirstOrDefault(h => h.Identifier.ToString() == HardwareId);
public Hardware? Hardware => Data.ComputerHardware?.FirstOrDefault(h => h.Identifier.ToString() == HardwareId);
[JsonIgnore]
public Sensor? Sensor => Hardware?.Sensors.FirstOrDefault(s => s.Identifier.ToString() == SensorId);
@@ -60,19 +59,22 @@ public class SensorEntry : INotifyDataErrorInfo, INotifyPropertyChanged
[JsonIgnore]
public bool HasErrors => _dataErrorDictionary.Any();
public IEnumerable GetErrors(string propertyName)
public IEnumerable GetErrors(string? propertyName)
{
return _dataErrorDictionary.GetErrors(propertyName);
if (string.IsNullOrWhiteSpace(propertyName))
throw new InvalidOperationException();
return _dataErrorDictionary.GetErrors(propertyName) ?? Enumerable.Empty<string>();
}
public event EventHandler<DataErrorsChangedEventArgs> ErrorsChanged;
public event EventHandler<DataErrorsChangedEventArgs>? ErrorsChanged;
private void DataErrorDictionaryErrorsChanged(object sender, DataErrorsChangedEventArgs e)
private void DataErrorDictionaryErrorsChanged(object? sender, DataErrorsChangedEventArgs e)
{
ErrorsChanged?.Invoke(this, new DataErrorsChangedEventArgs(e.PropertyName));
}
private bool ValidateLabel(string newValue)
private bool ValidateLabel(string? newValue)
{
_dataErrorDictionary.ClearErrors(nameof(Label));
@@ -84,14 +86,14 @@ public class SensorEntry : INotifyDataErrorInfo, INotifyPropertyChanged
return false;
}
public event PropertyChangedEventHandler PropertyChanged;
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string propertyName = null)
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value)) return false;
field = value;

View File

@@ -0,0 +1,18 @@
using System;
using Velopack.Logging;
namespace HardwareMonitorStatusWindow.StatusWindow;
internal class SerilogVelopackLogger : IVelopackLogger
{
public void Log(VelopackLogLevel logLevel, string? message, Exception? exception)
{
if (exception != null)
{
Serilog.Log.Error(exception, "");
return;
}
Serilog.Log.Information("[{time}] [{logLevel}] {message}", DateTime.Now.ToShortTimeString(), logLevel, message);
}
}

View File

@@ -58,12 +58,12 @@
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Hardware.Type}"
<TextBlock Text="{Binding Path=Hardware.Type, FallbackValue=null}"
Height="Auto"
FontSize="10"
VerticalAlignment="Center"
Margin="0,2,0,2" />
<TextBlock Text="{Binding Path=Hardware.Name}"
<TextBlock Text="{Binding Path=Hardware.Name, FallbackValue=null}"
Height="Auto"
VerticalAlignment="Center" />
</StackPanel>
@@ -75,12 +75,12 @@
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Text="{Binding Path=Sensor.Type}"
<TextBlock Text="{Binding Path=Sensor.Type, FallbackValue=null}"
Height="Auto"
FontSize="10"
VerticalAlignment="Center"
Margin="0,2,0,2" />
<TextBlock Text="{Binding Path=Sensor.Name}"
<TextBlock Text="{Binding Path=Sensor.Name, FallbackValue=null}"
Height="Auto"
VerticalAlignment="Center" />
</StackPanel>

View File

@@ -7,7 +7,7 @@ namespace HardwareMonitorStatusWindow.StatusWindow.SettingsWindow;
public partial class HardwareSettingsPanel
{
private CollectionViewSource _collectionViewSource;
private CollectionViewSource? _collectionViewSource;
public HardwareSettingsPanel()
{
@@ -105,7 +105,7 @@ public partial class HardwareSettingsPanel
SensorDataGrid.SelectedItems.CopyTo(selectedItems, 0);
foreach (var sensorEntry in selectedItems)
Data.SensorEntries.Remove(sensorEntry);
Data.SensorEntries?.Remove(sensorEntry);
SetSensorButtonStates();
}

View File

@@ -12,7 +12,7 @@ public partial class SensorWindow
InitializeComponent();
}
public bool? Display(SensorEntry sensorEntry, Window owner)
public bool? Display(SensorEntry sensorEntry, Window? owner)
{
DataContext = sensorEntry;

View File

@@ -1,6 +1,5 @@
using System.Windows;
using System.Windows.Input;
using HardwareMonitorStatusWindow.StatusWindow;
namespace HardwareMonitorStatusWindow.StatusWindow.SettingsWindow;

View File

@@ -28,10 +28,10 @@
<PackageReference Include="ChrisKaczor.Wpf.Controls.Link" Version="1.0.4" />
<PackageReference Include="ChrisKaczor.Wpf.Validation" Version="1.0.4" />
<PackageReference Include="ChrisKaczor.Wpf.Windows.CategoryWindow" Version="1.0.2" />
<PackageReference Include="ChrisKaczor.Wpf.Windows.FloatingStatusWindow" Version="2.0.0.7" />
<PackageReference Include="ChrisKaczor.Wpf.Windows.FloatingStatusWindow" Version="2.0.0.8" />
<PackageReference Include="gong-wpf-dragdrop" Version="4.0.0" />
<PackageReference Include="PipeMethodCalls" Version="4.0.3" />
<PackageReference Include="Serilog" Version="4.3.0" />
<PackageReference Include="Serilog" Version="4.3.1" />
<PackageReference Include="Serilog.Sinks.File" Version="7.0.0" />
<PackageReference Include="Velopack" Version="0.0.1298" />
</ItemGroup>

View File

@@ -9,9 +9,9 @@ namespace HardwareMonitorStatusWindow.StatusWindow;
internal static class UpdateCheck
{
private static UpdateManager _updateManager;
private static UpdateManager? _updateManager;
public static UpdateManager UpdateManager => _updateManager ??= new UpdateManager(new GithubSource("https://gitea.kaczorzoo.net/ckaczor/HardwareMonitorStatusWindow", null, false));
public static UpdateManager UpdateManager => _updateManager ??= new UpdateManager(new GiteaSource("https://gitea.kaczorzoo.net/ckaczor/HardwareMonitorStatusWindow", null, false));
public static string LocalVersion => (UpdateManager.CurrentVersion ?? new SemanticVersion(0, 0, 0)).ToString();
@@ -30,11 +30,11 @@ internal static class UpdateCheck
if (MessageBox.Show(updateCheckMessage, updateCheckTitle, MessageBoxButton.YesNo, MessageBoxImage.Question) != MessageBoxResult.Yes)
return;
Log.Logger.Information("Downloading update");
Log.Information("Downloading update");
await UpdateManager.DownloadUpdatesAsync(newVersion);
Log.Logger.Information("Installing update");
Log.Information("Installing update");
UpdateManager.ApplyUpdatesAndRestart(newVersion);
}

View File

@@ -2,6 +2,7 @@
using ChrisKaczor.Wpf.Windows.FloatingStatusWindow;
using HardwareMonitorStatusWindow.Service;
using HardwareMonitorStatusWindow.StatusWindow.SettingsWindow;
using LibreHardwareMonitor.PawnIo;
using Microsoft.Win32.TaskScheduler;
using Serilog;
using System;
@@ -23,25 +24,44 @@ internal class WindowSource : IWindowSource, IDisposable
private readonly FloatingStatusWindow _floatingStatusWindow;
private readonly Timer _timer;
private readonly Dispatcher _dispatcher;
internal WindowSource()
{
try
{
using var taskService = new TaskService();
if (!PawnIo.IsInstalled)
{
Log.Information("PawnIO not installed");
}
else
{
var pawnIoVersion = PawnIo.Version;
Log.Information("PawnIO installed: {version}", pawnIoVersion);
}
Log.Information("Checking for task name: {name}", HardwareMonitorService.ScheduledTaskName);
var existingTask = taskService.FindTask(HardwareMonitorService.ScheduledTaskName);
Log.Information("Task: {existingTask}", existingTask);
if (existingTask == null)
{
var assembly = Assembly.GetExecutingAssembly();
var path = Path.GetDirectoryName(assembly.Location);
Log.Information("Service path: {path}", path);
if (path != null)
{
var fileName = Path.Combine(path, "HardwareMonitorService.exe");
Log.Information("Full service path: {fileName}", fileName);
var startInfo = new ProcessStartInfo
{
FileName = fileName,
@@ -50,13 +70,16 @@ internal class WindowSource : IWindowSource, IDisposable
Verb = "runas"
};
Log.Information("Starting process");
Process.Start(startInfo);
}
}
}
catch (Exception)
catch (Exception exception)
{
// Ignored
Log.Error(exception, "");
}
_floatingStatusWindow = new FloatingStatusWindow(this);
@@ -73,36 +96,44 @@ internal class WindowSource : IWindowSource, IDisposable
{
try
{
Log.Information("IsInstalled: {isInstalled}", UpdateCheck.IsInstalled);
if (!UpdateCheck.IsInstalled)
return false;
Log.Information("CheckVersionAtStartup: {checkVersionAtStartup}", Settings.Default.CheckVersionAtStartup);
if (!Settings.Default.CheckVersionAtStartup)
return false;
Log.Logger.Information("Checking for update");
await _dispatcher.InvokeAsync(() => _floatingStatusWindow.SetText(Resources.CheckingForUpdate));
Log.Information("Checking for updates");
var newVersion = await UpdateCheck.UpdateManager.CheckForUpdatesAsync();
Log.Information("New version: {version}", newVersion);
if (newVersion == null)
return false;
Log.Logger.Information("Downloading update");
await _dispatcher.InvokeAsync(() => _floatingStatusWindow.SetText(Resources.DownloadingUpdate));
Log.Information("Downloading update");
await UpdateCheck.UpdateManager.DownloadUpdatesAsync(newVersion);
Log.Logger.Information("Installing update");
await _dispatcher.InvokeAsync(() => _floatingStatusWindow.SetText(Resources.InstallingUpdate));
Log.Information("Installing update");
UpdateCheck.UpdateManager.ApplyUpdatesAndRestart(newVersion);
}
catch (Exception e)
{
Log.Logger.Error(e, nameof(UpdateApp));
Log.Error(e, nameof(UpdateApp));
return false;
}
return true;
@@ -110,16 +141,16 @@ internal class WindowSource : IWindowSource, IDisposable
private async Task Start(bool hasUpdate)
{
Log.Logger.Information("Start: hasUpdate={hasUpdate}", hasUpdate);
Log.Information("Start: hasUpdate={hasUpdate}", hasUpdate);
if (hasUpdate)
return;
Log.Logger.Information("Load");
Log.Information("Load");
await Load();
Log.Logger.Information("Starting timer");
Log.Information("Starting timer");
_timer.Elapsed += HandleTimerElapsed;
_timer.AutoReset = false;
@@ -133,7 +164,7 @@ internal class WindowSource : IWindowSource, IDisposable
Data.Load();
}
private void Save()
private static void Save()
{
Data.Save();
}
@@ -212,6 +243,9 @@ internal class WindowSource : IWindowSource, IDisposable
foreach (var sensorEntry in Data.SensorEntries)
{
if (sensorEntry.Sensor == null)
continue;
if (text.Length > 0)
text.AppendLine();