Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions src/Backend/VanDaemon.Api/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -41,6 +42,10 @@
});
});

// Register JSON file persistence
builder.Services.AddSingleton(sp =>
new JsonFileStore(sp.GetRequiredService<ILogger<JsonFileStore>>(), Path.Combine(AppContext.BaseDirectory, "data")));

// Register plugins
builder.Services.AddSingleton<ISensorPlugin, SimulatedSensorPlugin>();
builder.Services.AddSingleton<IControlPlugin, SimulatedControlPlugin>();
Expand Down
138 changes: 138 additions & 0 deletions src/Backend/VanDaemon.Application/Persistence/JsonFileStore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
using System.Text.Json;
using Microsoft.Extensions.Logging;

namespace VanDaemon.Application.Persistence;

/// <summary>
/// Simple JSON file-based persistence for application data
/// </summary>
public class JsonFileStore
{
private readonly string _dataDirectory;
private readonly ILogger<JsonFileStore> _logger;
private readonly JsonSerializerOptions _jsonOptions;
private static readonly SemaphoreSlim _semaphore = new(1, 1);

public JsonFileStore(ILogger<JsonFileStore> 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
};
}

/// <summary>
/// Save data to a JSON file
/// </summary>
public async Task SaveAsync<T>(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();
}
}

/// <summary>
/// Load data from a JSON file
/// </summary>
public async Task<T?> LoadAsync<T>(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<T>(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();
}
}

/// <summary>
/// Delete a JSON file
/// </summary>
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();
}
}

/// <summary>
/// Check if a file exists
/// </summary>
public bool Exists(string fileName)
{
var filePath = Path.Combine(_dataDirectory, fileName);
return File.Exists(filePath);
}

/// <summary>
/// Get all files in the data directory
/// </summary>
public IEnumerable<string> GetAllFiles()
{
return Directory.GetFiles(_dataDirectory, "*.json")
.Select(Path.GetFileName)
.Where(f => f != null)
.Select(f => f!);
}
}
53 changes: 26 additions & 27 deletions src/Backend/VanDaemon.Application/Services/AlertService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand Down
62 changes: 52 additions & 10 deletions src/Backend/VanDaemon.Application/Services/ControlService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using Microsoft.Extensions.Logging;
using VanDaemon.Application.Interfaces;
using VanDaemon.Application.Persistence;
using VanDaemon.Core.Entities;
using VanDaemon.Plugins.Abstractions;

Expand All @@ -11,15 +12,55 @@ namespace VanDaemon.Application.Services;
public class ControlService : IControlService
{
private readonly ILogger<ControlService> _logger;
private readonly JsonFileStore _fileStore;
private readonly List<Control> _controls;
private readonly Dictionary<string, IControlPlugin> _controlPlugins;
private const string ControlsFileName = "controls.json";

public ControlService(ILogger<ControlService> logger, IEnumerable<IControlPlugin> controlPlugins)
public ControlService(ILogger<ControlService> logger, JsonFileStore fileStore, IEnumerable<IControlPlugin> controlPlugins)
{
_logger = logger;
_fileStore = fileStore;
_controls = new List<Control>();
_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<List<Control>>(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()
Expand Down Expand Up @@ -61,7 +102,7 @@ private void InitializeDefaultControls()
ControlConfiguration = new Dictionary<string, object> { ["controlId"] = "water_pump" },
LastUpdated = DateTime.UtcNow,
IsActive = true,
IconName = "water_pump"
IconName = "water_drop"
});

_controls.Add(new Control
Expand Down Expand Up @@ -154,7 +195,7 @@ public async Task<bool> SetControlStateAsync(Guid id, object state, Cancellation
return false;
}

public Task<Control> UpdateControlAsync(Control control, CancellationToken cancellationToken = default)
public async Task<Control> UpdateControlAsync(Control control, CancellationToken cancellationToken = default)
{
var existingControl = _controls.FirstOrDefault(c => c.Id == control.Id);
if (existingControl != null)
Expand All @@ -165,27 +206,28 @@ public Task<Control> 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<Control> CreateControlAsync(Control control, CancellationToken cancellationToken = default)
public async Task<Control> 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;
}
}
2 changes: 0 additions & 2 deletions src/Backend/VanDaemon.Application/Services/SettingsService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@ private SystemConfiguration InitializeDefaultConfiguration()
ToolbarPosition = ToolbarPosition.Left,
AlertSettings = new AlertSettings
{
TankLowLevelThreshold = 10.0,
TankHighLevelThreshold = 90.0,
EnableAudioAlerts = true,
EnablePushNotifications = false
},
Expand Down
Loading
Loading