VanDaemon is an IoT control system for camper vans built with .NET 10, Blazor WebAssembly, and SignalR. It monitors and controls van systems (water tanks, LPG, lighting, heating, electrical) through a modular plugin architecture that supports multiple hardware integration methods including MQTT-based LED dimmers, Modbus devices, and Victron Cerbo GX. The system features a touch-friendly dashboard with draggable overlays, real-time sensor updates, and configurable alerts.
| Layer | Technology | Version | Purpose |
|---|---|---|---|
| Runtime | .NET | 10.0 | Cross-platform backend and frontend |
| Backend | ASP.NET Core Web API | 10.x | REST API with SignalR real-time |
| Frontend | Blazor WebAssembly | 10.x | SPA with offline capability |
| UI Components | MudBlazor | 6.x | Material Design components |
| Real-time | SignalR | 10.x | WebSocket communication |
| Logging | Serilog | 8.x | Structured logging to console/file |
| Testing | xUnit + FluentAssertions + Moq | 2.6/6.12/4.20 | Unit and integration tests |
| E2E Testing | Playwright | 1.49.x | Browser automation tests |
| MQTT | MQTTnet | 4.3.x | LED dimmer communication |
| Modbus | FluentModbus | 5.x | Modbus TCP/RTU communication |
# Prerequisites: .NET 10.0 SDK, Docker (optional)
# Clone and build
git clone <repo>
cd vandaemon
dotnet build VanDaemon.sln
# Run (two terminals)
# Terminal 1 - API
cd src/Backend/VanDaemon.Api && dotnet run
# API: http://localhost:5000, Swagger: http://localhost:5000/swagger
# Terminal 2 - Web UI
cd src/Frontend/VanDaemon.Web && dotnet run
# Web UI: http://localhost:5001
# Run tests
dotnet test VanDaemon.sln
# Run E2E tests (Windows PowerShell)
./run-e2e-tests.ps1
# Docker deployment
docker compose up -d
# Web UI: http://localhost:8080, API: http://localhost:5000vandaemon/
├── src/
│ ├── Backend/
│ │ ├── VanDaemon.Api/ # REST API, SignalR hub, background services
│ │ ├── VanDaemon.Core/ # Domain entities and enums
│ │ ├── VanDaemon.Application/ # Services, interfaces, JSON persistence
│ │ ├── VanDaemon.Infrastructure/# Future SQLite (currently empty)
│ │ └── VanDaemon.Plugins/ # Hardware integration plugins
│ │ ├── Abstractions/ # IHardwarePlugin, ISensorPlugin, IControlPlugin
│ │ ├── Simulated/ # Development/testing plugins
│ │ ├── Modbus/ # Modbus TCP/RTU devices
│ │ ├── I2C/ # Direct I2C sensors
│ │ ├── Victron/ # Cerbo GX via MQTT
│ │ └── MqttLedDimmer/ # ESP32 LED dimmer control
│ └── Frontend/
│ └── VanDaemon.Web/ # Blazor WASM application
│ └── Pages/ # Dashboard, Tanks, Controls, Devices, Settings
├── tests/
│ ├── VanDaemon.Api.Tests/
│ ├── VanDaemon.Application.Tests/
│ ├── VanDaemon.Plugins.Modbus.Tests/
│ └── VanDaemon.E2E.Tests/ # Playwright browser tests
├── hw/
│ └── LEDDimmer/ # ESP32 8-channel PWM LED dimmer (KiCad + Arduino)
├── docker/ # Dockerfile.api, Dockerfile.web, Dockerfile.combined
├── tools/
│ └── CerboGXTest/ # Victron MQTT test tool
└── docs/
VanDaemon follows Clean Architecture with these layers:
┌─────────────────────────────────────────────────────────┐
│ Frontend (Blazor WASM) │
│ Pages: Dashboard, Tanks, Controls, Devices, Settings │
│ SignalR client for real-time updates │
└─────────────────────────────┬───────────────────────────┘
│ HTTP/WebSocket
┌─────────────────────────────▼───────────────────────────┐
│ API Layer │
│ Controllers (thin), TelemetryHub, BackgroundService │
└─────────────────────────────┬───────────────────────────┘
│
┌─────────────────────────────▼───────────────────────────┐
│ Application Layer │
│ Services: Tank, Control, Alert, Settings, Electrical │
│ JsonFileStore for configuration persistence │
└─────────────┬───────────────────────────┬───────────────┘
│ │
┌─────────────▼─────────────┐ ┌───────────▼───────────────┐
│ Core Layer │ │ Plugin System │
│ Entities, Enums │ │ Simulated, Modbus, │
│ No external dependencies │ │ MqttLedDimmer, Victron │
└───────────────────────────┘ └───────────────────────────┘
| Entity | Location | Purpose |
|---|---|---|
Tank |
Core/Entities | Water, waste, LPG, fuel monitoring with alert thresholds |
Control |
Core/Entities | Switches, dimmers, momentary buttons |
Alert |
Core/Entities | System alerts with severity levels (Info, Warning, Error, Critical) |
SystemConfiguration |
Core/Entities | Van settings, theme, toolbar position, driving side |
ElectricalDevice |
Core/Entities | Electrical system components |
ElectricalSystem |
Core/Entities | Overall electrical system state |
| Service | Interface | Purpose |
|---|---|---|
TankService |
ITankService |
Tank CRUD, level monitoring, sensor integration |
ControlService |
IControlService |
Control state management, plugin coordination |
AlertService |
IAlertService |
Alert generation, acknowledgment, clearing |
SettingsService |
ISettingsService |
System configuration persistence |
ElectricalService |
IElectricalService |
Electrical system monitoring |
ElectricalDeviceService |
IElectricalDeviceService |
Device management |
UnifiedConfigService |
IUnifiedConfigService |
Combined configuration management |
Plugins implement ISensorPlugin (reading) or IControlPlugin (actuating):
// Plugin registration in Program.cs
builder.Services.AddSingleton<ISensorPlugin, SimulatedSensorPlugin>();
builder.Services.AddSingleton<IControlPlugin, ModbusControlPlugin>();
builder.Services.AddSingleton<MqttLedDimmerPlugin>();
builder.Services.AddSingleton<IControlPlugin>(sp => sp.GetRequiredService<MqttLedDimmerPlugin>());
// Initialize after app.Build()
var plugins = app.Services.GetServices<IControlPlugin>();
foreach (var plugin in plugins)
await plugin.InitializeAsync(config);Available Plugins:
Simulated- Generates realistic fake data for development/testingModbus- Industrial Modbus TCP/RTU protocol (fully implemented)MqttLedDimmer- ESP32-based 8-channel PWM LED controller via MQTTVictron- Cerbo GX integration via MQTT (placeholder)I2C- Direct I2C sensor integration (placeholder)
Creating New Plugins:
- Reference
VanDaemon.Plugins.Abstractions - Implement
ISensorPluginorIControlPlugin - Accept
ILogger<T>via constructor injection - Store plugin state in private fields/dictionaries
- Implement
IDisposablefor cleanup - Register in
Program.csas singleton - Initialize after
app.Build()with configuration dictionary
Hub: /hubs/telemetry
Groups: tanks, controls, alerts, electrical
Client Methods: SubscribeToTanks(), SubscribeToControls(), SubscribeToAlerts(), SubscribeToElectrical()
Server Events: TankLevelUpdated, ControlStateChanged, AlertsUpdated
Background service (TelemetryBackgroundService) polls sensors every 5 seconds (configurable via VanDaemon:RefreshIntervalSeconds).
Two-tier storage model:
-
Configuration (Persistent) - JSON files via
JsonFileStore- Location:
{AppContext.BaseDirectory}/data/ - Files:
tanks.json,controls.json,alerts.json,settings.json - Thread-safe with
SemaphoreSlim
- Location:
-
Real-time Data (Volatile) - In-memory only
- Tank levels, control states stored in service
List<T>fields - Live sensor readings never persisted
- Tank levels, control states stored in service
| Command | Description |
|---|---|
dotnet build VanDaemon.sln |
Build solution |
dotnet build --configuration Release |
Release build |
dotnet clean VanDaemon.sln |
Clean build artifacts |
dotnet test VanDaemon.sln |
Run all tests |
dotnet test --verbosity normal |
Tests with output |
dotnet test --collect:"XPlat Code Coverage" |
Tests with coverage |
./run-e2e-tests.ps1 |
E2E tests (starts API+Web) |
./run-e2e-tests.ps1 -Headless $false -SlowMo 500 |
E2E with visible browser |
docker compose up -d |
Start Docker containers |
docker compose logs -f |
View container logs |
| Environment | API | Web UI | SignalR |
|---|---|---|---|
| Development | 5000 | 5001 | ws://localhost:5000/hubs/telemetry |
| Docker | 5000 | 8080 | ws://localhost:5000/hubs/telemetry |
Important: Frontend loads API URL from wwwroot/appsettings.json in development, uses same-origin in production.
- Create interface:
Application/Interfaces/I{Name}Service.cs - Implement:
Application/Services/{Name}Service.cs - Accept dependencies via constructor injection
- Register in
Program.cs:builder.Services.AddSingleton<I{Name}Service, {Name}Service>() - Consider persistence needs (add JsonFileStore calls if needed)
- Create/extend controller in
Api/Controllers/ - Inject service via constructor
- Use
[HttpGet],[HttpPost]attributes - Return
ActionResult<T>, acceptCancellationToken - Routing is automatic via
[ApiController]and[Route("api/[controller]")]
- Add enum value to
VanDaemon.Core/Enums/TankType.cs - No other code changes needed (dynamic configuration)
await _hubContext.Clients.Group("tanks").SendAsync(
"TankLevelUpdated", tankId, currentLevel, tankName, cancellationToken);Framework: xUnit + FluentAssertions + Moq
// Example test pattern
var mockService = new Mock<ITankService>();
mockService.Setup(x => x.GetAllTanksAsync(It.IsAny<CancellationToken>()))
.ReturnsAsync(testData);
result.Should().NotBeNull();
result.Should().HaveCount(3);
result.Should().AllSatisfy(t => t.IsActive.Should().BeTrue());
// JsonFileStore testing - use temporary directory
var tempPath = Path.Combine(Path.GetTempPath(), $"vandaemon-tests-{Guid.NewGuid()}");
var fileStore = new JsonFileStore(loggerMock.Object, tempPath);Test Projects:
VanDaemon.Api.Tests- Controller and hub testsVanDaemon.Application.Tests- Service and JsonFileStore testsVanDaemon.Plugins.Modbus.Tests- Modbus plugin testsVanDaemon.E2E.Tests- Playwright browser automation
{
"VanDaemon": {
"RefreshIntervalSeconds": 5,
"EnableSimulatedPlugins": true
},
"MqttLedDimmer": {
"MqttBroker": "localhost",
"MqttPort": 1883,
"BaseTopic": "vandaemon/leddimmer",
"AutoDiscovery": true
}
}| Variable | Description |
|---|---|
ASPNETCORE_ENVIRONMENT |
Production/Development |
API_PORT, WEB_PORT |
Network ports (5000, 8080) |
DEFAULT_SENSOR_PLUGIN, DEFAULT_CONTROL_PLUGIN |
Plugin selection |
MODBUS_IP_ADDRESS, MODBUS_PORT |
Modbus connection |
VICTRON_MQTT_BROKER, VICTRON_DEVICE_ID |
Victron config |
docker compose up -d
# Web: http://localhost:8080, API: http://localhost:5000Two-container setup: vandaemon-api + vandaemon-web (nginx)
flyctl deploy
# Single container with nginx + .NET APIAuto-deployment via .github/workflows/deploy-fly.yml
- Install Docker:
curl -fsSL https://get.docker.com | sh - Clone repo and run
docker compose up -d - Enable I2C:
sudo raspi-config → Interface Options → I2C - Add systemd service for auto-start (see README.md)
ESP32-based 8-channel PWM LED controller with MQTT communication:
- Firmware: Arduino/PlatformIO (
led_dimmer.ino) - PCB Design: KiCad (see
HARDWARE_V2_DESIGN.md,PCB_LAYOUT_GUIDE.md) - Build:
pio run -e 8ch -t upload - MQTT Topics:
vandaemon/leddimmer/{deviceId}/channel/{N}/set
Note: KiCad doesn't support semicolon comments
See hw/LEDDimmer/README.md for full documentation.
- Files: PascalCase for C# files (
TankService.cs,Tank.cs) - Classes/Interfaces: PascalCase, interfaces prefixed with
I(ITankService) - Methods: PascalCase, async suffix for async methods (
GetAllTanksAsync) - Variables: camelCase for locals and parameters (
tankLevel),_camelCasefor private fields (_logger) - Properties: PascalCase (
CurrentLevel,IsActive)
- Nullable reference types enabled (
<Nullable>enable</Nullable>) - All async methods accept optional
CancellationTokenparameter - Structured logging:
_logger.LogInformation("Tank {TankId} updated to {Level}%", tankId, level) - Soft deletes: Use
IsActive = falseinstead of removing entities - Plugin config:
Dictionary<string, object>(JSON-serializable types only) - JSON enums serialized as strings via
JsonStringEnumConverter
- Singletons: All services, plugins,
JsonFileStore,TelemetryService,SettingsStateService,IHubContext<TelemetryHub> - Scoped: Controllers (automatic by ASP.NET Core)
- Hosted Services:
TelemetryBackgroundService(sensor polling),MqttLedDimmerService(MQTT device discovery)
-
Background Service Scope: Create new scope when accessing services from background services (they're singletons)
-
Plugin Initialization: Must happen after
app.Build()but beforeapp.Run()in Program.cs -
JsonFileStore Thread Safety: Always
awaitoperations - concurrent access protected bySemaphoreSlim -
SignalR Subscriptions: Clients must call
SubscribeToTanks()etc. before receiving group broadcasts -
CORS in Development: Frontend (5001) → API (5000) requires explicit CORS config (handled in Program.cs)
-
Docker Networking: Services use container names (
http://api:80) not localhost -
Control.State Type: Cast based on
ControlType(bool for Toggle, int for Dimmer 0-255) -
Alert Thresholds:
AlertWhenOver=falsealerts when level drops below threshold (consumables),=truewhen above (waste)
| Endpoint | Method | Description |
|---|---|---|
/api/tanks |
GET | List all active tanks |
/api/tanks/{id} |
GET/PUT/DELETE | Tank CRUD operations |
/api/tanks/{id}/level |
GET | Current tank level |
/api/tanks/refresh |
POST | Refresh all tank levels from sensors |
/api/controls |
GET | List all active controls |
/api/controls/{id}/state |
POST | Set control state |
/api/electrical |
GET | Electrical system state |
/api/settings |
GET/PUT | System configuration |
/api/settings/overlay-positions |
GET/POST | Dashboard overlay positions |
/health |
GET | Health check (returns status + timestamp) |
- @README.md - Full project documentation with troubleshooting
- @PROJECT_PLAN.md - Development roadmap
- @DEPLOYMENT.md - Fly.io deployment guide
- @DOCKER.md - Docker configuration details
- @hw/LEDDimmer/README.md - LED dimmer hardware and MQTT integration
When working on tasks involving these technologies, invoke the corresponding skill:
| Skill | Invoke When |
|---|---|
| dotnet | Configures .NET 10 projects, builds, and manages C# runtime |
| moq | Creates mock objects and configures test dependencies with Moq |
| xunit | Writes unit tests and integration tests with xUnit framework |
| playwright | Automates browser testing and E2E test scenarios with Playwright |
| aspnet-core | Builds REST APIs, SignalR hubs, and ASP.NET Core Web API applications |
| mudblazor | Creates Material Design UI components and touch-optimized interfaces |
| blazor | Develops Blazor WebAssembly single-page applications with component-based UI |
| csharp | Writes C# code following VanDaemon conventions and clean architecture patterns |
| signalr | Implements real-time WebSocket communication and SignalR hub subscriptions |
| fluent-assertions | Writes fluent, readable test assertions and validations |
| serilog | Implements structured logging and configures Serilog sinks |
| platformio | Builds and deploys ESP32 firmware with PlatformIO |
| mqttnet | Manages MQTT broker connections and message publishing/subscription |
| kicad | Designs PCB schematics and circuit layouts for hardware projects |
| docker | Configures containerization with Docker and Docker Compose |
| frontend-design | Applies UI design with Material Design components and SVG diagrams |