From c920859ceba362863210a6d10ca2a8cb7b32b78d Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Nov 2025 17:08:40 +0000 Subject: [PATCH 1/3] feat: Add JSON file persistence and simplify tank alert system Backend Persistence: - Add JsonFileStore service for local JSON file-based data persistence - Store tanks, controls, and configuration in individual JSON files in /data directory - Auto-load persisted data on startup with fallback to defaults - Thread-safe concurrent operations with SemaphoreSlim - Tanks saved to tanks.json, controls to controls.json Tank Alert System Improvements: - Replace LowLevelThreshold and HighLevelThreshold with single AlertLevel field - Add AlertWhenOver boolean to configure alert direction: * false = alert when UNDER threshold (tank running empty) - for fresh water, LPG, fuel * true = alert when OVER threshold (tank getting full) - for waste water - Update default tanks with appropriate alert configurations: * Fresh Water: Alert at 20% when under (running empty) * Waste Water: Alert at 80% when over (getting full) * LPG: Alert at 20% when under (running empty) - Remove global tank thresholds from SystemConfiguration.AlertSettings - Alert levels now configured per-tank in Tanks page UI Updates: - Tanks.razor: Add alert level slider and "Alert When Over/Under" toggle with visual feedback - Index.razor: Update tank color indicators to use new alert system - Settings.razor: Remove global threshold fields, add note about per-tank configuration - Improved alert messages with actual percentage values - Dynamic alert severity based on criticality (Warning vs Error) All data persists to JSON files and survives application restarts. Data location: {AppContext.BaseDirectory}/data/ --- src/Backend/VanDaemon.Api/Program.cs | 5 + .../Persistence/JsonFileStore.cs | 138 ++++++++++++++++++ .../Services/ControlService.cs | 62 ++++++-- .../Services/TankService.cs | 74 ++++++++-- .../Entities/SystemConfiguration.cs | 2 - src/Backend/VanDaemon.Core/Entities/Tank.cs | 4 +- src/Frontend/VanDaemon.Web/Pages/Index.razor | 24 ++- .../VanDaemon.Web/Pages/Settings.razor | 19 +-- src/Frontend/VanDaemon.Web/Pages/Tanks.razor | 92 +++++++----- 9 files changed, 335 insertions(+), 85 deletions(-) create mode 100644 src/Backend/VanDaemon.Application/Persistence/JsonFileStore.cs diff --git a/src/Backend/VanDaemon.Api/Program.cs b/src/Backend/VanDaemon.Api/Program.cs index 1f4e2bc..e1ae121 100644 --- a/src/Backend/VanDaemon.Api/Program.cs +++ b/src/Backend/VanDaemon.Api/Program.cs @@ -3,6 +3,7 @@ using VanDaemon.Api.Hubs; using VanDaemon.Api.Services; using VanDaemon.Application.Interfaces; +using VanDaemon.Application.Persistence; using VanDaemon.Application.Services; using VanDaemon.Plugins.Abstractions; using VanDaemon.Plugins.Simulated; @@ -41,6 +42,10 @@ }); }); +// Register JSON file persistence +builder.Services.AddSingleton(sp => + new JsonFileStore(sp.GetRequiredService>(), Path.Combine(AppContext.BaseDirectory, "data"))); + // Register plugins builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Backend/VanDaemon.Application/Persistence/JsonFileStore.cs b/src/Backend/VanDaemon.Application/Persistence/JsonFileStore.cs new file mode 100644 index 0000000..d621ecd --- /dev/null +++ b/src/Backend/VanDaemon.Application/Persistence/JsonFileStore.cs @@ -0,0 +1,138 @@ +using System.Text.Json; +using Microsoft.Extensions.Logging; + +namespace VanDaemon.Application.Persistence; + +/// +/// Simple JSON file-based persistence for application data +/// +public class JsonFileStore +{ + private readonly string _dataDirectory; + private readonly ILogger _logger; + private readonly JsonSerializerOptions _jsonOptions; + private static readonly SemaphoreSlim _semaphore = new(1, 1); + + public JsonFileStore(ILogger logger, string? dataDirectory = null) + { + _logger = logger; + _dataDirectory = dataDirectory ?? Path.Combine(AppContext.BaseDirectory, "data"); + + // Ensure data directory exists + if (!Directory.Exists(_dataDirectory)) + { + Directory.CreateDirectory(_dataDirectory); + _logger.LogInformation("Created data directory at {Path}", _dataDirectory); + } + + _jsonOptions = new JsonSerializerOptions + { + WriteIndented = true, + PropertyNameCaseInsensitive = true + }; + } + + /// + /// Save data to a JSON file + /// + public async Task SaveAsync(string fileName, T data) + { + var filePath = Path.Combine(_dataDirectory, fileName); + + await _semaphore.WaitAsync(); + try + { + var json = JsonSerializer.Serialize(data, _jsonOptions); + await File.WriteAllTextAsync(filePath, json); + _logger.LogDebug("Saved data to {FileName}", fileName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving data to {FileName}", fileName); + throw; + } + finally + { + _semaphore.Release(); + } + } + + /// + /// Load data from a JSON file + /// + public async Task LoadAsync(string fileName) where T : class + { + var filePath = Path.Combine(_dataDirectory, fileName); + + if (!File.Exists(filePath)) + { + _logger.LogDebug("File {FileName} does not exist, returning null", fileName); + return null; + } + + await _semaphore.WaitAsync(); + try + { + var json = await File.ReadAllTextAsync(filePath); + var data = JsonSerializer.Deserialize(json, _jsonOptions); + _logger.LogDebug("Loaded data from {FileName}", fileName); + return data; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading data from {FileName}", fileName); + return null; + } + finally + { + _semaphore.Release(); + } + } + + /// + /// Delete a JSON file + /// + public async Task DeleteAsync(string fileName) + { + var filePath = Path.Combine(_dataDirectory, fileName); + + await _semaphore.WaitAsync(); + try + { + if (File.Exists(filePath)) + { + File.Delete(filePath); + _logger.LogDebug("Deleted file {FileName}", fileName); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error deleting file {FileName}", fileName); + throw; + } + finally + { + _semaphore.Release(); + } + } + + /// + /// Check if a file exists + /// + public bool Exists(string fileName) + { + var filePath = Path.Combine(_dataDirectory, fileName); + return File.Exists(filePath); + } + + /// + /// Get all files in the data directory + /// + public IEnumerable GetAllFiles() + { + return Directory.GetFiles(_dataDirectory, "*.json") + .Select(Path.GetFileName) + .Where(f => f != null) + .Select(f => f!); + } +} diff --git a/src/Backend/VanDaemon.Application/Services/ControlService.cs b/src/Backend/VanDaemon.Application/Services/ControlService.cs index 5bdb2ec..6f670c2 100644 --- a/src/Backend/VanDaemon.Application/Services/ControlService.cs +++ b/src/Backend/VanDaemon.Application/Services/ControlService.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using VanDaemon.Application.Interfaces; +using VanDaemon.Application.Persistence; using VanDaemon.Core.Entities; using VanDaemon.Plugins.Abstractions; @@ -11,15 +12,55 @@ namespace VanDaemon.Application.Services; public class ControlService : IControlService { private readonly ILogger _logger; + private readonly JsonFileStore _fileStore; private readonly List _controls; private readonly Dictionary _controlPlugins; + private const string ControlsFileName = "controls.json"; - public ControlService(ILogger logger, IEnumerable controlPlugins) + public ControlService(ILogger logger, JsonFileStore fileStore, IEnumerable controlPlugins) { _logger = logger; + _fileStore = fileStore; _controls = new List(); _controlPlugins = controlPlugins.ToDictionary(p => p.Name, p => p); - InitializeDefaultControls(); + _ = LoadControlsAsync(); // Fire and forget to load controls on startup + } + + private async Task LoadControlsAsync() + { + try + { + var controls = await _fileStore.LoadAsync>(ControlsFileName); + if (controls != null && controls.Any()) + { + _controls.Clear(); + _controls.AddRange(controls); + _logger.LogInformation("Loaded {Count} controls from {FileName}", controls.Count, ControlsFileName); + } + else + { + InitializeDefaultControls(); + await SaveControlsAsync(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading controls from JSON, using defaults"); + InitializeDefaultControls(); + } + } + + private async Task SaveControlsAsync() + { + try + { + await _fileStore.SaveAsync(ControlsFileName, _controls); + _logger.LogDebug("Saved {Count} controls to {FileName}", _controls.Count, ControlsFileName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving controls to JSON"); + } } private void InitializeDefaultControls() @@ -61,7 +102,7 @@ private void InitializeDefaultControls() ControlConfiguration = new Dictionary { ["controlId"] = "water_pump" }, LastUpdated = DateTime.UtcNow, IsActive = true, - IconName = "water_pump" + IconName = "water_drop" }); _controls.Add(new Control @@ -154,7 +195,7 @@ public async Task SetControlStateAsync(Guid id, object state, Cancellation return false; } - public Task UpdateControlAsync(Control control, CancellationToken cancellationToken = default) + public async Task UpdateControlAsync(Control control, CancellationToken cancellationToken = default) { var existingControl = _controls.FirstOrDefault(c => c.Id == control.Id); if (existingControl != null) @@ -165,27 +206,28 @@ public Task UpdateControlAsync(Control control, CancellationToken cance control.LastUpdated = DateTime.UtcNow; _controls.Add(control); _logger.LogInformation("Updated control {ControlName}", control.Name); - return Task.FromResult(control); + await SaveControlsAsync(); + return control; } - public Task CreateControlAsync(Control control, CancellationToken cancellationToken = default) + public async Task CreateControlAsync(Control control, CancellationToken cancellationToken = default) { control.Id = Guid.NewGuid(); control.LastUpdated = DateTime.UtcNow; _controls.Add(control); _logger.LogInformation("Created control {ControlName} with ID {ControlId}", control.Name, control.Id); - return Task.FromResult(control); + await SaveControlsAsync(); + return control; } - public Task DeleteControlAsync(Guid id, CancellationToken cancellationToken = default) + public async Task DeleteControlAsync(Guid id, CancellationToken cancellationToken = default) { var control = _controls.FirstOrDefault(c => c.Id == id); if (control != null) { control.IsActive = false; _logger.LogInformation("Deleted control {ControlName}", control.Name); + await SaveControlsAsync(); } - - return Task.CompletedTask; } } diff --git a/src/Backend/VanDaemon.Application/Services/TankService.cs b/src/Backend/VanDaemon.Application/Services/TankService.cs index e85d722..48166ee 100644 --- a/src/Backend/VanDaemon.Application/Services/TankService.cs +++ b/src/Backend/VanDaemon.Application/Services/TankService.cs @@ -1,5 +1,6 @@ using Microsoft.Extensions.Logging; using VanDaemon.Application.Interfaces; +using VanDaemon.Application.Persistence; using VanDaemon.Core.Entities; using VanDaemon.Plugins.Abstractions; @@ -11,15 +12,55 @@ namespace VanDaemon.Application.Services; public class TankService : ITankService { private readonly ILogger _logger; - private readonly List _tanks; // In-memory storage for now + private readonly JsonFileStore _fileStore; + private readonly List _tanks; private readonly Dictionary _sensorPlugins; + private const string TanksFileName = "tanks.json"; - public TankService(ILogger logger, IEnumerable sensorPlugins) + public TankService(ILogger logger, JsonFileStore fileStore, IEnumerable sensorPlugins) { _logger = logger; + _fileStore = fileStore; _tanks = new List(); _sensorPlugins = sensorPlugins.ToDictionary(p => p.Name, p => p); - InitializeDefaultTanks(); + _ = LoadTanksAsync(); // Fire and forget to load tanks on startup + } + + private async Task LoadTanksAsync() + { + try + { + var tanks = await _fileStore.LoadAsync>(TanksFileName); + if (tanks != null && tanks.Any()) + { + _tanks.Clear(); + _tanks.AddRange(tanks); + _logger.LogInformation("Loaded {Count} tanks from {FileName}", tanks.Count, TanksFileName); + } + else + { + InitializeDefaultTanks(); + await SaveTanksAsync(); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error loading tanks from JSON, using defaults"); + InitializeDefaultTanks(); + } + } + + private async Task SaveTanksAsync() + { + try + { + await _fileStore.SaveAsync(TanksFileName, _tanks); + _logger.LogDebug("Saved {Count} tanks to {FileName}", _tanks.Count, TanksFileName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error saving tanks to JSON"); + } } private void InitializeDefaultTanks() @@ -32,8 +73,8 @@ private void InitializeDefaultTanks() Type = Core.Enums.TankType.FreshWater, Capacity = 100, CurrentLevel = 75, - LowLevelThreshold = 10, - HighLevelThreshold = 90, + AlertLevel = 20.0, + AlertWhenOver = false, // Alert when UNDER 20% (empty) SensorPlugin = "Simulated Sensor Plugin", SensorConfiguration = new Dictionary { ["sensorId"] = "fresh_water" }, LastUpdated = DateTime.UtcNow, @@ -47,8 +88,8 @@ private void InitializeDefaultTanks() Type = Core.Enums.TankType.WasteWater, Capacity = 80, CurrentLevel = 25, - LowLevelThreshold = 10, - HighLevelThreshold = 90, + AlertLevel = 80.0, + AlertWhenOver = true, // Alert when OVER 80% (full) SensorPlugin = "Simulated Sensor Plugin", SensorConfiguration = new Dictionary { ["sensorId"] = "waste_water" }, LastUpdated = DateTime.UtcNow, @@ -62,8 +103,8 @@ private void InitializeDefaultTanks() Type = Core.Enums.TankType.LPG, Capacity = 30, CurrentLevel = 60, - LowLevelThreshold = 10, - HighLevelThreshold = 90, + AlertLevel = 20.0, + AlertWhenOver = false, // Alert when UNDER 20% (empty) SensorPlugin = "Simulated Sensor Plugin", SensorConfiguration = new Dictionary { ["sensorId"] = "lpg" }, LastUpdated = DateTime.UtcNow, @@ -114,7 +155,7 @@ public async Task GetTankLevelAsync(Guid id, CancellationToken cancellat return tank.CurrentLevel; } - public Task UpdateTankAsync(Tank tank, CancellationToken cancellationToken = default) + public async Task UpdateTankAsync(Tank tank, CancellationToken cancellationToken = default) { var existingTank = _tanks.FirstOrDefault(t => t.Id == tank.Id); if (existingTank != null) @@ -125,28 +166,29 @@ public Task UpdateTankAsync(Tank tank, CancellationToken cancellationToken tank.LastUpdated = DateTime.UtcNow; _tanks.Add(tank); _logger.LogInformation("Updated tank {TankName}", tank.Name); - return Task.FromResult(tank); + await SaveTanksAsync(); + return tank; } - public Task CreateTankAsync(Tank tank, CancellationToken cancellationToken = default) + public async Task CreateTankAsync(Tank tank, CancellationToken cancellationToken = default) { tank.Id = Guid.NewGuid(); tank.LastUpdated = DateTime.UtcNow; _tanks.Add(tank); _logger.LogInformation("Created tank {TankName} with ID {TankId}", tank.Name, tank.Id); - return Task.FromResult(tank); + await SaveTanksAsync(); + return tank; } - public Task DeleteTankAsync(Guid id, CancellationToken cancellationToken = default) + public async Task DeleteTankAsync(Guid id, CancellationToken cancellationToken = default) { var tank = _tanks.FirstOrDefault(t => t.Id == id); if (tank != null) { tank.IsActive = false; _logger.LogInformation("Deleted tank {TankName}", tank.Name); + await SaveTanksAsync(); } - - return Task.CompletedTask; } public async Task RefreshAllTankLevelsAsync(CancellationToken cancellationToken = default) diff --git a/src/Backend/VanDaemon.Core/Entities/SystemConfiguration.cs b/src/Backend/VanDaemon.Core/Entities/SystemConfiguration.cs index 31bec2c..7a5a340 100644 --- a/src/Backend/VanDaemon.Core/Entities/SystemConfiguration.cs +++ b/src/Backend/VanDaemon.Core/Entities/SystemConfiguration.cs @@ -30,8 +30,6 @@ public class SystemConfiguration /// public class AlertSettings { - public double TankLowLevelThreshold { get; set; } = 10.0; - public double TankHighLevelThreshold { get; set; } = 90.0; public bool EnableAudioAlerts { get; set; } = true; public bool EnablePushNotifications { get; set; } = false; } diff --git a/src/Backend/VanDaemon.Core/Entities/Tank.cs b/src/Backend/VanDaemon.Core/Entities/Tank.cs index 5e819c6..669162f 100644 --- a/src/Backend/VanDaemon.Core/Entities/Tank.cs +++ b/src/Backend/VanDaemon.Core/Entities/Tank.cs @@ -12,8 +12,8 @@ public class Tank public TankType Type { get; set; } public double CurrentLevel { get; set; } // Percentage (0-100) public double Capacity { get; set; } // Liters - public double LowLevelThreshold { get; set; } = 10.0; // Percentage - public double HighLevelThreshold { get; set; } = 90.0; // Percentage + public double AlertLevel { get; set; } = 20.0; // Percentage + public bool AlertWhenOver { get; set; } = false; // false = alert when under (empty), true = alert when over (full) public string SensorPlugin { get; set; } = string.Empty; public Dictionary SensorConfiguration { get; set; } = new(); public DateTime LastUpdated { get; set; } diff --git a/src/Frontend/VanDaemon.Web/Pages/Index.razor b/src/Frontend/VanDaemon.Web/Pages/Index.razor index f409f5a..349dd43 100644 --- a/src/Frontend/VanDaemon.Web/Pages/Index.razor +++ b/src/Frontend/VanDaemon.Web/Pages/Index.razor @@ -607,12 +607,24 @@ private Color GetTankColor(TankDto tank) { - if (tank.Type == TankType.WasteWater) + bool inAlertState = tank.AlertWhenOver + ? tank.CurrentLevel >= tank.AlertLevel + : tank.CurrentLevel <= tank.AlertLevel; + + if (!inAlertState) + return Color.Success; + + // In alert state + if (tank.AlertWhenOver) { - return tank.CurrentLevel > tank.HighLevelThreshold ? Color.Error : Color.Info; + // Tank getting full + return tank.CurrentLevel >= 95 ? Color.Error : Color.Warning; + } + else + { + // Tank running empty + return tank.CurrentLevel <= 10 ? Color.Error : Color.Warning; } - - return tank.CurrentLevel < tank.LowLevelThreshold ? Color.Error : Color.Success; } public async ValueTask DisposeAsync() @@ -644,8 +656,8 @@ public TankType Type { get; set; } public double CurrentLevel { get; set; } public double Capacity { get; set; } - public double LowLevelThreshold { get; set; } - public double HighLevelThreshold { get; set; } + public double AlertLevel { get; set; } + public bool AlertWhenOver { get; set; } } public class ControlDto diff --git a/src/Frontend/VanDaemon.Web/Pages/Settings.razor b/src/Frontend/VanDaemon.Web/Pages/Settings.razor index a231761..3d5f43f 100644 --- a/src/Frontend/VanDaemon.Web/Pages/Settings.razor +++ b/src/Frontend/VanDaemon.Web/Pages/Settings.razor @@ -124,20 +124,9 @@ else Alert Settings - - - - + + Alert levels are configured individually for each tank in the Tanks page. + - - + + + @(editingTank.AlertWhenOver ? "🔔 Alert when OVER " + editingTank.AlertLevel + "% (tank getting full)" : "🔔 Alert when UNDER " + editingTank.AlertLevel + "% (tank running empty)") + + Sensor Provider Configuration @@ -305,8 +308,8 @@ else Name = "", Type = TankType.FreshWater, Capacity = 100, - LowLevelThreshold = 10, - HighLevelThreshold = 90, + AlertLevel = 20.0, + AlertWhenOver = false, // Default: alert when tank is running empty CurrentLevel = 0 }; providerType = "Simulated"; @@ -329,8 +332,8 @@ else Name = tank.Name, Type = tank.Type, Capacity = tank.Capacity, - LowLevelThreshold = tank.LowLevelThreshold, - HighLevelThreshold = tank.HighLevelThreshold, + AlertLevel = tank.AlertLevel, + AlertWhenOver = tank.AlertWhenOver, CurrentLevel = tank.CurrentLevel, SensorPlugin = tank.SensorPlugin, SensorConfiguration = tank.SensorConfiguration != null @@ -478,16 +481,24 @@ else private Color GetTankProgressColor(TankDto tank) { - if (tank.Type == TankType.WasteWater) + bool inAlertState = tank.AlertWhenOver + ? tank.CurrentLevel > tank.AlertLevel + : tank.CurrentLevel < tank.AlertLevel; + + if (!inAlertState) + return Color.Success; + + // In alert state - check severity + if (tank.AlertWhenOver) { - return tank.CurrentLevel > tank.HighLevelThreshold ? Color.Error : Color.Info; + // Tank getting full + return tank.CurrentLevel >= 95 ? Color.Error : Color.Warning; + } + else + { + // Tank running empty + return tank.CurrentLevel <= 10 ? Color.Error : Color.Warning; } - - if (tank.CurrentLevel < tank.LowLevelThreshold) - return Color.Error; - if (tank.CurrentLevel < tank.LowLevelThreshold * 2) - return Color.Warning; - return Color.Success; } private Color GetTankTypeColor(TankType type) @@ -518,26 +529,41 @@ else private bool IsTankInWarningState(TankDto tank) { - if (tank.Type == TankType.WasteWater) - return tank.CurrentLevel >= tank.HighLevelThreshold; - - return tank.CurrentLevel <= tank.LowLevelThreshold; + return tank.AlertWhenOver + ? tank.CurrentLevel >= tank.AlertLevel + : tank.CurrentLevel <= tank.AlertLevel; } private Severity GetAlertSeverity(TankDto tank) { - if (tank.Type == TankType.WasteWater) + if (tank.AlertWhenOver) + { + // Tank getting full return tank.CurrentLevel >= 95 ? Severity.Error : Severity.Warning; - - return tank.CurrentLevel <= (tank.LowLevelThreshold / 2) ? Severity.Error : Severity.Warning; + } + else + { + // Tank running empty + return tank.CurrentLevel <= 10 ? Severity.Error : Severity.Warning; + } } private string GetWarningMessage(TankDto tank) { - if (tank.Type == TankType.WasteWater) - return tank.CurrentLevel >= 95 ? "Tank almost full!" : "Tank level high"; - - return tank.CurrentLevel <= (tank.LowLevelThreshold / 2) ? "Tank critically low!" : "Tank level low"; + if (tank.AlertWhenOver) + { + // Tank getting full + return tank.CurrentLevel >= 95 + ? $"Tank almost full! ({tank.CurrentLevel:F1}%)" + : $"Tank level high ({tank.CurrentLevel:F1}% > {tank.AlertLevel}%)"; + } + else + { + // Tank running empty + return tank.CurrentLevel <= 10 + ? $"Tank critically low! ({tank.CurrentLevel:F1}%)" + : $"Tank level low ({tank.CurrentLevel:F1}% < {tank.AlertLevel}%)"; + } } public enum TankType @@ -556,8 +582,8 @@ else public TankType Type { get; set; } public double CurrentLevel { get; set; } public double Capacity { get; set; } - public double LowLevelThreshold { get; set; } - public double HighLevelThreshold { get; set; } + public double AlertLevel { get; set; } = 20.0; + public bool AlertWhenOver { get; set; } = false; public string SensorPlugin { get; set; } = "Simulated"; public Dictionary? SensorConfiguration { get; set; } public DateTime LastUpdated { get; set; } From c9b510a036a9a0707ba6e62285f4aa4766287955 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Nov 2025 17:13:03 +0000 Subject: [PATCH 2/3] fix: Update SettingsService and AlertService to use new simplified alert system - Remove TankLowLevelThreshold and TankHighLevelThreshold from AlertSettings initialization - Update AlertService.CheckTankAlertsAsync to use AlertLevel and AlertWhenOver properties - Improve alert messages with threshold information - Dynamic severity determination based on alert direction (over vs under) --- .../Services/AlertService.cs | 53 +++++++++---------- .../Services/SettingsService.cs | 2 - 2 files changed, 26 insertions(+), 29 deletions(-) diff --git a/src/Backend/VanDaemon.Application/Services/AlertService.cs b/src/Backend/VanDaemon.Application/Services/AlertService.cs index c7a0b68..336833c 100644 --- a/src/Backend/VanDaemon.Application/Services/AlertService.cs +++ b/src/Backend/VanDaemon.Application/Services/AlertService.cs @@ -96,39 +96,38 @@ public async Task CheckTankAlertsAsync(CancellationToken cancellationToken = def foreach (var tank in tanks) { var currentLevel = tank.CurrentLevel; + bool inAlertState = tank.AlertWhenOver + ? currentLevel >= tank.AlertLevel // Alert when over (tank getting full) + : currentLevel <= tank.AlertLevel; // Alert when under (tank running empty) - // Check for low level alerts (for fresh water, LPG, fuel) - if (tank.Type is TankType.FreshWater or TankType.LPG or TankType.Fuel or TankType.Battery) + if (inAlertState) { - if (currentLevel <= tank.LowLevelThreshold) + // Determine severity based on how critical the situation is + AlertSeverity severity; + string message; + + if (tank.AlertWhenOver) { - var severity = currentLevel <= tank.LowLevelThreshold / 2 - ? AlertSeverity.Critical - : AlertSeverity.Warning; - - await CreateAlertAsync( - severity, - tank.Id.ToString(), - $"{tank.Name} level is low: {currentLevel:F1}%", - cancellationToken); + // Tank getting full - critical if >= 95% + severity = currentLevel >= 95 ? AlertSeverity.Critical : AlertSeverity.Warning; + message = currentLevel >= 95 + ? $"{tank.Name} is almost full: {currentLevel:F1}%" + : $"{tank.Name} level is high: {currentLevel:F1}% (alert at {tank.AlertLevel:F0}%)"; } - } - - // Check for high level alerts (for waste water) - if (tank.Type == TankType.WasteWater) - { - if (currentLevel >= tank.HighLevelThreshold) + else { - var severity = currentLevel >= 95 - ? AlertSeverity.Critical - : AlertSeverity.Warning; - - await CreateAlertAsync( - severity, - tank.Id.ToString(), - $"{tank.Name} level is high: {currentLevel:F1}%", - cancellationToken); + // Tank running empty - critical if <= 10% + severity = currentLevel <= 10 ? AlertSeverity.Critical : AlertSeverity.Warning; + message = currentLevel <= 10 + ? $"{tank.Name} is critically low: {currentLevel:F1}%" + : $"{tank.Name} level is low: {currentLevel:F1}% (alert at {tank.AlertLevel:F0}%)"; } + + await CreateAlertAsync( + severity, + tank.Id.ToString(), + message, + cancellationToken); } } diff --git a/src/Backend/VanDaemon.Application/Services/SettingsService.cs b/src/Backend/VanDaemon.Application/Services/SettingsService.cs index 5e6d504..064d59f 100644 --- a/src/Backend/VanDaemon.Application/Services/SettingsService.cs +++ b/src/Backend/VanDaemon.Application/Services/SettingsService.cs @@ -28,8 +28,6 @@ private SystemConfiguration InitializeDefaultConfiguration() ToolbarPosition = ToolbarPosition.Left, AlertSettings = new AlertSettings { - TankLowLevelThreshold = 10.0, - TankHighLevelThreshold = 90.0, EnableAudioAlerts = true, EnablePushNotifications = false }, From d0d5021a030080dad011cc56ca655eae6da3bdfb Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 9 Nov 2025 17:16:52 +0000 Subject: [PATCH 3/3] test: Update TankServiceTests to use new JsonFileStore dependency - Add JsonFileStore mock and ILogger mock to test setup - Create temporary directory for test file storage to avoid conflicts - Update TankService constructor call to include JsonFileStore parameter - All existing tests continue to work with new persistence layer --- .../Services/TankServiceTests.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/VanDaemon.Application.Tests/Services/TankServiceTests.cs b/tests/VanDaemon.Application.Tests/Services/TankServiceTests.cs index 67baeba..b3386c3 100644 --- a/tests/VanDaemon.Application.Tests/Services/TankServiceTests.cs +++ b/tests/VanDaemon.Application.Tests/Services/TankServiceTests.cs @@ -1,6 +1,7 @@ using FluentAssertions; using Microsoft.Extensions.Logging; using Moq; +using VanDaemon.Application.Persistence; using VanDaemon.Application.Services; using VanDaemon.Plugins.Abstractions; using Xunit; @@ -10,18 +11,25 @@ namespace VanDaemon.Application.Tests.Services; public class TankServiceTests { private readonly Mock> _loggerMock; + private readonly Mock> _fileStoreLoggerMock; private readonly Mock _sensorPluginMock; + private readonly JsonFileStore _fileStore; private readonly TankService _tankService; public TankServiceTests() { _loggerMock = new Mock>(); + _fileStoreLoggerMock = new Mock>(); _sensorPluginMock = new Mock(); _sensorPluginMock.Setup(x => x.Name).Returns("Simulated Sensor Plugin"); _sensorPluginMock.Setup(x => x.Version).Returns("1.0.0"); - _tankService = new TankService(_loggerMock.Object, new[] { _sensorPluginMock.Object }); + // Create a temporary directory for test file storage + var tempPath = Path.Combine(Path.GetTempPath(), $"vandaemon-tests-{Guid.NewGuid()}"); + _fileStore = new JsonFileStore(_fileStoreLoggerMock.Object, tempPath); + + _tankService = new TankService(_loggerMock.Object, _fileStore, new[] { _sensorPluginMock.Object }); } [Fact]