Skip to content
Open
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
42 changes: 42 additions & 0 deletions OCPPChargerSim/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -135,12 +135,53 @@
}
});

// ---------------------------------------------------------------------------
// External meter values endpoint
// Called by Home Assistant (or any external source) to push real meter data.
// The simulator will use these values in the next MeterValues message to Octopus.
// POST /api/meters
// {
// "energyWhImport": 256937, // Energy.Active.Import.Register in Wh
// "powerKwImport": 3.45, // Power.Active.Import in kW
// "frequencyHz": 50.01, // Frequency in Hz
// "powerKwOffered": 3.45, // Power.Offered in kW (optional — calculated from currentAmpsOffered if omitted)
// "currentAmpsImport": 15.0, // Current.Import — actual draw in A
// "currentAmpsOffered": 15.0, // Current.Offered in A (optional — uses chargingALimitConn1 if omitted)
// "stateOfChargePercent": 42.0 // SoC in % (optional)
// }
// All fields are optional — omit any you don't have and the simulator will
// fall back to its own calculated value for that measurand.
// ---------------------------------------------------------------------------
app.MapPost("/api/meters", (OcppSimulator.ExternalMeterValues values, SimulatorState state) =>
{
state.SetExternalMeterValues(values);
return Results.Accepted();
});

app.MapGet("/api/meters", (SimulatorState state) =>
{
var values = state.GetExternalMeterValues();
if (values is null)
{
return Results.Ok(new { source = "simulated", values = (object?)null });
}

return Results.Ok(new { source = "external", values });
});

app.MapDelete("/api/meters", (SimulatorState state) =>
{
state.SetExternalMeterValues(null);
return Results.Accepted();
});

app.MapGet("/api/state", (SimulatorState state, SimulatorConfigurationProvider configProvider, ChargerCatalog catalog) =>
{
var sample = state.LatestSample;
var (url, identity, authKey) = state.GetConnectionDetails();
var (requiresConfiguration, configFileMissing) = state.ConfigurationStatus;
var (chargePointSerial, chargeBoxSerial) = state.GetSerialNumbers();
var externalMeters = state.GetExternalMeterValues();
return Results.Ok(new
{
vehicleState = state.VehicleState,
Expand Down Expand Up @@ -168,6 +209,7 @@
}),
selectedCharger = state.SelectedChargerId,
serialNumbers = new { chargePointSerial, chargeBoxSerial },
externalMeterSource = externalMeters is not null ? "external" : "simulated",
});
});

Expand Down
5 changes: 5 additions & 0 deletions OCPPChargerSim/Services/SimulatorHostedService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using Microsoft.AspNetCore.SignalR;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

using OcppSimulator;
using OcppWeb.Hubs;

Expand Down Expand Up @@ -130,6 +131,10 @@ private async Task RunWithConfigurationAsync(SimulatorConfigurationSnapshot snap
supportSoC: options.SupportSoC,
enableHeartbeat: options.SupportHeartbeat);

// Wire up Home Assistant / external meter values so the client always
// reads the latest pushed values when it builds a MeterValues payload.
client.SetExternalMeterValuesProvider(() => _state.GetExternalMeterValues());

_coordinator.Attach(client, logger);
_state.SetVehicleState(client.VehicleState);
_state.SetConfigurationSnapshot(client.ConfigurationSnapshot);
Expand Down
39 changes: 39 additions & 0 deletions OCPPChargerSim/Services/SimulatorState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public sealed class SimulatorState
private string? _selectedChargerId;
private string _chargePointSerial = "0";
private string _chargeBoxSerial = "0";
private OcppSimulator.ExternalMeterValues? _externalMeterValues;

public void AddLog(string message)
{
Expand Down Expand Up @@ -221,4 +222,42 @@ public IReadOnlyDictionary<string, string> GetBootConfiguration()
return new Dictionary<string, string>(_bootConfiguration);
}
}

/// <summary>
/// Stores the latest meter values received from an external source (e.g. Home Assistant).
/// Pass null to clear and revert to simulated values.
/// </summary>
public void SetExternalMeterValues(OcppSimulator.ExternalMeterValues? values)
{
lock (_sync)
{
_externalMeterValues = values;
}
}

/// <summary>
/// Returns a snapshot of the latest external meter values, or null if none have been received.
/// </summary>
public OcppSimulator.ExternalMeterValues? GetExternalMeterValues()
{
lock (_sync)
{
if (_externalMeterValues is null)
{
return null;
}

// Return a copy so callers can't mutate shared state
return new OcppSimulator.ExternalMeterValues
{
EnergyWhImport = _externalMeterValues.EnergyWhImport,
PowerKwImport = _externalMeterValues.PowerKwImport,
FrequencyHz = _externalMeterValues.FrequencyHz,
PowerKwOffered = _externalMeterValues.PowerKwOffered,
CurrentAmpsImport = _externalMeterValues.CurrentAmpsImport,
CurrentAmpsOffered = _externalMeterValues.CurrentAmpsOffered,
StateOfChargePercent = _externalMeterValues.StateOfChargePercent,
};
}
}
}
Loading