+
}
@code {
- private DtoPvValues? _pvValues;
+ private int? PowerBuffer { get; set; }
+ private int? _lastSavedPowerBuffer;
private bool _displayValue;
protected override async Task OnInitializedAsync()
@@ -30,21 +33,23 @@
_displayValue = result.Value;
if(_displayValue)
{
- _pvValues = await HttpClient.GetFromJsonAsync
("api/Index/GetPvValues").ConfigureAwait(false);
+ var powerBufferResult = await HttpClient.GetFromJsonAsync>("api/BaseConfiguration/PowerBuffer").ConfigureAwait(false);
+ if (powerBufferResult != default)
+ {
+ _lastSavedPowerBuffer = powerBufferResult.Value;
+ PowerBuffer = powerBufferResult.Value;
+ }
}
}
private async Task UpdatePowerBuffer()
{
- if(_pvValues == default)
- {
- return;
- }
- var response = await HttpClient.GetAsync($"api/BaseConfiguration/UpdatePowerBuffer?powerBuffer={_pvValues.PowerBuffer ?? 0}").ConfigureAwait(false);
+ var response = await HttpClient.GetAsync($"api/BaseConfiguration/UpdatePowerBuffer?powerBuffer={PowerBuffer ?? 0}").ConfigureAwait(false);
if (response.IsSuccessStatusCode)
{
Snackbar.Add("Power Buffer updated", Severity.Success);
+ _lastSavedPowerBuffer = PowerBuffer;
}
else
{
diff --git a/TeslaSolarCharger/Client/Components/RestValueConfigurationComponent.razor b/TeslaSolarCharger/Client/Components/RestValueConfigurationComponent.razor
index 72a8db578..528044f17 100644
--- a/TeslaSolarCharger/Client/Components/RestValueConfigurationComponent.razor
+++ b/TeslaSolarCharger/Client/Components/RestValueConfigurationComponent.razor
@@ -34,7 +34,7 @@
{
CloseButton = true,
CloseOnEscapeKey = false,
- DisableBackdropClick = true,
+ BackdropClick = true,
};
var parameters = new DialogParameters
{
diff --git a/TeslaSolarCharger/Client/Pages/HandledChargesList.razor b/TeslaSolarCharger/Client/Pages/HandledChargesList.razor
index 50c722802..bf1bd5d54 100644
--- a/TeslaSolarCharger/Client/Pages/HandledChargesList.razor
+++ b/TeslaSolarCharger/Client/Pages/HandledChargesList.razor
@@ -18,7 +18,18 @@ else
Height="@datagridHeight" Breakpoint="Breakpoint.None">
+ Title="@StringHelper.GenerateFriendlyStringWithOutIdSuffix(nameof(DtoHandledCharge.StartTime))">
+
+ @if(context.Item.EndTime.HasValue)
+ {
+ @context.Item.StartTime
+ }
+ else
+ {
+ @context.Item.StartTime
+ }
+
+
diff --git a/TeslaSolarCharger/Client/Pages/Index.razor b/TeslaSolarCharger/Client/Pages/Index.razor
index d19191423..05e82e597 100644
--- a/TeslaSolarCharger/Client/Pages/Index.razor
+++ b/TeslaSolarCharger/Client/Pages/Index.razor
@@ -322,7 +322,7 @@ else
}
-
+
@if (_newCarDetailStates.Any(c => c.Key == car.CarId))
{
diff --git a/TeslaSolarCharger/Client/Pages/TimeSeriesChart.razor b/TeslaSolarCharger/Client/Pages/TimeSeriesChart.razor
new file mode 100644
index 000000000..d63153dc0
--- /dev/null
+++ b/TeslaSolarCharger/Client/Pages/TimeSeriesChart.razor
@@ -0,0 +1,69 @@
+@page "/TimeSeries/{carId:int}/{startEpoch:long}/{endEpoch:long}"
+
+@using MudBlazor.Components.Chart.Models
+@using TeslaSolarCharger.Shared.Dtos.TimeSeries
+@using TeslaSolarCharger.Shared.Enums
+@using TeslaSolarCharger.Shared.Helper.Contracts
+@inject HttpClient Http
+@inject IStringHelper StringHelper
+
+
+
+
+
+@code
+{
+ [Parameter]
+ public int CarId { get; set; }
+ [Parameter]
+ public long StartEpoch { get; set; }
+ [Parameter]
+ public long EndEpoch { get; set; }
+
+
+ private readonly ChartOptions _options = new ChartOptions
+ {
+ YAxisLines = false,
+ YAxisRequireZeroPoint = true,
+ XAxisLines = false,
+ LineStrokeWidth = 1,
+ };
+
+ private readonly List _series = new();
+
+ protected override async Task OnParametersSetAsync()
+ {
+ await LoadTimeSeriesData();
+ }
+
+ private async Task LoadTimeSeriesData()
+ {
+ await AddChartSeries(CarValueType.ModuleTempMin).ConfigureAwait(false);
+ await AddChartSeries(CarValueType.ModuleTempMax).ConfigureAwait(false);
+ await AddChartSeries(CarValueType.StateOfCharge).ConfigureAwait(false);
+ await AddChartSeries(CarValueType.StateOfChargeLimit).ConfigureAwait(false);
+
+ StateHasChanged();
+ }
+
+ private async Task AddChartSeries(CarValueType carValueType)
+ {
+ var response = await Http.GetFromJsonAsync>($"api/TimeSeriesData/GetTimeSeriesData?carId={CarId}&startEpoch={StartEpoch}&endEpoch={EndEpoch}&carValueType={carValueType}");
+ if (response != null && response.Count > 0)
+ {
+ var chartSeries = new TimeSeriesChartSeries
+ {
+ Index = 0,
+ Name = StringHelper.GenerateFriendlyStringFromPascalString(carValueType.ToString()),
+ Data = response.Select(d => new TimeSeriesChartSeries.TimeValue(d.Timestamp, d.Value ?? 0)).ToList(),
+ IsVisible = true,
+ Type = TimeSeriesDiplayType.Line,
+ };
+ _series.Add(chartSeries);
+ }
+ }
+}
\ No newline at end of file
diff --git a/TeslaSolarCharger/Client/Shared/MainLayout.razor b/TeslaSolarCharger/Client/Shared/MainLayout.razor
index 5f0f55851..762c06a55 100644
--- a/TeslaSolarCharger/Client/Shared/MainLayout.razor
+++ b/TeslaSolarCharger/Client/Shared/MainLayout.razor
@@ -7,6 +7,7 @@
@inject HttpClient HttpClient
+
@@ -36,7 +37,7 @@
readonly MudTheme _tscTheme = new MudTheme()
{
- Palette = new PaletteLight()
+ PaletteLight = new PaletteLight()
{
Primary = "#1b6ec2",
Secondary = "#6c757d",
diff --git a/TeslaSolarCharger/Client/TeslaSolarCharger.Client.csproj b/TeslaSolarCharger/Client/TeslaSolarCharger.Client.csproj
index 9c16febf7..6bdbad9e9 100644
--- a/TeslaSolarCharger/Client/TeslaSolarCharger.Client.csproj
+++ b/TeslaSolarCharger/Client/TeslaSolarCharger.Client.csproj
@@ -23,12 +23,12 @@
-
-
+
+
-
+
-
+
diff --git a/TeslaSolarCharger/Server/Contracts/IBaseConfigurationService.cs b/TeslaSolarCharger/Server/Contracts/IBaseConfigurationService.cs
index 30ce4c67c..a4eef337c 100644
--- a/TeslaSolarCharger/Server/Contracts/IBaseConfigurationService.cs
+++ b/TeslaSolarCharger/Server/Contracts/IBaseConfigurationService.cs
@@ -8,7 +8,7 @@ public interface IBaseConfigurationService
{
Task UpdateBaseConfigurationAsync(DtoBaseConfiguration baseConfiguration);
Task UpdateMaxCombinedCurrent(int? maxCombinedCurrent);
- void UpdatePowerBuffer(int powerBuffer);
+ Task UpdatePowerBuffer(int powerBuffer);
Task DownloadBackup(string backupFileNamePrefix, string? backupZipDestinationDirectory);
Task RestoreBackup(IFormFile file);
Task CreateLocalBackupZipFile(string backupFileNamePrefix, string? backupZipDestinationDirectory, bool clearBackupDirectoryBeforeBackup);
diff --git a/TeslaSolarCharger/Server/Controllers/BaseConfigurationController.cs b/TeslaSolarCharger/Server/Controllers/BaseConfigurationController.cs
index 119d579ee..09214c8b1 100644
--- a/TeslaSolarCharger/Server/Controllers/BaseConfigurationController.cs
+++ b/TeslaSolarCharger/Server/Controllers/BaseConfigurationController.cs
@@ -27,7 +27,10 @@ public Task UpdateMaxCombinedCurrent(int? maxCombinedCurrent) =>
service.UpdateMaxCombinedCurrent(maxCombinedCurrent);
[HttpGet]
- public void UpdatePowerBuffer(int powerBuffer) =>
+ public DtoValue PowerBuffer() => new(configurationWrapper.PowerBuffer());
+
+ [HttpGet]
+ public Task UpdatePowerBuffer(int powerBuffer) =>
service.UpdatePowerBuffer(powerBuffer);
[HttpGet]
diff --git a/TeslaSolarCharger/Server/Controllers/TimeSeriesDataController.cs b/TeslaSolarCharger/Server/Controllers/TimeSeriesDataController.cs
new file mode 100644
index 000000000..2a4161b55
--- /dev/null
+++ b/TeslaSolarCharger/Server/Controllers/TimeSeriesDataController.cs
@@ -0,0 +1,14 @@
+using Microsoft.AspNetCore.Mvc;
+using TeslaSolarCharger.Server.Services.Contracts;
+using TeslaSolarCharger.Shared.Dtos.TimeSeries;
+using TeslaSolarCharger.Shared.Enums;
+using TeslaSolarCharger.SharedBackend.Abstracts;
+
+namespace TeslaSolarCharger.Server.Controllers;
+
+public class TimeSeriesDataController(ITimeSeriesDataService service) : ApiBaseController
+{
+ [HttpGet]
+ public Task> GetTimeSeriesData(int carId, long startEpoch, long endEpoch, CarValueType carValueType) =>
+ service.GetTimeSeriesData(carId, startEpoch, endEpoch, carValueType);
+}
diff --git a/TeslaSolarCharger/Server/Program.cs b/TeslaSolarCharger/Server/Program.cs
index 1ac661da0..2a3142bdd 100644
--- a/TeslaSolarCharger/Server/Program.cs
+++ b/TeslaSolarCharger/Server/Program.cs
@@ -118,13 +118,6 @@ async Task DoStartupStuff(WebApplication webApplication, ILogger logger
var baseConfigurationService = webApplication.Services.GetRequiredService();
var teslaMateContextWrapper = webApplication.Services.GetRequiredService();
var teslaMateContext = teslaMateContextWrapper.GetTeslaMateContextIfAvailable();
- //This needs to be done before first base configuration update otherwise all TeslaMate values are removed
- if (teslaMateContext != default)
- {
- baseConfiguration.UseTeslaMateIntegration = true;
- baseConfiguration.UseTeslaMateAsDataSource = true;
- }
- await baseConfigurationService.UpdateBaseConfigurationAsync(baseConfiguration);
if (teslaMateContext != default)
{
try
diff --git a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs
index 8bd30eac0..9ea7ce85b 100644
--- a/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs
+++ b/TeslaSolarCharger/Server/ServiceCollectionExtensions.cs
@@ -114,6 +114,7 @@ public static IServiceCollection AddMyDependencies(this IServiceCollection servi
.AddTransient()
.AddTransient()
.AddSingleton()
+ .AddSingleton()
.AddSharedBackendDependencies();
return services;
}
diff --git a/TeslaSolarCharger/Server/Services/ApiServices/IndexService.cs b/TeslaSolarCharger/Server/Services/ApiServices/IndexService.cs
index 61dc589df..9edd92dda 100644
--- a/TeslaSolarCharger/Server/Services/ApiServices/IndexService.cs
+++ b/TeslaSolarCharger/Server/Services/ApiServices/IndexService.cs
@@ -33,7 +33,7 @@ public class IndexService(
public DtoPvValues GetPvValues()
{
logger.LogTrace("{method}()", nameof(GetPvValues));
- int? powerBuffer = configurationWrapper.PowerBuffer(true);
+ int? powerBuffer = configurationWrapper.PowerBuffer();
if (settings.InverterPower == null && settings.Overage == null)
{
powerBuffer = null;
diff --git a/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs b/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs
index 520d6f392..31b95cd8f 100644
--- a/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs
+++ b/TeslaSolarCharger/Server/Services/BaseConfigurationService.cs
@@ -17,7 +17,6 @@ public class BaseConfigurationService(
JobManager jobManager,
ITeslaMateMqttService teslaMateMqttService,
ISettings settings,
- IPvValueService pvValueService,
IDbConnectionStringHelper dbConnectionStringHelper,
IConstants constants)
: IBaseConfigurationService
@@ -32,7 +31,6 @@ public async Task UpdateBaseConfigurationAsync(DtoBaseConfiguration baseConfigur
{
await teslaMateMqttService.ConnectClientIfNotConnected().ConfigureAwait(false);
}
- settings.PowerBuffer = null;
if (restartNeeded)
{
@@ -47,9 +45,11 @@ public async Task UpdateMaxCombinedCurrent(int? maxCombinedCurrent)
await configurationWrapper.UpdateBaseConfigurationAsync(baseConfiguration).ConfigureAwait(false);
}
- public void UpdatePowerBuffer(int powerBuffer)
+ public async Task UpdatePowerBuffer(int powerBuffer)
{
- settings.PowerBuffer = powerBuffer;
+ var config = await configurationWrapper.GetBaseConfigurationAsync();
+ config.PowerBuffer = powerBuffer;
+ await configurationWrapper.UpdateBaseConfigurationAsync(config);
}
public async Task DownloadBackup(string backupFileNamePrefix, string? backupZipDestinationDirectory)
diff --git a/TeslaSolarCharger/Server/Services/ChargingService.cs b/TeslaSolarCharger/Server/Services/ChargingService.cs
index c1db7e426..ba6470b5f 100644
--- a/TeslaSolarCharger/Server/Services/ChargingService.cs
+++ b/TeslaSolarCharger/Server/Services/ChargingService.cs
@@ -188,7 +188,7 @@ public async Task CalculatePowerToControl()
{
logger.LogTrace("{method}()", nameof(CalculatePowerToControl));
- var buffer = configurationWrapper.PowerBuffer(true);
+ var buffer = configurationWrapper.PowerBuffer();
logger.LogDebug("Adding powerbuffer {powerbuffer}", buffer);
var averagedOverage = settings.Overage ?? constants.DefaultOverage;
logger.LogDebug("Averaged overage {averagedOverage}", averagedOverage);
diff --git a/TeslaSolarCharger/Server/Services/Contracts/ITimeSeriesDataService.cs b/TeslaSolarCharger/Server/Services/Contracts/ITimeSeriesDataService.cs
new file mode 100644
index 000000000..0399bb53d
--- /dev/null
+++ b/TeslaSolarCharger/Server/Services/Contracts/ITimeSeriesDataService.cs
@@ -0,0 +1,9 @@
+using TeslaSolarCharger.Shared.Dtos.TimeSeries;
+using TeslaSolarCharger.Shared.Enums;
+
+namespace TeslaSolarCharger.Server.Services.Contracts;
+
+public interface ITimeSeriesDataService
+{
+ Task> GetTimeSeriesData(int carId, long startEpoch, long endEpoch, CarValueType carValueType);
+}
diff --git a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs
index a947ed28c..617cb42ec 100644
--- a/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs
+++ b/TeslaSolarCharger/Server/Services/FleetTelemetryWebSocketService.cs
@@ -1,5 +1,6 @@
using Microsoft.EntityFrameworkCore;
using Newtonsoft.Json;
+using System.Globalization;
using System.Net.WebSockets;
using System.Text;
using TeslaSolarCharger.Model.Entities.TeslaSolarCharger;
@@ -8,14 +9,18 @@
using TeslaSolarCharger.Server.Helper;
using TeslaSolarCharger.Server.Services.Contracts;
using TeslaSolarCharger.Shared.Contracts;
+using TeslaSolarCharger.Shared.Dtos.Contracts;
+using TeslaSolarCharger.Shared.Dtos.Settings;
using TeslaSolarCharger.Shared.Enums;
namespace TeslaSolarCharger.Server.Services;
-public class FleetTelemetryWebSocketService(ILogger logger,
+public class FleetTelemetryWebSocketService(
+ ILogger logger,
IConfigurationWrapper configurationWrapper,
IDateTimeProvider dateTimeProvider,
- IServiceProvider serviceProvider) : IFleetTelemetryWebSocketService
+ IServiceProvider serviceProvider,
+ ISettings settings) : IFleetTelemetryWebSocketService
{
private readonly TimeSpan _heartbeatsendTimeout = TimeSpan.FromSeconds(5);
@@ -28,11 +33,7 @@ public async Task ReconnectWebSocketsForEnabledCars()
var context = scope.ServiceProvider.GetRequiredService();
var cars = await context.Cars
.Where(c => c.UseFleetTelemetry && (c.ShouldBeManaged == true))
- .Select(c => new
- {
- c.Vin,
- c.UseFleetTelemetryForLocationData,
- })
+ .Select(c => new { c.Vin, c.UseFleetTelemetryForLocationData, })
.ToListAsync();
var bytesToSend = Encoding.UTF8.GetBytes("Heartbeat");
foreach (var car in cars)
@@ -41,6 +42,7 @@ public async Task ReconnectWebSocketsForEnabledCars()
{
continue;
}
+
var existingClient = Clients.FirstOrDefault(c => c.Vin == car.Vin);
if (existingClient != default)
{
@@ -58,6 +60,7 @@ await existingClient.WebSocketClient.SendAsync(segment, WebSocketMessageType.Tex
existingClient.WebSocketClient.Dispose();
Clients.Remove(existingClient);
}
+
continue;
}
else
@@ -66,6 +69,7 @@ await existingClient.WebSocketClient.SendAsync(segment, WebSocketMessageType.Tex
Clients.Remove(existingClient);
}
}
+
ConnectToFleetTelemetryApi(car.Vin, car.UseFleetTelemetryForLocationData);
}
}
@@ -78,8 +82,11 @@ public async Task DisconnectWebSocketsByVin(string vin)
{
if (client.WebSocketClient.State == WebSocketState.Open)
{
- await client.WebSocketClient.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", new CancellationTokenSource(_heartbeatsendTimeout).Token).ConfigureAwait(false);
+ await client.WebSocketClient
+ .CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", new CancellationTokenSource(_heartbeatsendTimeout).Token)
+ .ConfigureAwait(false);
}
+
client.WebSocketClient.Dispose();
Clients.Remove(client);
}
@@ -95,12 +102,14 @@ private async Task ConnectToFleetTelemetryApi(string vin, bool useFleetTelemetry
.Where(t => t.ExpiresAtUtc > currentDate)
.OrderByDescending(t => t.ExpiresAtUtc)
.FirstOrDefaultAsync().ConfigureAwait(false);
- if(token == default)
+ if (token == default)
{
logger.LogError("Can not connect to WebSocket: No token found for car {vin}", vin);
return;
}
- var url = configurationWrapper.FleetTelemetryApiUrl() + $"teslaToken={token.AccessToken}®ion={token.Region}&vin={vin}&forceReconfiguration=false&includeLocation={useFleetTelemetryForLocationData}";
+
+ var url = configurationWrapper.FleetTelemetryApiUrl() +
+ $"teslaToken={token.AccessToken}®ion={token.Region}&vin={vin}&forceReconfiguration=false&includeLocation={useFleetTelemetryForLocationData}";
using var client = new ClientWebSocket();
try
{
@@ -130,7 +139,8 @@ private async Task ConnectToFleetTelemetryApi(string vin, bool useFleetTelemetry
Clients.Remove(dtoClient);
if (dtoClient.WebSocketClient.State != WebSocketState.Closed && dtoClient.WebSocketClient.State != WebSocketState.Aborted)
{
- await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing", new CancellationTokenSource(_heartbeatsendTimeout).Token).ConfigureAwait(false);
+ await client.CloseAsync(WebSocketCloseStatus.NormalClosure, "Closing",
+ new CancellationTokenSource(_heartbeatsendTimeout).Token).ConfigureAwait(false);
}
}
}
@@ -160,11 +170,12 @@ private async Task ReceiveMessages(ClientWebSocket webSocket, CancellationToken
{
// Decode the received message
var jsonMessage = Encoding.UTF8.GetString(buffer, 0, result.Count);
- if(jsonMessage == "Heartbeat")
+ if (jsonMessage == "Heartbeat")
{
logger.LogTrace("Received heartbeat: {message}", jsonMessage);
continue;
}
+
var message = DeserializeFleetTelemetryMessage(jsonMessage);
if (message != null)
{
@@ -177,6 +188,7 @@ private async Task ReceiveMessages(ClientWebSocket webSocket, CancellationToken
{
logger.LogDebug("Save location message for car {carId}", carId);
}
+
var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService();
var carValueLog = new CarValueLog
@@ -194,6 +206,58 @@ private async Task ReceiveMessages(ClientWebSocket webSocket, CancellationToken
};
context.CarValueLogs.Add(carValueLog);
await context.SaveChangesAsync().ConfigureAwait(false);
+ var settingsCar = settings.Cars.First(c => c.Vin == vin);
+ string? propertyName = null;
+ switch (message.Type)
+ {
+ case CarValueType.ChargeAmps:
+ propertyName = nameof(DtoCar.ChargerActualCurrent);
+ break;
+ case CarValueType.ChargeCurrentRequest:
+ propertyName = nameof(DtoCar.ChargerRequestedCurrent);
+ break;
+ case CarValueType.IsPluggedIn:
+ propertyName = nameof(DtoCar.PluggedIn);
+ break;
+ case CarValueType.IsCharging:
+ if (carValueLog.BooleanValue == true && settingsCar.State != CarStateEnum.Charging)
+ {
+ logger.LogDebug("Set car state for car {carId} to charging", carId);
+ settingsCar.State = CarStateEnum.Charging;
+ }
+ else if (carValueLog.BooleanValue == false && settingsCar.State == CarStateEnum.Charging)
+ {
+ logger.LogDebug("Set car state for car {carId} to online", carId);
+ settingsCar.State = CarStateEnum.Online;
+ }
+ break;
+ case CarValueType.ChargerPilotCurrent:
+ propertyName = nameof(DtoCar.ChargerPilotCurrent);
+ break;
+ case CarValueType.Longitude:
+ propertyName = nameof(DtoCar.Longitude);
+ break;
+ case CarValueType.Latitude:
+ propertyName = nameof(DtoCar.Latitude);
+ break;
+ case CarValueType.StateOfCharge:
+ propertyName = nameof(DtoCar.SoC);
+ break;
+ case CarValueType.StateOfChargeLimit:
+ propertyName = nameof(DtoCar.SocLimit);
+ break;
+ case CarValueType.ChargerPhases:
+ propertyName = nameof(DtoCar.SocLimit);
+ break;
+ case CarValueType.ChargerVoltage:
+ propertyName = nameof(DtoCar.ChargerVoltage);
+ break;
+ }
+
+ if (propertyName != default)
+ {
+ UpdateDtoCarProperty(settingsCar, carValueLog, propertyName);
+ }
}
else
{
@@ -212,12 +276,186 @@ private async Task ReceiveMessages(ClientWebSocket webSocket, CancellationToken
{
var settings = new JsonSerializerSettings
{
- Converters = new List
- {
- new EnumDefaultConverter(CarValueType.Unknown),
- },
+ Converters = new List { new EnumDefaultConverter(CarValueType.Unknown), },
};
var message = JsonConvert.DeserializeObject(jsonMessage, settings);
return message;
}
+
+ internal void UpdateDtoCarProperty(DtoCar car, CarValueLog carValueLog, string propertyName)
+ {
+ logger.LogTrace("{method}({carId}, ***secret***, {propertyName})", nameof(UpdateDtoCarProperty), car.Id, propertyName);
+ // List of relevant property names
+ var relevantPropertyNames = new List
+ {
+ nameof(CarValueLog.DoubleValue),
+ nameof(CarValueLog.IntValue),
+ nameof(CarValueLog.StringValue),
+ nameof(CarValueLog.UnknownValue),
+ nameof(CarValueLog.BooleanValue),
+ };
+
+ // Filter properties to only the relevant ones
+ var carValueProperties = typeof(CarValueLog)
+ .GetProperties()
+ .Where(p => relevantPropertyNames.Contains(p.Name));
+
+ object valueToConvert = null;
+
+ // Find the first non-null property in CarValueLog among the relevant ones
+ foreach (var prop in carValueProperties)
+ {
+ var value = prop.GetValue(carValueLog);
+ if (value != null)
+ {
+ valueToConvert = value;
+ break;
+ }
+ }
+
+ if (valueToConvert != null)
+ {
+ var dtoProperty = typeof(DtoCar).GetProperty(propertyName);
+ if (dtoProperty != null)
+ {
+ var dtoPropertyType = dtoProperty.PropertyType;
+
+ // Handle nullable types
+ var targetType = Nullable.GetUnderlyingType(dtoPropertyType) ?? dtoPropertyType;
+ object? convertedValue = null;
+
+ try
+ {
+ // Directly handle numeric conversions without converting to string
+ if (targetType == typeof(int))
+ {
+ if (valueToConvert is int intValue)
+ {
+ convertedValue = intValue;
+ }
+ else if (valueToConvert is double doubleValue)
+ {
+ // Decide how to handle the fractional part
+ intValue = (int)Math.Round(doubleValue); // Or Math.Floor(doubleValue), Math.Ceiling(doubleValue)
+ convertedValue = intValue;
+ }
+ else if (valueToConvert is string valueString)
+ {
+ // Use InvariantCulture when parsing the string
+ if (int.TryParse(valueString, NumberStyles.Integer, CultureInfo.InvariantCulture, out intValue))
+ {
+ convertedValue = intValue;
+ }
+ else if (double.TryParse(valueString, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out doubleValue))
+ {
+ intValue = (int)Math.Round(doubleValue);
+ convertedValue = intValue;
+ }
+ }
+ else if (valueToConvert is IConvertible)
+ {
+ convertedValue = Convert.ToInt32(valueToConvert);
+ }
+ }
+ else if (targetType == typeof(double))
+ {
+ if (valueToConvert is double doubleValue)
+ {
+ convertedValue = doubleValue;
+ }
+ else if (valueToConvert is int intValue)
+ {
+ convertedValue = (double)intValue;
+ }
+ else if (valueToConvert is string valueString)
+ {
+ if (double.TryParse(valueString, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out doubleValue))
+ {
+ convertedValue = doubleValue;
+ }
+ }
+ else if (valueToConvert is IConvertible)
+ {
+ convertedValue = Convert.ToDouble(valueToConvert, CultureInfo.InvariantCulture);
+ }
+ }
+ else if (targetType == typeof(decimal))
+ {
+ if (valueToConvert is decimal decimalValue)
+ {
+ convertedValue = decimalValue;
+ }
+ else if (valueToConvert is double doubleValue)
+ {
+ convertedValue = (decimal)doubleValue;
+ }
+ else if (valueToConvert is int intValue)
+ {
+ convertedValue = (decimal)intValue;
+ }
+ else if (valueToConvert is string valueString)
+ {
+ if (decimal.TryParse(valueString, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out decimalValue))
+ {
+ convertedValue = decimalValue;
+ }
+ }
+ else if (valueToConvert is IConvertible)
+ {
+ convertedValue = Convert.ToDecimal(valueToConvert, CultureInfo.InvariantCulture);
+ }
+ }
+ else if (targetType == typeof(bool))
+ {
+ if (valueToConvert is bool boolValue)
+ {
+ convertedValue = boolValue;
+ }
+ else if (valueToConvert is string valueString)
+ {
+ if (bool.TryParse(valueString, out boolValue))
+ {
+ convertedValue = boolValue;
+ }
+ }
+ else if (valueToConvert is IConvertible)
+ {
+ convertedValue = Convert.ToBoolean(valueToConvert, CultureInfo.InvariantCulture);
+ }
+ }
+ else if (targetType == typeof(string))
+ {
+ // Use InvariantCulture to ensure consistent formatting
+ convertedValue = Convert.ToString(valueToConvert, CultureInfo.InvariantCulture);
+ }
+ else
+ {
+ // For other types, attempt to convert using ChangeType
+ if (valueToConvert is IConvertible)
+ {
+ convertedValue = Convert.ChangeType(valueToConvert, targetType, CultureInfo.InvariantCulture);
+ }
+ else if (targetType.IsAssignableFrom(valueToConvert.GetType()))
+ {
+ convertedValue = valueToConvert;
+ }
+ }
+
+ // Update the property if conversion was successful
+ if (convertedValue != null)
+ {
+ dtoProperty.SetValue(car, convertedValue);
+ }
+ else
+ {
+ logger.LogInformation("Do not update {propertyName} on car {carId} as converted value is null", propertyName, car.Id);
+ }
+ }
+ catch (Exception ex)
+ {
+ logger.LogError(ex, "Error converting {propertyName} on car {carId}", propertyName, car.Id);
+ }
+ }
+ }
+ }
}
diff --git a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs
index d0c9103c4..35a223b86 100644
--- a/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs
+++ b/TeslaSolarCharger/Server/Services/TeslaFleetApiService.cs
@@ -267,7 +267,7 @@ public async Task RefreshCarData()
foreach (var carId in carIds)
{
var car = settings.Cars.First(c => c.Id == carId);
- if (!IsCarDataRefreshNeeded(car))
+ if (!(await IsCarDataRefreshNeeded(car)))
{
logger.LogDebug("Do not refresh car data for car {carId} to prevent rate limits", car.Id);
continue;
@@ -288,14 +288,29 @@ public async Task RefreshCarData()
var vehicleState = vehicleResult.State;
if (configurationWrapper.GetVehicleDataFromTesla())
{
+ var carStateLog = new CarValueLog()
+ {
+ CarId = car.Id,
+ Timestamp = dateTimeProvider.UtcNow(),
+ Source = CarValueSource.FleetApi,
+ Type = CarValueType.AsleepOrOffline,
+ };
if (vehicleState == "asleep")
{
+ carStateLog.BooleanValue = true;
car.State = CarStateEnum.Asleep;
}
else if (vehicleState == "offline")
{
+ carStateLog.BooleanValue = true;
car.State = CarStateEnum.Offline;
}
+ else
+ {
+ carStateLog.BooleanValue = false;
+ }
+ teslaSolarChargerContext.CarValueLogs.Add(carStateLog);
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
}
if (vehicleState is "asleep" or "offline")
@@ -323,10 +338,26 @@ public async Task RefreshCarData()
await errorHandlingService.HandleErrorResolved(issueKeys.GetVehicleData, car.Vin);
if (configurationWrapper.GetVehicleDataFromTesla())
{
-
+ var timeStamp = dateTimeProvider.UtcNow();
car.Name = vehicleDataResult.VehicleState.VehicleName;
car.SoC = vehicleDataResult.ChargeState.BatteryLevel;
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = timeStamp,
+ Type = CarValueType.StateOfCharge,
+ Source = CarValueSource.FleetApi,
+ IntValue = vehicleDataResult.ChargeState.BatteryLevel,
+ });
car.SocLimit = vehicleDataResult.ChargeState.ChargeLimitSoc;
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = timeStamp,
+ Type = CarValueType.StateOfChargeLimit,
+ Source = CarValueSource.FleetApi,
+ IntValue = vehicleDataResult.ChargeState.ChargeLimitSoc,
+ });
var minimumSettableSocLimit = vehicleDataResult.ChargeState.ChargeLimitSocMin;
if (car.MinimumSoC > car.SocLimit && car.SocLimit > minimumSettableSocLimit)
{
@@ -335,9 +366,41 @@ public async Task RefreshCarData()
logger.LogError("Can not handle lower Soc than minimumSoc");
}
car.ChargerPhases = vehicleDataResult.ChargeState.ChargerPhases;
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = timeStamp,
+ Type = CarValueType.ChargerPhases,
+ Source = CarValueSource.FleetApi,
+ IntValue = vehicleDataResult.ChargeState.ChargerPhases is null or > 1 ? 3 : 1,
+ });
car.ChargerVoltage = vehicleDataResult.ChargeState.ChargerVoltage;
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = timeStamp,
+ Type = CarValueType.ChargerVoltage,
+ Source = CarValueSource.FleetApi,
+ IntValue = vehicleDataResult.ChargeState.ChargerVoltage,
+ });
car.ChargerActualCurrent = vehicleDataResult.ChargeState.ChargerActualCurrent;
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = timeStamp,
+ Type = CarValueType.ChargeAmps,
+ Source = CarValueSource.FleetApi,
+ IntValue = vehicleDataResult.ChargeState.ChargerActualCurrent,
+ });
car.PluggedIn = vehicleDataResult.ChargeState.ChargingState != "Disconnected";
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = timeStamp,
+ Type = CarValueType.IsPluggedIn,
+ Source = CarValueSource.FleetApi,
+ BooleanValue = vehicleDataResult.ChargeState.ChargingState != "Disconnected",
+ });
car.ClimateOn = vehicleDataResult.ClimateState.IsClimateOn;
car.TimeUntilFullCharge = TimeSpan.FromHours(vehicleDataResult.ChargeState.TimeToFullCharge);
var teslaCarStateString = vehicleDataResult.State;
@@ -345,6 +408,14 @@ public async Task RefreshCarData()
var teslaCarSoftwareUpdateState = vehicleDataResult.VehicleState.SoftwareUpdate.Status;
var chargingState = vehicleDataResult.ChargeState.ChargingState;
car.State = DetermineCarState(teslaCarStateString, teslaCarShiftState, teslaCarSoftwareUpdateState, chargingState);
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = timeStamp,
+ Type = CarValueType.IsCharging,
+ Source = CarValueSource.FleetApi,
+ BooleanValue = vehicleDataResult.ChargeState.ChargingState != "Charging",
+ });
if (car.State == CarStateEnum.Unknown)
{
await errorHandlingService.HandleError(nameof(TeslaFleetApiService), nameof(RefreshCarData), $"Error determining car state for car {car.Vin}",
@@ -356,10 +427,43 @@ public async Task RefreshCarData()
}
car.Healthy = true;
car.ChargerRequestedCurrent = vehicleDataResult.ChargeState.ChargeCurrentRequest;
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = timeStamp,
+ Type = CarValueType.ChargeCurrentRequest,
+ Source = CarValueSource.FleetApi,
+ IntValue = vehicleDataResult.ChargeState.ChargeCurrentRequest,
+ });
car.ChargerPilotCurrent = vehicleDataResult.ChargeState.ChargerPilotCurrent;
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = timeStamp,
+ Type = CarValueType.ChargerPilotCurrent,
+ Source = CarValueSource.FleetApi,
+ IntValue = vehicleDataResult.ChargeState.ChargerPilotCurrent,
+ });
car.ScheduledChargingStartTime = vehicleDataResult.ChargeState.ScheduledChargingStartTime == null ? (DateTimeOffset?)null : DateTimeOffset.FromUnixTimeSeconds(vehicleDataResult.ChargeState.ScheduledChargingStartTime.Value);
car.Longitude = vehicleDataResult.DriveState.Longitude;
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = timeStamp,
+ Type = CarValueType.Longitude,
+ Source = CarValueSource.FleetApi,
+ DoubleValue = vehicleDataResult.DriveState.Longitude,
+ });
car.Latitude = vehicleDataResult.DriveState.Latitude;
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = timeStamp,
+ Type = CarValueType.Latitude,
+ Source = CarValueSource.FleetApi,
+ DoubleValue = vehicleDataResult.DriveState.Latitude,
+ });
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
}
await errorHandlingService.HandleErrorResolved(issueKeys.UnhandledCarStateRefresh, car.Vin);
@@ -373,7 +477,7 @@ public async Task RefreshCarData()
}
}
- private bool IsCarDataRefreshNeeded(DtoCar car)
+ private async Task IsCarDataRefreshNeeded(DtoCar car)
{
logger.LogTrace("{method}({vin})", nameof(IsCarDataRefreshNeeded), car.Vin);
var latestRefresh = car.VehicleDataCalls.OrderByDescending(c => c).FirstOrDefault();
@@ -423,6 +527,25 @@ private bool IsCarDataRefreshNeeded(DtoCar car)
return true;
}
+ if (await FleetTelemetryValueChanged(car.Id, CarValueType.IsCharging, latestRefresh).ConfigureAwait(false))
+ {
+ logger.LogDebug("Send a request as Fleet Telemetry detected a change in is charging in state.");
+ return true;
+ }
+
+ if (await FleetTelemetryValueChanged(car.Id, CarValueType.IsPluggedIn, latestRefresh).ConfigureAwait(false))
+ {
+ logger.LogDebug("Send a request as Fleet Telemetry detected a change in plugged in state.");
+ return true;
+ }
+
+ var values = await GetLatestTwoValues(car.Id, CarValueType.ChargeAmps).ConfigureAwait(false);
+ if (LatestValueChangeAfterLatestFleetApiRefresh(latestRefresh, values) && values.Any(v => v.DoubleValue == 0))
+ {
+ logger.LogDebug("Send a request as Fleet Telemetry detected at least one 0 value in charging amps.");
+ return true;
+ }
+
var latestChargeStartOrWakeUp = car.WakeUpCalls.Concat(car.ChargeStartCalls).OrderByDescending(c => c).FirstOrDefault();
if (latestChargeStartOrWakeUp == default)
{
@@ -448,6 +571,69 @@ private bool IsCarDataRefreshNeeded(DtoCar car)
return false;
}
+ private async Task FleetTelemetryValueChanged(int carId, CarValueType carValueType, DateTime latestRefresh)
+ {
+ var values = await GetLatestTwoValues(carId, carValueType).ConfigureAwait(false);
+
+ if (!LatestValueChangeAfterLatestFleetApiRefresh(latestRefresh, values))
+ {
+ return false;
+ }
+
+ var currentValue = values[0];
+ var previousValue = values[1];
+
+ var doubleValueChanged = !Nullable.Equals(currentValue.DoubleValue, previousValue.DoubleValue);
+ var intValueChanged = !Nullable.Equals(currentValue.IntValue, previousValue.IntValue);
+ var stringValueChanged = currentValue.StringValue != previousValue.StringValue;
+ var unknownValueChanged = currentValue.UnknownValue != previousValue.UnknownValue;
+ var booleanValueChanged = !Nullable.Equals(currentValue.BooleanValue, previousValue.BooleanValue);
+ var invalidValueChanged = !Nullable.Equals(currentValue.InvalidValue, previousValue.InvalidValue);
+
+ return (currentValue.Timestamp > latestRefresh)
+ && (doubleValueChanged || intValueChanged || stringValueChanged || unknownValueChanged || booleanValueChanged || invalidValueChanged);
+ }
+
+ private static bool LatestValueChangeAfterLatestFleetApiRefresh(DateTime latestRefresh, List values)
+ {
+ //Only one value available
+ if (values.Count != 2)
+ {
+ return false;
+ }
+
+ //latest value change before latest fleet API refresh
+ if (values.Count(c => c.Timestamp > latestRefresh) < 1)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ private async Task> GetLatestTwoValues(int carId, CarValueType carValueType)
+ {
+ var values = await teslaSolarChargerContext.CarValueLogs
+ .Where(c => c.Type == carValueType
+ && c.Source == CarValueSource.FleetTelemetry
+ && c.CarId == carId)
+ .OrderByDescending(c => c.Timestamp)
+ .Select(c => new CarValueLogTimeStampAndValues
+ {
+ Timestamp = c.Timestamp,
+ DoubleValue = c.DoubleValue,
+ IntValue = c.IntValue,
+ StringValue = c.StringValue,
+ UnknownValue = c.UnknownValue,
+ BooleanValue = c.BooleanValue,
+ InvalidValue = c.InvalidValue,
+ })
+ .Take(2)
+ .ToListAsync();
+ return values;
+ }
+
+
private CarStateEnum? DetermineCarState(string teslaCarStateString, string? teslaCarShiftState, string teslaCarSoftwareUpdateState, string chargingState)
{
logger.LogTrace("{method}({teslaCarStateString}, {teslaCarShiftState}, {teslaCarSoftwareUpdateState}, {chargingState})", nameof(DetermineCarState), teslaCarStateString, teslaCarShiftState, teslaCarSoftwareUpdateState, chargingState);
diff --git a/TeslaSolarCharger/Server/Services/TeslaMateMqttService.cs b/TeslaSolarCharger/Server/Services/TeslaMateMqttService.cs
index 8b1824dd8..9445e2c06 100644
--- a/TeslaSolarCharger/Server/Services/TeslaMateMqttService.cs
+++ b/TeslaSolarCharger/Server/Services/TeslaMateMqttService.cs
@@ -1,6 +1,8 @@
using MQTTnet;
using MQTTnet.Client;
using System.Globalization;
+using TeslaSolarCharger.Model.Contracts;
+using TeslaSolarCharger.Model.Entities.TeslaSolarCharger;
using TeslaSolarCharger.Server.Contracts;
using TeslaSolarCharger.Shared.Contracts;
using TeslaSolarCharger.Shared.Dtos.Contracts;
@@ -8,15 +10,16 @@
namespace TeslaSolarCharger.Server.Services;
-public class TeslaMateMqttService : ITeslaMateMqttService
+public class TeslaMateMqttService(
+ ILogger logger,
+ IMqttClient mqttClient,
+ MqttFactory mqttFactory,
+ ISettings settings,
+ IConfigurationWrapper configurationWrapper,
+ IDateTimeProvider dateTimeProvider,
+ ITeslaSolarChargerContext teslaSolarChargerContext)
+ : ITeslaMateMqttService
{
- private readonly ILogger _logger;
- private readonly IMqttClient _mqttClient;
- private readonly MqttFactory _mqttFactory;
- private readonly ISettings _settings;
- private readonly IConfigurationWrapper _configurationWrapper;
- private readonly IConfigJsonService _configJsonService;
- private readonly IDateTimeProvider _dateTimeProvider;
// ReSharper disable once InconsistentNaming
private const string TopicDisplayName = "display_name";
@@ -53,49 +56,35 @@ public class TeslaMateMqttService : ITeslaMateMqttService
// ReSharper disable once InconsistentNaming
private const string TopicSpeed = "speed";
- public bool IsMqttClientConnected => _mqttClient.IsConnected;
-
- public TeslaMateMqttService(ILogger logger, IMqttClient mqttClient, MqttFactory mqttFactory,
- ISettings settings, IConfigurationWrapper configurationWrapper,
- IConfigJsonService configJsonService, IDateTimeProvider dateTimeProvider)
- {
- _logger = logger;
- _mqttClient = mqttClient;
- _mqttFactory = mqttFactory;
- _settings = settings;
- _configurationWrapper = configurationWrapper;
- _configJsonService = configJsonService;
- _dateTimeProvider = dateTimeProvider;
- }
+ public bool IsMqttClientConnected => mqttClient.IsConnected;
public async Task ConnectMqttClient()
{
- _logger.LogTrace("{method}()", nameof(ConnectMqttClient));
+ logger.LogTrace("{method}()", nameof(ConnectMqttClient));
var guid = Guid.NewGuid();
- var mqqtClientId = _configurationWrapper.MqqtClientId() + guid;
- var mosquitoServer = _configurationWrapper.MosquitoServer();
+ var mqqtClientId = configurationWrapper.MqqtClientId() + guid;
+ var mosquitoServer = configurationWrapper.MosquitoServer();
var mqttClientOptions = new MqttClientOptionsBuilder()
.WithClientId(mqqtClientId)
.WithTcpServer(mosquitoServer)
.Build();
- _mqttClient.ApplicationMessageReceivedAsync += e =>
+ mqttClient.ApplicationMessageReceivedAsync += async e =>
{
var value = GetValueFromMessage(e.ApplicationMessage);
- if ((!_configurationWrapper.LogLocationData()) && (string.Equals(value.Topic, TopicLongitude) || string.Equals(value.Topic, TopicLatitude)))
+ if ((!configurationWrapper.LogLocationData()) && (string.Equals(value.Topic, TopicLongitude) || string.Equals(value.Topic, TopicLatitude)))
{
- _logger.LogTrace("Car Id: {carId}, Topic: {topic}, Value: xx.xxxxx", value.CarId, value.Topic);
+ logger.LogTrace("Car Id: {carId}, Topic: {topic}, Value: xx.xxxxx", value.CarId, value.Topic);
}
else
{
- _logger.LogTrace("Car Id: {carId}, Topic: {topic}, Value: {value}", value.CarId, value.Topic, value.Value);
+ logger.LogTrace("Car Id: {carId}, Topic: {topic}, Value: {value}", value.CarId, value.Topic, value.Value);
}
- UpdateCar(value);
- return Task.CompletedTask;
+ await UpdateCar(value);
};
- if (_mqttClient.IsConnected)
+ if (mqttClient.IsConnected)
{
await DisconnectClient("Reconnecting with new configuration").ConfigureAwait(false);
await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
@@ -103,17 +92,17 @@ public async Task ConnectMqttClient()
try
{
- await _mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None).ConfigureAwait(false);
+ await mqttClient.ConnectAsync(mqttClientOptions, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
- _logger.LogError(ex, "Could not connect to TeslaMate mqtt server");
+ logger.LogError(ex, "Could not connect to TeslaMate mqtt server");
return;
}
var topicPrefix = "teslamate/cars/+/";
- var mqttSubscribeOptions = _mqttFactory.CreateSubscribeOptionsBuilder()
+ var mqttSubscribeOptions = mqttFactory.CreateSubscribeOptionsBuilder()
.WithTopicFilter(f =>
{
f.WithTopic($"{topicPrefix}{TopicDisplayName}");
@@ -184,46 +173,46 @@ public async Task ConnectMqttClient()
})
.Build();
- await _mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None).ConfigureAwait(false);
+ await mqttClient.SubscribeAsync(mqttSubscribeOptions, CancellationToken.None).ConfigureAwait(false);
}
public async Task DisconnectClient(string reason)
{
- _logger.LogTrace("{method}({reason})", nameof(DisconnectClient), reason);
- if (_mqttClient.IsConnected)
+ logger.LogTrace("{method}({reason})", nameof(DisconnectClient), reason);
+ if (mqttClient.IsConnected)
{
- await _mqttClient.DisconnectAsync().ConfigureAwait(false);
+ await mqttClient.DisconnectAsync().ConfigureAwait(false);
}
}
public async Task ConnectClientIfNotConnected()
{
- _logger.LogTrace("{method}()", nameof(ConnectClientIfNotConnected));
- if (_mqttClient.IsConnected)
+ logger.LogTrace("{method}()", nameof(ConnectClientIfNotConnected));
+ if (mqttClient.IsConnected)
{
- _logger.LogTrace("MqttClient is connected");
+ logger.LogTrace("MqttClient is connected");
return;
}
- if (_configurationWrapper.GetVehicleDataFromTesla())
+ if (configurationWrapper.GetVehicleDataFromTesla())
{
- _logger.LogInformation("Not connecting to TeslaMate as data is retrieved from Teslas Fleet API");
+ logger.LogInformation("Not connecting to TeslaMate as data is retrieved from Teslas Fleet API");
return;
}
- _logger.LogWarning("MqttClient is not connected");
+ logger.LogWarning("MqttClient is not connected");
await ConnectMqttClient().ConfigureAwait(false);
}
- internal void UpdateCar(TeslaMateValue value)
+ internal async Task UpdateCar(TeslaMateValue value)
{
- _logger.LogTrace("{method}({@param})", nameof(UpdateCar), value);
- var car = _settings.Cars.FirstOrDefault(c => c.TeslaMateCarId == value.CarId);
+ logger.LogTrace("{method}({@param})", nameof(UpdateCar), value);
+ var car = settings.Cars.FirstOrDefault(c => c.TeslaMateCarId == value.CarId);
if (car == null)
{
// Logge einen Fehler oder handle den Fall, dass kein Auto gefunden wurde
- _logger.LogError($"No car found with TeslaMateCarId {value.CarId}");
+ logger.LogError($"No car found with TeslaMateCarId {value.CarId}");
return; // oder andere geeignete Maßnahme
}
@@ -240,18 +229,36 @@ internal void UpdateCar(TeslaMateValue value)
if (!string.IsNullOrWhiteSpace(value.Value))
{
car.SoC = Convert.ToInt32(value.Value);
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = dateTimeProvider.UtcNow(),
+ Source = CarValueSource.TeslaMate,
+ Type = CarValueType.StateOfCharge,
+ IntValue = car.SoC,
+ });
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
}
break;
case TopicChargeLimit:
if (!string.IsNullOrWhiteSpace(value.Value))
{
car.SocLimit = Convert.ToInt32(value.Value);
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = dateTimeProvider.UtcNow(),
+ Source = CarValueSource.TeslaMate,
+ Type = CarValueType.StateOfChargeLimit,
+ IntValue = car.SocLimit,
+ });
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
var minimumSettableSocLimit = 50;
if (car.MinimumSoC > car.SocLimit && car.SocLimit > minimumSettableSocLimit)
{
- _logger.LogWarning("Reduce Minimum SoC {minimumSoC} as charge limit {chargeLimit} is lower.", car.MinimumSoC, car.SocLimit);
+ logger.LogWarning("Reduce Minimum SoC {minimumSoC} as charge limit {chargeLimit} is lower.", car.MinimumSoC, car.SocLimit);
car.MinimumSoC = (int)car.SocLimit;
- _logger.LogError("Can not handle lower Soc than minimumSoc");
+ logger.LogError("Can not handle lower Soc than minimumSoc");
}
}
break;
@@ -259,11 +266,20 @@ internal void UpdateCar(TeslaMateValue value)
if (!string.IsNullOrWhiteSpace(value.Value))
{
car.ChargerPhases = Convert.ToInt32(value.Value);
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = dateTimeProvider.UtcNow(),
+ Source = CarValueSource.TeslaMate,
+ Type = CarValueType.ChargerPhases,
+ IntValue = car.ChargerPhases is null or > 1 ? 3 : 1,
+ });
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
}
else
{
//This is needed as TeslaMate sometime sends empty values during charger being connected.
- _logger.LogDebug($"{nameof(TopicChargerPhases)} is {value.Value}. Do not overwrite charger phases.");
+ logger.LogDebug($"{nameof(TopicChargerPhases)} is {value.Value}. Do not overwrite charger phases.");
//car.CarState.ChargerPhases = null;
}
break;
@@ -271,6 +287,15 @@ internal void UpdateCar(TeslaMateValue value)
if (!string.IsNullOrWhiteSpace(value.Value))
{
car.ChargerVoltage = Convert.ToInt32(value.Value);
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = dateTimeProvider.UtcNow(),
+ Source = CarValueSource.TeslaMate,
+ Type = CarValueType.ChargerVoltage,
+ IntValue = car.ChargerVoltage,
+ });
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
}
else
{
@@ -281,19 +306,28 @@ internal void UpdateCar(TeslaMateValue value)
if (!string.IsNullOrWhiteSpace(value.Value))
{
car.ChargerActualCurrent = Convert.ToInt32(value.Value);
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = dateTimeProvider.UtcNow(),
+ Source = CarValueSource.TeslaMate,
+ Type = CarValueType.ChargeAmps,
+ IntValue = car.ChargerActualCurrent,
+ });
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
if (car.ChargerActualCurrent < 5 &&
car.ChargerRequestedCurrent == car.LastSetAmp &&
car.LastSetAmp == car.ChargerActualCurrent - 1 &&
car.LastSetAmp > 0)
{
- _logger.LogWarning("CarId {carId}: Reducing {actualCurrent} from {originalValue} to {newValue} due to error in TeslaApi", car.Id, nameof(car.ChargerActualCurrent), car.ChargerActualCurrent, car.LastSetAmp);
+ logger.LogWarning("CarId {carId}: Reducing {actualCurrent} from {originalValue} to {newValue} due to error in TeslaApi", car.Id, nameof(car.ChargerActualCurrent), car.ChargerActualCurrent, car.LastSetAmp);
//ToDo: Set to average of requested and actual current
car.ChargerActualCurrent = car.LastSetAmp;
}
if (car.ChargerActualCurrent > 0 && car.PluggedIn != true)
{
- _logger.LogWarning("Car {carId} is not detected as plugged in but actual current > 0 => set plugged in to true", car.Id);
+ logger.LogWarning("Car {carId} is not detected as plugged in but actual current > 0 => set plugged in to true", car.Id);
car.PluggedIn = true;
}
}
@@ -306,6 +340,15 @@ internal void UpdateCar(TeslaMateValue value)
if (!string.IsNullOrWhiteSpace(value.Value))
{
car.PluggedIn = Convert.ToBoolean(value.Value);
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = dateTimeProvider.UtcNow(),
+ Source = CarValueSource.TeslaMate,
+ Type = CarValueType.IsPluggedIn,
+ BooleanValue = car.PluggedIn == true,
+ });
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
}
break;
case TopicIsClimateOn:
@@ -325,59 +368,107 @@ internal void UpdateCar(TeslaMateValue value)
}
break;
case TopicState:
+ var asleepValueLog = new CarValueLog()
+ {
+ CarId = car.Id,
+ Timestamp = dateTimeProvider.UtcNow(),
+ Source = CarValueSource.TeslaMate,
+ Type = CarValueType.AsleepOrOffline,
+ };
+ var chargingValueLog = new CarValueLog()
+ {
+ CarId = car.Id,
+ Timestamp = dateTimeProvider.UtcNow(),
+ Source = CarValueSource.TeslaMate,
+ Type = CarValueType.IsCharging,
+ };
switch (value.Value)
{
case "asleep":
car.State = CarStateEnum.Asleep;
+ asleepValueLog.BooleanValue = true;
+ chargingValueLog.BooleanValue = false;
break;
case "offline":
car.State = CarStateEnum.Offline;
+ asleepValueLog.BooleanValue = true;
+ chargingValueLog.BooleanValue = false;
break;
case "online":
car.State = CarStateEnum.Online;
+ asleepValueLog.BooleanValue = false;
+ chargingValueLog.BooleanValue = false;
break;
case "charging":
car.State = CarStateEnum.Charging;
+ asleepValueLog.BooleanValue = false;
+ chargingValueLog.BooleanValue = true;
break;
case "suspended":
car.State = CarStateEnum.Suspended;
+ asleepValueLog.BooleanValue = false;
+ chargingValueLog.BooleanValue = false;
break;
case "driving":
car.State = CarStateEnum.Driving;
+ asleepValueLog.BooleanValue = false;
+ chargingValueLog.BooleanValue = false;
break;
case "updating":
car.State = CarStateEnum.Updating;
+ asleepValueLog.BooleanValue = false;
break;
default:
- _logger.LogWarning("Unknown car state deteckted: {carState}", value.Value);
+ logger.LogWarning("Unknown car state deteckted: {carState}", value.Value);
car.State = CarStateEnum.Unknown;
break;
}
+ teslaSolarChargerContext.CarValueLogs.Add(asleepValueLog);
+ teslaSolarChargerContext.CarValueLogs.Add(chargingValueLog);
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
break;
case TopicHealthy:
car.Healthy = Convert.ToBoolean(value.Value);
- _logger.LogTrace("Car healthiness if car {carId} changed to {healthiness}", car.Id, car.Healthy);
+ logger.LogTrace("Car healthiness if car {carId} changed to {healthiness}", car.Id, car.Healthy);
break;
case TopicChargeCurrentRequest:
if (!string.IsNullOrWhiteSpace(value.Value))
{
car.ChargerRequestedCurrent = Convert.ToInt32(value.Value);
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = dateTimeProvider.UtcNow(),
+ Source = CarValueSource.TeslaMate,
+ Type = CarValueType.ChargeCurrentRequest,
+ IntValue = car.ChargerRequestedCurrent,
+ });
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
}
break;
case TopicChargeCurrentRequestMax:
if (!string.IsNullOrWhiteSpace(value.Value))
{
car.ChargerPilotCurrent = Convert.ToInt32(value.Value);
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = dateTimeProvider.UtcNow(),
+ Source = CarValueSource.TeslaMate,
+ Type = CarValueType.ChargerPilotCurrent,
+ IntValue = car.ChargerPilotCurrent,
+ });
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
}
break;
case TopicScheduledChargingStartTime:
- _logger.LogTrace("{topicName} changed to {value}", nameof(TopicScheduledChargingStartTime), value.Value);
+ logger.LogTrace("{topicName} changed to {value}", nameof(TopicScheduledChargingStartTime), value.Value);
if (!string.IsNullOrWhiteSpace(value.Value))
{
var parsedScheduledChargingStartTime = DateTimeOffset.Parse(value.Value);
- if (parsedScheduledChargingStartTime < _dateTimeProvider.DateTimeOffSetNow().AddDays(-14))
+ if (parsedScheduledChargingStartTime < dateTimeProvider.DateTimeOffSetNow().AddDays(-14))
{
- _logger.LogWarning("TeslaMate set scheduled charging start time to {teslaMateValue}. As this is in the past, it will be ignored.", parsedScheduledChargingStartTime);
+ logger.LogWarning("TeslaMate set scheduled charging start time to {teslaMateValue}. As this is in the past, it will be ignored.", parsedScheduledChargingStartTime);
car.ScheduledChargingStartTime = null;
}
else
@@ -394,12 +485,30 @@ internal void UpdateCar(TeslaMateValue value)
if (!string.IsNullOrWhiteSpace(value.Value))
{
car.Longitude = Convert.ToDouble(value.Value, CultureInfo.InvariantCulture);
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = dateTimeProvider.UtcNow(),
+ Source = CarValueSource.TeslaMate,
+ Type = CarValueType.Longitude,
+ DoubleValue = car.Longitude,
+ });
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
}
break;
case TopicLatitude:
if (!string.IsNullOrWhiteSpace(value.Value))
{
car.Latitude = Convert.ToDouble(value.Value, CultureInfo.InvariantCulture);
+ teslaSolarChargerContext.CarValueLogs.Add(new()
+ {
+ CarId = car.Id,
+ Timestamp = dateTimeProvider.UtcNow(),
+ Source = CarValueSource.TeslaMate,
+ Type = CarValueType.Latitude,
+ DoubleValue = car.Latitude,
+ });
+ await teslaSolarChargerContext.SaveChangesAsync().ConfigureAwait(false);
}
break;
case TopicSpeed:
diff --git a/TeslaSolarCharger/Server/Services/TimeSeriesDataService.cs b/TeslaSolarCharger/Server/Services/TimeSeriesDataService.cs
new file mode 100644
index 000000000..440a11bb7
--- /dev/null
+++ b/TeslaSolarCharger/Server/Services/TimeSeriesDataService.cs
@@ -0,0 +1,40 @@
+using AutoMapper.QueryableExtensions;
+using Microsoft.EntityFrameworkCore;
+using TeslaSolarCharger.Model.Contracts;
+using TeslaSolarCharger.Model.Entities.TeslaSolarCharger;
+using TeslaSolarCharger.Server.Services.Contracts;
+using TeslaSolarCharger.Shared.Dtos.TimeSeries;
+using TeslaSolarCharger.Shared.Enums;
+using TeslaSolarCharger.SharedBackend.MappingExtensions;
+
+namespace TeslaSolarCharger.Server.Services;
+
+public class TimeSeriesDataService(ILogger logger,
+ ITeslaSolarChargerContext teslaSolarChargerContext,
+ IMapperConfigurationFactory mapperConfigurationFactory) : ITimeSeriesDataService
+{
+ public async Task> GetTimeSeriesData(int carId, long startEpoch, long endEpoch, CarValueType carValueType)
+ {
+ logger.LogTrace("{method}({carId}, {startEpoch}, {endEpoch}, {carValueType})", nameof(GetTimeSeriesData), carId, startEpoch, endEpoch, carValueType);
+ var startDate = DateTimeOffset.FromUnixTimeSeconds(startEpoch).DateTime;
+ var endDate = DateTimeOffset.FromUnixTimeSeconds(endEpoch).DateTime;
+
+ var mapper = mapperConfigurationFactory.Create(cfg =>
+ {
+ cfg.CreateMap()
+ .ForMember(d => d.Timestamp, opt => opt.MapFrom(c => c.Timestamp.ToLocalTime()))
+ .ForMember(d => d.Value, opt => opt.MapFrom(c => c.IntValue ?? c.DoubleValue))
+ ;
+ });
+
+ var result = await teslaSolarChargerContext.CarValueLogs
+ .Where(c => c.CarId == carId)
+ .Where(c => c.Timestamp >= startDate && c.Timestamp <= endDate)
+ .Where(c => c.Type == carValueType)
+ .OrderBy(c => c.Timestamp)
+ .ProjectTo(mapper)
+ .ToListAsync();
+
+ return result;
+ }
+}
diff --git a/TeslaSolarCharger/Server/Services/TscOnlyChargingCostService.cs b/TeslaSolarCharger/Server/Services/TscOnlyChargingCostService.cs
index 59e072c5d..15970487a 100644
--- a/TeslaSolarCharger/Server/Services/TscOnlyChargingCostService.cs
+++ b/TeslaSolarCharger/Server/Services/TscOnlyChargingCostService.cs
@@ -113,6 +113,7 @@ public async Task> GetFinalizedChargingProcesses(int carI
//ToDo: Maybe possible null exceptions as not all members that are nullable in database are also nullable in dto
cfg.CreateMap()
.ForMember(d => d.StartTime, opt => opt.MapFrom(h => h.StartDate.ToLocalTime()))
+ .ForMember(d => d.EndTime, opt => opt.MapFrom(h => h.EndDate.HasValue ? h.EndDate.Value.ToLocalTime() : (DateTime?)null))
.ForMember(d => d.CalculatedPrice, opt => opt.MapFrom(h => h.Cost == null ? 0m : Math.Round(h.Cost.Value, 2)))
.ForMember(d => d.UsedGridEnergy, opt => opt.MapFrom(h => h.UsedGridEnergyKwh == null ? 0m : Math.Round(h.UsedGridEnergyKwh.Value, 2)))
.ForMember(d => d.UsedHomeBatteryEnergy, opt => opt.MapFrom(h => h.UsedHomeBatteryEnergyKwh == null ? 0m : Math.Round(h.UsedHomeBatteryEnergyKwh.Value, 2)))
@@ -286,7 +287,7 @@ private List AddDefaultChargePrices(List prices, DateTimeOffset fr
public async Task AddChargingDetailsForAllCars()
{
logger.LogTrace("{method}()", nameof(AddChargingDetailsForAllCars));
- var powerBuffer = configurationWrapper.PowerBuffer(true);
+ var powerBuffer = configurationWrapper.PowerBuffer();
var overage = settings.Overage ?? (settings.InverterPower - (powerBuffer < 0 ? 0 : powerBuffer));
var homeBatteryDischargingPower = (- settings.HomeBatteryPower) ?? 0;
if (homeBatteryDischargingPower < 0)
diff --git a/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj b/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj
index 20e6cb8a8..8af36d709 100644
--- a/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj
+++ b/TeslaSolarCharger/Server/TeslaSolarCharger.Server.csproj
@@ -45,8 +45,8 @@
-
-
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
@@ -66,7 +66,7 @@
-
+
diff --git a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs
index 21aa9eda2..e539e8f64 100644
--- a/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs
+++ b/TeslaSolarCharger/Shared/Contracts/IConfigurationWrapper.cs
@@ -16,7 +16,7 @@ public interface IConfigurationWrapper
string GeoFence();
TimeSpan TimespanUntilSwitchOn();
TimeSpan TimespanUntilSwitchOff();
- int PowerBuffer(bool getInMemoryValueIfAvailable);
+ int PowerBuffer();
string? TelegramBotKey();
string? TelegramChannelId();
string? CurrentInverterPowerJsonPattern();
diff --git a/TeslaSolarCharger/Shared/Dtos/BaseConfiguration/BaseConfigurationBase.cs b/TeslaSolarCharger/Shared/Dtos/BaseConfiguration/BaseConfigurationBase.cs
index b54c76ea4..8a0508382 100644
--- a/TeslaSolarCharger/Shared/Dtos/BaseConfiguration/BaseConfigurationBase.cs
+++ b/TeslaSolarCharger/Shared/Dtos/BaseConfiguration/BaseConfigurationBase.cs
@@ -45,7 +45,7 @@ public class BaseConfigurationBase
public int MinutesUntilSwitchOff { get; set; } = 5;
[Required]
public int PowerBuffer { get; set; } = 0;
- [HelperText("If enabled, the configured power buffer is displayed on the home screen, including the option to directly change it. Note: The values you set on the home screen will be overwritten on every TSC restart. To set a permanent power buffer, use the field above.")]
+ [HelperText("If enabled, the configured power buffer is displayed on the home screen, including the option to directly change it.")]
public bool AllowPowerBufferChangeOnHome { get; set; }
public string? CurrentPowerToGridJsonPattern { get; set; }
public decimal CurrentPowerToGridCorrectionFactor { get; set; } = 1;
diff --git a/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs b/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs
index 248a23626..ae0744343 100644
--- a/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs
+++ b/TeslaSolarCharger/Shared/Dtos/CarBasicConfiguration.cs
@@ -53,6 +53,6 @@ public CarBasicConfiguration(int id, string? name)
[HelperText("If enabled, some data will be transferred via Fleet Telemetry. This improves the delay in the TSC detection of plugin and out of the car, as well as changes in the charging speed. Note: All data transferred via Fleet Telemetry passes my server. For now, the fleet telemetry fields ModuleTempMin, ModuleTempMax,ChargeAmps, ChargeCurrentRequest, ChargeCurrentRequestMax, DetailedChargeState are requested.")]
public bool UseFleetTelemetry { get; set; }
- [HelperText("Enabling this results in additionally streaming the field Location over my server. If disabled and TeslaMate is not selected as DataSource TSC takes up to 8 minutes to detect if the car is at home. If you do not mind that your car location data passes my server, do not disable this option.")]
+ [HelperText("This further improves the detection if the car is at home. Enabling this results in additionally streaming the field Location over my server. If you do not mind that your car location data passes my server, do not disable this option.")]
public bool UseFleetTelemetryForLocationData { get; set; } = true;
}
diff --git a/TeslaSolarCharger/Shared/Dtos/ChargingCost/DtoHandledCharge.cs b/TeslaSolarCharger/Shared/Dtos/ChargingCost/DtoHandledCharge.cs
index 207b3353d..5553560d7 100644
--- a/TeslaSolarCharger/Shared/Dtos/ChargingCost/DtoHandledCharge.cs
+++ b/TeslaSolarCharger/Shared/Dtos/ChargingCost/DtoHandledCharge.cs
@@ -2,7 +2,8 @@
public class DtoHandledCharge
{
- public DateTime? StartTime { get; set; }
+ public DateTime StartTime { get; set; }
+ public DateTime? EndTime { get; set; }
public decimal CalculatedPrice { get; set; }
public decimal PricePerKwh { get; set; }
public decimal UsedGridEnergy { get; set; }
diff --git a/TeslaSolarCharger/Shared/Dtos/Contracts/ISettings.cs b/TeslaSolarCharger/Shared/Dtos/Contracts/ISettings.cs
index 1adca29ff..d26c97cbb 100644
--- a/TeslaSolarCharger/Shared/Dtos/Contracts/ISettings.cs
+++ b/TeslaSolarCharger/Shared/Dtos/Contracts/ISettings.cs
@@ -6,7 +6,6 @@ public interface ISettings
{
int? InverterPower { get; set; }
int? Overage { get; set; }
- int? PowerBuffer { get; set; }
int? HomeBatterySoc { get; set; }
int? HomeBatteryPower { get; set; }
bool ControlledACarAtLastCycle { get; set; }
diff --git a/TeslaSolarCharger/Shared/Dtos/Settings/Settings.cs b/TeslaSolarCharger/Shared/Dtos/Settings/Settings.cs
index 7c9227673..e8f43835e 100644
--- a/TeslaSolarCharger/Shared/Dtos/Settings/Settings.cs
+++ b/TeslaSolarCharger/Shared/Dtos/Settings/Settings.cs
@@ -7,7 +7,6 @@ public class Settings : ISettings
public bool IsNewVersionAvailable { get; set; }
public int? InverterPower { get; set; }
public int? Overage { get; set; }
- public int? PowerBuffer { get; set; }
public List CarsToManage => Cars.Where(c => c.ShouldBeManaged == true).OrderBy(c => c.ChargingPriority).ToList();
public int? HomeBatterySoc { get; set; }
public int? HomeBatteryPower { get; set; }
diff --git a/TeslaSolarCharger/Shared/Dtos/TimeSeries/DtoTimeSeriesDatum.cs b/TeslaSolarCharger/Shared/Dtos/TimeSeries/DtoTimeSeriesDatum.cs
new file mode 100644
index 000000000..d84e62921
--- /dev/null
+++ b/TeslaSolarCharger/Shared/Dtos/TimeSeries/DtoTimeSeriesDatum.cs
@@ -0,0 +1,9 @@
+using TeslaSolarCharger.Shared.Enums;
+
+namespace TeslaSolarCharger.Shared.Dtos.TimeSeries;
+
+public class DtoTimeSeriesDatum
+{
+ public DateTime Timestamp { get; set; }
+ public double? Value { get; set; }
+}
diff --git a/TeslaSolarCharger/Shared/Enums/CarValueSource.cs b/TeslaSolarCharger/Shared/Enums/CarValueSource.cs
index 57d14690c..4db3dbfda 100644
--- a/TeslaSolarCharger/Shared/Enums/CarValueSource.cs
+++ b/TeslaSolarCharger/Shared/Enums/CarValueSource.cs
@@ -4,4 +4,5 @@ public enum CarValueSource
{
FleetTelemetry,
FleetApi,
+ TeslaMate,
}
diff --git a/TeslaSolarCharger/Shared/Enums/CarValueType.cs b/TeslaSolarCharger/Shared/Enums/CarValueType.cs
index 327a605fc..ecb241efc 100644
--- a/TeslaSolarCharger/Shared/Enums/CarValueType.cs
+++ b/TeslaSolarCharger/Shared/Enums/CarValueType.cs
@@ -16,5 +16,10 @@ public enum CarValueType
Location,
Longitude,
Latitude,
+ StateOfCharge,
+ StateOfChargeLimit,
+ ChargerPhases,
+ ChargerVoltage,
+ AsleepOrOffline,
Unknown = 9999,
}
diff --git a/TeslaSolarCharger/Shared/TeslaSolarCharger.Shared.csproj b/TeslaSolarCharger/Shared/TeslaSolarCharger.Shared.csproj
index a668c6612..6a6bb58f1 100644
--- a/TeslaSolarCharger/Shared/TeslaSolarCharger.Shared.csproj
+++ b/TeslaSolarCharger/Shared/TeslaSolarCharger.Shared.csproj
@@ -12,17 +12,18 @@
+
-
+
-
+
diff --git a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs
index fd85b529b..e9d7e92ff 100644
--- a/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs
+++ b/TeslaSolarCharger/Shared/Wrappers/ConfigurationWrapper.cs
@@ -559,16 +559,8 @@ public TimeSpan TimespanUntilSwitchOff()
return value;
}
- public int PowerBuffer(bool getInMemoryValueIfAvailable)
+ public int PowerBuffer()
{
- if (getInMemoryValueIfAvailable)
- {
- var settingsPowerBuffer = settings.PowerBuffer;
- if (settingsPowerBuffer != null)
- {
- return settingsPowerBuffer.Value;
- }
- }
return GetBaseConfiguration().PowerBuffer;
}