diff --git a/.github/workflows/alphaRelease.yml b/.github/workflows/alphaRelease.yml index 362397e14..46dd789a1 100644 --- a/.github/workflows/alphaRelease.yml +++ b/.github/workflows/alphaRelease.yml @@ -42,7 +42,7 @@ jobs: push: true tags: pkuehnel/smartteslaampsetter:alpha - SmaEnergymeterplugin: + SmaEnergymeterPlugin: name: Building SMAPlugin Image runs-on: ubuntu-latest @@ -70,3 +70,32 @@ jobs: platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true tags: pkuehnel/smartteslaampsettersmaplugin:alpha + + SolarEdgePlugin: + name: Building SolarEdgePlugin Image + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: DockerHub login + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASS}} + + - name: Build and push SolarEdgePlugin + uses: docker/build-push-action@v2 + with: + file: ./Plugins.SolarEdge/Dockerfile + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: true + tags: pkuehnel/smartteslaampsettersolaredgeplugin:alpha diff --git a/.github/workflows/edgeRelease.yml b/.github/workflows/edgeRelease.yml index 975b72829..cd4edcc1e 100644 --- a/.github/workflows/edgeRelease.yml +++ b/.github/workflows/edgeRelease.yml @@ -44,7 +44,7 @@ jobs: push: true tags: pkuehnel/smartteslaampsetter:edge - SmaEnergymeterplugin: + SmaEnergymeterPlugin: name: Building SMAPlugin Image runs-on: ubuntu-latest @@ -72,3 +72,32 @@ jobs: platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true tags: pkuehnel/smartteslaampsettersmaplugin:edge + + SolarEdgePlugin: + name: Building SolarEdgePlugin Image + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: DockerHub login + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASS}} + + - name: Build and push SolarEdgePlugin + uses: docker/build-push-action@v2 + with: + file: ./Plugins.SolarEdge/Dockerfile + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: true + tags: pkuehnel/smartteslaampsettersolaredgeplugin:edge diff --git a/.github/workflows/versionRelease.yml b/.github/workflows/versionRelease.yml index 1cc63acd7..7e356776b 100644 --- a/.github/workflows/versionRelease.yml +++ b/.github/workflows/versionRelease.yml @@ -72,6 +72,22 @@ jobs: # Delete output directory rm -r "$release_name" + - name: Build Plugins.SolarEdge + shell: bash + run: | + # Define some variables for things we need + tag=$(git describe --tags --abbrev=0) + release_name="Plugins.SolarEdge-$tag-${{ matrix.kind }}" + + # Build everything + dotnet publish Plugins.SolarEdge/Plugins.SolarEdge.csproj --runtime "${{ matrix.kind }}" -c Release -o "$release_name" + + # Pack files + tar czvf "${release_name}.tar.gz" "$release_name" + + # Delete output directory + rm -r "$release_name" + - name: Publish uses: softprops/action-gh-release@v1 with: @@ -157,4 +173,44 @@ jobs: file: ./Plugins.SmaEnergymeter/Dockerfile platforms: linux/amd64,linux/arm64,linux/arm/v7 push: true + tags: ${{ steps.meta.outputs.tags }} + + releaseDockerLatestSolarEdgeplugin: + name: Building SolarEdgePlugin Image + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Docker meta + id: meta + uses: docker/metadata-action@v3 + with: + # list of Docker images to use as base name for tags + images: | + pkuehnel/smartteslaampsettersolaredgeplugin + # generate Docker tags based on the following events/attributes + tags: | + type=semver,pattern={{version}} + + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + + - name: Buildx + id: buildx + uses: docker/setup-buildx-action@v1 + + - name: DockerHub login + uses: docker/login-action@v1 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_PASS}} + + - name: Build and push SolarEdgePlugin + uses: docker/build-push-action@v2 + with: + file: ./Plugins.SolarEdge/Dockerfile + platforms: linux/amd64,linux/arm64,linux/arm/v7 + push: true tags: ${{ steps.meta.outputs.tags }} \ No newline at end of file diff --git a/Plugins.SmaEnergymeter/SharedValues.cs b/Plugins.SmaEnergymeter/SharedValues.cs index c035a9a04..b0b9a6c79 100644 --- a/Plugins.SmaEnergymeter/SharedValues.cs +++ b/Plugins.SmaEnergymeter/SharedValues.cs @@ -1,6 +1,4 @@ -using Plugins.SmaEnergymeter.Dtos; - -namespace Plugins.SmaEnergymeter; +namespace Plugins.SmaEnergymeter; public class SharedValues { diff --git a/Plugins.SmaEnergymeter/appsettings.json b/Plugins.SmaEnergymeter/appsettings.json index 5f46c4f23..7fe25fecb 100644 --- a/Plugins.SmaEnergymeter/appsettings.json +++ b/Plugins.SmaEnergymeter/appsettings.json @@ -13,7 +13,10 @@ }, "WriteTo": [ { - "Name": "Console" + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss.fff} {Level:u3} {SourceContext}] {Message:lj}{NewLine}{Exception}" + } } ], "Enrich": [ diff --git a/Plugins.SolarEdge/Contracts/ICurrentValuesService.cs b/Plugins.SolarEdge/Contracts/ICurrentValuesService.cs new file mode 100644 index 000000000..9c94a92d8 --- /dev/null +++ b/Plugins.SolarEdge/Contracts/ICurrentValuesService.cs @@ -0,0 +1,7 @@ +namespace Plugins.SolarEdge.Contracts; + +public interface ICurrentValuesService +{ + Task GetCurrentPowerToGrid(); + Task GetInverterPower(); +} \ No newline at end of file diff --git a/Plugins.SolarEdge/Controllers/CurrentValuesController.cs b/Plugins.SolarEdge/Controllers/CurrentValuesController.cs new file mode 100644 index 000000000..b8fa10cb1 --- /dev/null +++ b/Plugins.SolarEdge/Controllers/CurrentValuesController.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Mvc; +using Plugins.SolarEdge.Contracts; + +namespace Plugins.SolarEdge.Controllers; + +[Route("api/[controller]/[action]")] +[ApiController] +public class CurrentValuesController : ControllerBase +{ + private readonly ICurrentValuesService _currentValuesService; + + public CurrentValuesController(ICurrentValuesService currentValuesService) + { + _currentValuesService = currentValuesService; + } + + [HttpGet] + public Task GetPowerToGrid() + { + return _currentValuesService.GetCurrentPowerToGrid(); + } + + [HttpGet] + public Task GetInverterPower() + { + return _currentValuesService.GetInverterPower(); + } +} \ No newline at end of file diff --git a/Plugins.SolarEdge/Dockerfile b/Plugins.SolarEdge/Dockerfile new file mode 100644 index 000000000..64e1c8e18 --- /dev/null +++ b/Plugins.SolarEdge/Dockerfile @@ -0,0 +1,22 @@ +#See https://aka.ms/containerfastmode to understand how Visual Studio uses this Dockerfile to build your images for faster debugging. + +FROM mcr.microsoft.com/dotnet/aspnet:6.0 AS base +WORKDIR /app +EXPOSE 80 +EXPOSE 443 + +FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim-amd64 AS build +WORKDIR /src +COPY ["Plugins.SolarEdge/Plugins.SolarEdge.csproj", "Plugins.SolarEdge/"] +RUN dotnet restore "Plugins.SolarEdge/Plugins.SolarEdge.csproj" +COPY . . +WORKDIR "/src/Plugins.SolarEdge" +RUN dotnet build "Plugins.SolarEdge.csproj" -c Release -o /app/build + +FROM build AS publish +RUN dotnet publish "Plugins.SolarEdge.csproj" -c Release -o /app/publish + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Plugins.SolarEdge.dll"] \ No newline at end of file diff --git a/Plugins.SolarEdge/Dtos/CloudApi/CloudApiValue.cs b/Plugins.SolarEdge/Dtos/CloudApi/CloudApiValue.cs new file mode 100644 index 000000000..e60f75cfc --- /dev/null +++ b/Plugins.SolarEdge/Dtos/CloudApi/CloudApiValue.cs @@ -0,0 +1,8 @@ +namespace Plugins.SolarEdge.Dtos.CloudApi; + +// CloudApiValue myDeserializedClass = JsonConvert.DeserializeObject(myJsonResponse); + +public class CloudApiValue +{ + public SiteCurrentPowerFlow SiteCurrentPowerFlow { get; set; } +} \ No newline at end of file diff --git a/Plugins.SolarEdge/Dtos/CloudApi/Connection.cs b/Plugins.SolarEdge/Dtos/CloudApi/Connection.cs new file mode 100644 index 000000000..912549bb1 --- /dev/null +++ b/Plugins.SolarEdge/Dtos/CloudApi/Connection.cs @@ -0,0 +1,7 @@ +namespace Plugins.SolarEdge.Dtos.CloudApi; + +public class Connection +{ + public string From { get; set; } + public string To { get; set; } +} \ No newline at end of file diff --git a/Plugins.SolarEdge/Dtos/CloudApi/GRID.cs b/Plugins.SolarEdge/Dtos/CloudApi/GRID.cs new file mode 100644 index 000000000..42b21984a --- /dev/null +++ b/Plugins.SolarEdge/Dtos/CloudApi/GRID.cs @@ -0,0 +1,7 @@ +namespace Plugins.SolarEdge.Dtos.CloudApi; + +public class Grid +{ + public string Status { get; set; } + public double CurrentPower { get; set; } +} \ No newline at end of file diff --git a/Plugins.SolarEdge/Dtos/CloudApi/LOAD.cs b/Plugins.SolarEdge/Dtos/CloudApi/LOAD.cs new file mode 100644 index 000000000..cd3c01f76 --- /dev/null +++ b/Plugins.SolarEdge/Dtos/CloudApi/LOAD.cs @@ -0,0 +1,7 @@ +namespace Plugins.SolarEdge.Dtos.CloudApi; + +public class Load +{ + public string Status { get; set; } + public double CurrentPower { get; set; } +} \ No newline at end of file diff --git a/Plugins.SolarEdge/Dtos/CloudApi/PV.cs b/Plugins.SolarEdge/Dtos/CloudApi/PV.cs new file mode 100644 index 000000000..9ffb23688 --- /dev/null +++ b/Plugins.SolarEdge/Dtos/CloudApi/PV.cs @@ -0,0 +1,7 @@ +namespace Plugins.SolarEdge.Dtos.CloudApi; + +public class Pv +{ + public string Status { get; set; } + public double CurrentPower { get; set; } +} \ No newline at end of file diff --git a/Plugins.SolarEdge/Dtos/CloudApi/STORAGE.cs b/Plugins.SolarEdge/Dtos/CloudApi/STORAGE.cs new file mode 100644 index 000000000..907df4ee6 --- /dev/null +++ b/Plugins.SolarEdge/Dtos/CloudApi/STORAGE.cs @@ -0,0 +1,9 @@ +namespace Plugins.SolarEdge.Dtos.CloudApi; + +public class Storage +{ + public string Status { get; set; } + public double CurrentPower { get; set; } + public int ChargeLevel { get; set; } + public bool Critical { get; set; } +} \ No newline at end of file diff --git a/Plugins.SolarEdge/Dtos/CloudApi/SiteCurrentPowerFlow.cs b/Plugins.SolarEdge/Dtos/CloudApi/SiteCurrentPowerFlow.cs new file mode 100644 index 000000000..27c91b705 --- /dev/null +++ b/Plugins.SolarEdge/Dtos/CloudApi/SiteCurrentPowerFlow.cs @@ -0,0 +1,12 @@ +namespace Plugins.SolarEdge.Dtos.CloudApi; + +public class SiteCurrentPowerFlow +{ + public int UpdateRefreshRate { get; set; } + public string Unit { get; set; } + public List Connections { get; set; } + public Grid Grid { get; set; } + public Load Load { get; set; } + public Pv Pv { get; set; } + public Storage Storage { get; set; } +} \ No newline at end of file diff --git a/Plugins.SolarEdge/Plugins.SolarEdge.csproj b/Plugins.SolarEdge/Plugins.SolarEdge.csproj new file mode 100644 index 000000000..4c8536b54 --- /dev/null +++ b/Plugins.SolarEdge/Plugins.SolarEdge.csproj @@ -0,0 +1,23 @@ + + + + net6.0 + enable + enable + Linux + + + + + + + + + + + + + + + + diff --git a/Plugins.SolarEdge/Program.cs b/Plugins.SolarEdge/Program.cs new file mode 100644 index 000000000..3e5a2e194 --- /dev/null +++ b/Plugins.SolarEdge/Program.cs @@ -0,0 +1,45 @@ +using Plugins.SolarEdge; +using Plugins.SolarEdge.Contracts; +using Plugins.SolarEdge.Services; +using Serilog; + +var builder = WebApplication.CreateBuilder(args); + +// Add services to the container. + +builder.Services.AddControllers(); +// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(); + +builder.Services.AddSingleton(); +builder.Services.AddTransient() + ; + +builder.Host.UseSerilog((context, configuration) => configuration + .ReadFrom.Configuration(context.Configuration)); + +builder.Configuration + .AddJsonFile("appsettings.json") + .AddEnvironmentVariables(); + +var environment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT"); +if (environment == "Development") +{ + builder.Configuration.AddJsonFile("appsettings.Development.json"); +} + +var app = builder.Build(); + +// Configure the HTTP request pipeline. +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseAuthorization(); + +app.MapControllers(); + +app.Run(); diff --git a/Plugins.SolarEdge/Properties/launchSettings.json b/Plugins.SolarEdge/Properties/launchSettings.json new file mode 100644 index 000000000..30f4a2e7a --- /dev/null +++ b/Plugins.SolarEdge/Properties/launchSettings.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:19132", + "sslPort": 0 + } + }, + "profiles": { + "Plugins.SolarEdge": { + "commandName": "Project", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5084", + "dotnetRunMessages": true + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}/swagger", + "publishAllPorts": true + } + } +} \ No newline at end of file diff --git a/Plugins.SolarEdge/Services/CurrentValuesService.cs b/Plugins.SolarEdge/Services/CurrentValuesService.cs new file mode 100644 index 000000000..417969962 --- /dev/null +++ b/Plugins.SolarEdge/Services/CurrentValuesService.cs @@ -0,0 +1,95 @@ +using System.Runtime.CompilerServices; +using Newtonsoft.Json; +using Plugins.SolarEdge.Contracts; +using Plugins.SolarEdge.Dtos.CloudApi; + +[assembly: InternalsVisibleTo("SmartTeslaAmpSetter.Tests")] +namespace Plugins.SolarEdge.Services; + +public class CurrentValuesService : ICurrentValuesService +{ + private readonly ILogger _logger; + private readonly SharedValues _sharedValues; + private readonly IConfiguration _configuration; + + public CurrentValuesService(ILogger logger, SharedValues sharedValues, IConfiguration configuration) + { + _logger = logger; + _sharedValues = sharedValues; + _configuration = configuration; + } + + public async Task GetCurrentPowerToGrid() + { + _logger.LogTrace("{method}()", nameof(GetCurrentPowerToGrid)); + var latestValue = await GetLatestValue(); + + var value = (int)latestValue.SiteCurrentPowerFlow.Grid.CurrentPower; + if (latestValue.SiteCurrentPowerFlow.Unit == "kW") + { + value *= 1000; + } + + if (latestValue.SiteCurrentPowerFlow.Connections.Any(c => c.From == "GRID")) + { + value = -value; + } + return value; + } + + public async Task GetInverterPower() + { + _logger.LogTrace("{method}()", nameof(GetInverterPower)); + var latestValue = await GetLatestValue(); + + if (latestValue.SiteCurrentPowerFlow.Unit == "kW") + { + return (int)(latestValue.SiteCurrentPowerFlow.Pv.CurrentPower * 1000); + } + + return (int)latestValue.SiteCurrentPowerFlow.Pv.CurrentPower; + } + + private async Task GetLatestValue() + { + var refreshIntervall = TimeSpan.FromSeconds(_configuration.GetValue("RefreshIntervallSeconds")); + _logger.LogDebug("Refresh Intervall is {refreshIntervall}", refreshIntervall); + + + if (_sharedValues.CloudApiValues.Count < 1 + || _sharedValues.CloudApiValues.Last().Key < DateTime.UtcNow - refreshIntervall) + { + _logger.LogDebug("Get new Values from SolarEdge API"); + var jsonString = await GetCloudApiString().ConfigureAwait(false); + var cloudApiValue = GetCloudApiValueFromString(jsonString); + AddCloudApiValueToSharedValues(cloudApiValue); + } + + var latestValue = _sharedValues.CloudApiValues.Last().Value; + return latestValue; + } + + private void AddCloudApiValueToSharedValues(CloudApiValue cloudApiValue) + { + _logger.LogTrace("{method}({param1})", nameof(AddCloudApiValueToSharedValues), cloudApiValue); + var currentDateTime = DateTime.UtcNow; + _sharedValues.CloudApiValues.Add(currentDateTime, cloudApiValue); + } + + private async Task GetCloudApiString() + { + _logger.LogTrace("{method}()", nameof(GetCloudApiString)); + using var httpClient = new HttpClient(); + var requestUrl = _configuration.GetValue("CloudUrl"); + _logger.LogDebug("Request URL is {requestUrl}", requestUrl); + var response = await httpClient.GetAsync(requestUrl).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + return await response.Content.ReadAsStringAsync().ConfigureAwait(false); + } + + internal CloudApiValue GetCloudApiValueFromString(string jsonString) + { + _logger.LogTrace("{method}({param1}", nameof(GetCloudApiValueFromString), jsonString); + return JsonConvert.DeserializeObject(jsonString) ?? throw new InvalidOperationException("Can not deserialize CloudApiValue"); + } +} \ No newline at end of file diff --git a/Plugins.SolarEdge/SharedValues.cs b/Plugins.SolarEdge/SharedValues.cs new file mode 100644 index 000000000..57f943af0 --- /dev/null +++ b/Plugins.SolarEdge/SharedValues.cs @@ -0,0 +1,12 @@ +using Plugins.SolarEdge.Dtos.CloudApi; + +namespace Plugins.SolarEdge; + +public class SharedValues +{ + public SharedValues() + { + CloudApiValues = new Dictionary(); + } + public Dictionary CloudApiValues { get; set; } +} \ No newline at end of file diff --git a/Plugins.SolarEdge/appsettings.Development.json b/Plugins.SolarEdge/appsettings.Development.json new file mode 100644 index 000000000..2444bcb30 --- /dev/null +++ b/Plugins.SolarEdge/appsettings.Development.json @@ -0,0 +1,29 @@ +{ + "Serilog": { + "Using": [ + "Serilog.Sinks.Console" + ], + "MinimumLevel": { + "Default": "Verbose", + "Override": { + "Microsoft": "Information", + "System": "Error", + "Microsoft.EntityFrameworkCore.Database.Command": "Information" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss.fff} {Level:u3} {SourceContext}] {Message:lj}{NewLine}{Exception}" + } + } + ], + "Enrich": [ + "FromLogContext" + ] + }, + "AllowedHosts": "*", + "CloudUrl": "https://monitoringapi.solaredge.com/site/????????/currentPowerFlow.json?api_key=asdfasfasdfasdfasfd", + "RefreshIntervallSeconds": 300 +} diff --git a/Plugins.SolarEdge/appsettings.json b/Plugins.SolarEdge/appsettings.json new file mode 100644 index 000000000..4c1b8cdf5 --- /dev/null +++ b/Plugins.SolarEdge/appsettings.json @@ -0,0 +1,29 @@ +{ + "Serilog": { + "Using": [ + "Serilog.Sinks.Console" + ], + "MinimumLevel": { + "Default": "Verbose", + "Override": { + "Microsoft": "Information", + "System": "Error", + "Microsoft.EntityFrameworkCore.Database.Command": "Information" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss.fff} {Level:u3} {SourceContext}] {Message:lj}{NewLine}{Exception}" + } + } + ], + "Enrich": [ + "FromLogContext" + ] + }, + "AllowedHosts": "*", + "CloudUrl": "https://monitoringapi.solaredge.com/site/????????/currentPowerFlow.json?api_key=asdfasdfasdfasdfasdfasdf", + "RefreshIntervallSeconds": 300 +} diff --git a/README.md b/README.md index 35d9d8a09..af8381c7a 100644 --- a/README.md +++ b/README.md @@ -21,8 +21,12 @@ Needs: - [UI](#UI) - [Charge Modes](#charge-modes) - [Telegram Notifications](#telegram-notifications) + - [Getting Values from XML](#getting-values-from-xml) + - [Grid-Power](#grid-power) + - [Inverter-Power](#inverter-power) - [Plugins](#plugins) - [SMA-EnergyMeter Plugin](#sma-energymeter-plugin) + - [Solaredge Plugin](#solaredge-plugin) ## How to use @@ -109,8 +113,14 @@ Note: TeslaMateApi has to be configured to allow any command without authenticat | **PowerBuffer** | int | Power Buffer in Watt | 0 | | **CurrentPowerToGridJsonPattern** | string | If Power to grid is json formated use this to extract the correct value | $.data.overage | | **CurrentPowerToGridInvertValue** | boolean | Set this to `true` if Power from grid has positive values and power to grid has negative values | true | +| **CurrentInverterPowerJsonPattern** | string | If Power from inverter is json formated use this to extract the correct value | $.data.overage | | **TelegramBotKey** | string | Telegram Bot API key | 1234567890:ASDFuiauhwerlfvasedr | | **TelegramChannelId** | string | ChannelId Telegram bot should send messages to | -156480125 | +| **TeslaMateDbServer** | string | Name or IP Address of the TeslaMate database service | database | +| **TeslaMateDbPort** | int | Port of the TeslaMate database service | 5432 | +| **TeslaMateDbDatabaseName** | string | Database Name of the TeslaMate database service | teslamate | +| **TeslaMateDbUser** | string | Database user name of the TeslaMate database service | teslamate | +| **TeslaMateDbPassword** | string | Database user's password of the TeslaMate database service | secret | ### Car Priorities If you set `CarPriorities` environment variable like the example above, the car with ID 2 will only start charing, if car 1 is charging at full speed and there is still power left, or if car 1 is not charging due to reached battery limit or not within specified geofence. Note: You always have to add the car Ids to this list separated by `|`. Even if you only have one car you need to ad the car's Id but then without `|`. @@ -131,6 +141,51 @@ Currently there are three different charge modes available: If you set the environment variables `TelegramBotKey`and `TelegramChannelId`, you get messages, if a car can not be woken up, or any command could not be sent to a Tesla. Note: If your car takes longer than 30 seconds to wake up probably you will get an error notification, but as soon as the car is online charging starts. You can check if your Key and Channel Id is working by restarting the container. If your configuration is working, on startup the application sends a demo message to the specified Telegram channel/chat. +### Getting Values from XML +If your energy monitoring device or inverter has no JSON but an XML API use the following instructions: +Given an API endpoint `http://192.168.xxx.xxx/measurements.xml` which returns the following XML: +```xml + + + + + + + + + + + + + + + + + + +``` + +#### Grid-Power +Assuming the `Measurement` node with `Type` `GridPower` is the power your house feeds to the grid you need the following environment variables: +```yaml +- CurrentPowerToGridUrl=http://192.168.xxx.xxx/measurements.xml +- CurrentPowerToGridXmlPattern=Device/Measurements/Measurement +- CurrentPowerToGridXmlAttributeHeaderName=Type +- CurrentPowerToGridXmlAttributeHeaderValue=GridPower +- CurrentPowerToGridXmlAttributeValueName=Value +``` + +#### Inverter-Power +Assuming the `Measurement` node with `Type` `AC_Power` is the power your inverter is currently feeding you can use the following environment variables: +```yaml +- CurrentInverterPowerUrl=http://192.168.xxx.xxx/measurements.xml +- CurrentInverterPowerXmlPattern=Device/Measurements/Measurement +- CurrentInverterPowerAttributeHeaderName=Type +- CurrentInverterPowerAttributeHeaderValue=AC_Power +- CurrentInverterPowerAttributeValueName=Value +``` +Note: This values are not needed, they are just used to show additional information. + ### Plugins If your SmartMeter does not have a REST Endpoint as needed you can use plugins: @@ -155,3 +210,33 @@ services: environment: - ASPNETCORE_URLS=http://+:8453 ``` + +#### Solaredge Plugin +[![Docker version](https://img.shields.io/docker/v/pkuehnel/smartteslaampsettersolaredgeplugin/latest)](https://hub.docker.com/r/pkuehnel/smartteslaampsettersolaredgeplugin) +[![Docker size](https://img.shields.io/docker/image-size/pkuehnel/smartteslaampsettersolaredgeplugin/latest)](https://hub.docker.com/r/pkuehnel/smartteslaampsettersolaredgeplugin) +[![Docker pulls](https://img.shields.io/docker/pulls/pkuehnel/smartteslaampsettersolaredgeplugin)](https://hub.docker.com/r/pkuehnel/smartteslaampsettersolaredgeplugin) + +Currently only the cloud API is supported. As there are not allowed more than 300 requests per plant, IP address and day, this integration is at this state very limited as you can only get the current power every few minutes. To use the solaredge plugin, you have to add another service to your `docker-compose.yml`: +```yaml +services: + solaredgeplugin: + image: pkuehnel/smartteslaampsettersolaredgeplugin:solaredge + logging: + driver: "json-file" + options: + max-file: "5" + max-size: "10m" + restart: always + environment: + - ASPNETCORE_URLS=http://+:8453 + - CloudUrl=https://monitoringapi.solaredge.com/site/1561056/currentPowerFlow.json?api_key=asdfasdfasdfasdfasdfasdf& + - RefreshIntervallSeconds=360 + ports: + - 8453:8453 +``` +Note: You have to change the cloud URL and also can change the refresh intervall. The default refresh intervall of 360 results in 240 of 300 allowed API calls per day. +To use the plugin in the `smartteslaampsetter` you have to add the following environmentvariables to the `smartteslaampsetter` service: +```yaml +- CurrentPowerToGridUrl=http://solaredgeplugin:8453/api/CurrentValues/GetPowerToGrid +- CurrentInverterPowerUrl=http://solaredgeplugin:8453/api/CurrentValues/GetInverterPower +``` diff --git a/SmartTeslaAmpSetter.Model/Contracts/IDbConnectionStringHelper.cs b/SmartTeslaAmpSetter.Model/Contracts/IDbConnectionStringHelper.cs new file mode 100644 index 000000000..57619cfe6 --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Contracts/IDbConnectionStringHelper.cs @@ -0,0 +1,6 @@ +namespace SmartTeslaAmpSetter.Model.Contracts; + +public interface IDbConnectionStringHelper +{ + string GetConnectionString(); +} \ No newline at end of file diff --git a/SmartTeslaAmpSetter.Model/Contracts/ITeslamateContext.cs b/SmartTeslaAmpSetter.Model/Contracts/ITeslamateContext.cs new file mode 100644 index 000000000..8a813bf65 --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Contracts/ITeslamateContext.cs @@ -0,0 +1,21 @@ +using Microsoft.EntityFrameworkCore; +using SmartTeslaAmpSetter.Model.Entities; + +namespace SmartTeslaAmpSetter.Model.Contracts; + +public interface ITeslamateContext +{ + DbSet
Addresses { get; set; } + DbSet Cars { get; set; } + DbSet CarSettings { get; set; } + DbSet Charges { get; set; } + DbSet ChargingProcesses { get; set; } + DbSet Drives { get; set; } + DbSet Geofences { get; set; } + DbSet Positions { get; set; } + DbSet SchemaMigrations { get; set; } + DbSet Settings { get; set; } + DbSet States { get; set; } + DbSet Tokens { get; set; } + DbSet Updates { get; set; } +} \ No newline at end of file diff --git a/SmartTeslaAmpSetter.Model/Entities/Address.cs b/SmartTeslaAmpSetter.Model/Entities/Address.cs new file mode 100644 index 000000000..9a929ef3f --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Entities/Address.cs @@ -0,0 +1,36 @@ +namespace SmartTeslaAmpSetter.Model.Entities +{ + public class Address + { + public Address() + { + ChargingProcesses = new HashSet(); + DriveEndAddresses = new HashSet(); + DriveStartAddresses = new HashSet(); + } + + public int Id { get; set; } + public string? DisplayName { get; set; } + public decimal? Latitude { get; set; } + public decimal? Longitude { get; set; } + public string? Name { get; set; } + public string? HouseNumber { get; set; } + public string? Road { get; set; } + public string? Neighbourhood { get; set; } + public string? City { get; set; } + public string? County { get; set; } + public string? Postcode { get; set; } + public string? State { get; set; } + public string? StateDistrict { get; set; } + public string? Country { get; set; } + public string? Raw { get; set; } + public DateTime InsertedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public long? OsmId { get; set; } + public string? OsmType { get; set; } + + public virtual ICollection ChargingProcesses { get; set; } + public virtual ICollection DriveEndAddresses { get; set; } + public virtual ICollection DriveStartAddresses { get; set; } + } +} diff --git a/SmartTeslaAmpSetter.Model/Entities/Car.cs b/SmartTeslaAmpSetter.Model/Entities/Car.cs new file mode 100644 index 000000000..0b8cd2dd4 --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Entities/Car.cs @@ -0,0 +1,37 @@ +namespace SmartTeslaAmpSetter.Model.Entities +{ + public class Car + { + public Car() + { + ChargingProcesses = new HashSet(); + Drives = new HashSet(); + Positions = new HashSet(); + States = new HashSet(); + Updates = new HashSet(); + } + + public short Id { get; set; } + public long Eid { get; set; } + public long Vid { get; set; } + public string? Model { get; set; } + public double? Efficiency { get; set; } + public DateTime InsertedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public string? Vin { get; set; } + public string? Name { get; set; } + public string? TrimBadging { get; set; } + public long SettingsId { get; set; } + public string? ExteriorColor { get; set; } + public string? SpoilerType { get; set; } + public string? WheelType { get; set; } + public short DisplayPriority { get; set; } + + public virtual CarSetting Settings { get; set; } = null!; + public virtual ICollection ChargingProcesses { get; set; } + public virtual ICollection Drives { get; set; } + public virtual ICollection Positions { get; set; } + public virtual ICollection States { get; set; } + public virtual ICollection Updates { get; set; } + } +} diff --git a/SmartTeslaAmpSetter.Model/Entities/CarSetting.cs b/SmartTeslaAmpSetter.Model/Entities/CarSetting.cs new file mode 100644 index 000000000..4f7188340 --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Entities/CarSetting.cs @@ -0,0 +1,14 @@ +namespace SmartTeslaAmpSetter.Model.Entities +{ + public class CarSetting + { + public long Id { get; set; } + public int SuspendMin { get; set; } + public int SuspendAfterIdleMin { get; set; } + public bool ReqNotUnlocked { get; set; } + public bool FreeSupercharging { get; set; } + public bool? UseStreamingApi { get; set; } + + public virtual Car Car { get; set; } = null!; + } +} diff --git a/SmartTeslaAmpSetter.Model/Entities/Charge.cs b/SmartTeslaAmpSetter.Model/Entities/Charge.cs new file mode 100644 index 000000000..c26986112 --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Entities/Charge.cs @@ -0,0 +1,30 @@ +namespace SmartTeslaAmpSetter.Model.Entities +{ + public class Charge + { + public int Id { get; set; } + public DateTime Date { get; set; } + public bool? BatteryHeaterOn { get; set; } + public short? BatteryLevel { get; set; } + public decimal ChargeEnergyAdded { get; set; } + public short? ChargerActualCurrent { get; set; } + public short? ChargerPhases { get; set; } + public short? ChargerPilotCurrent { get; set; } + public short ChargerPower { get; set; } + public short? ChargerVoltage { get; set; } + public bool? FastChargerPresent { get; set; } + public string? ConnChargeCable { get; set; } + public string? FastChargerBrand { get; set; } + public string? FastChargerType { get; set; } + public decimal IdealBatteryRangeKm { get; set; } + public bool? NotEnoughPowerToHeat { get; set; } + public decimal? OutsideTemp { get; set; } + public int ChargingProcessId { get; set; } + public bool? BatteryHeater { get; set; } + public bool? BatteryHeaterNoPower { get; set; } + public decimal? RatedBatteryRangeKm { get; set; } + public short? UsableBatteryLevel { get; set; } + + public virtual ChargingProcess ChargingProcess { get; set; } = null!; + } +} diff --git a/SmartTeslaAmpSetter.Model/Entities/ChargingProcess.cs b/SmartTeslaAmpSetter.Model/Entities/ChargingProcess.cs new file mode 100644 index 000000000..8fa99ed26 --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Entities/ChargingProcess.cs @@ -0,0 +1,35 @@ +namespace SmartTeslaAmpSetter.Model.Entities +{ + public class ChargingProcess + { + public ChargingProcess() + { + Charges = new HashSet(); + } + + public int Id { get; set; } + public DateTime StartDate { get; set; } + public DateTime? EndDate { get; set; } + public decimal? ChargeEnergyAdded { get; set; } + public decimal? StartIdealRangeKm { get; set; } + public decimal? EndIdealRangeKm { get; set; } + public short? StartBatteryLevel { get; set; } + public short? EndBatteryLevel { get; set; } + public short? DurationMin { get; set; } + public decimal? OutsideTempAvg { get; set; } + public short CarId { get; set; } + public int PositionId { get; set; } + public int? AddressId { get; set; } + public decimal? StartRatedRangeKm { get; set; } + public decimal? EndRatedRangeKm { get; set; } + public int? GeofenceId { get; set; } + public decimal? ChargeEnergyUsed { get; set; } + public decimal? Cost { get; set; } + + public Address? Address { get; set; } + public Car Car { get; set; } = null!; + public Geofence? Geofence { get; set; } + public Position Position { get; set; } = null!; + public ICollection Charges { get; set; } + } +} diff --git a/SmartTeslaAmpSetter.Model/Entities/Drive.cs b/SmartTeslaAmpSetter.Model/Entities/Drive.cs new file mode 100644 index 000000000..d8140f516 --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Entities/Drive.cs @@ -0,0 +1,43 @@ +namespace SmartTeslaAmpSetter.Model.Entities +{ + public class Drive + { + public Drive() + { + Positions = new HashSet(); + } + + public int Id { get; set; } + public DateTime StartDate { get; set; } + public DateTime? EndDate { get; set; } + public decimal? OutsideTempAvg { get; set; } + public short? SpeedMax { get; set; } + public short? PowerMax { get; set; } + public short? PowerMin { get; set; } + public decimal? StartIdealRangeKm { get; set; } + public decimal? EndIdealRangeKm { get; set; } + public double? StartKm { get; set; } + public double? EndKm { get; set; } + public double? Distance { get; set; } + public short? DurationMin { get; set; } + public short CarId { get; set; } + public decimal? InsideTempAvg { get; set; } + public int? StartAddressId { get; set; } + public int? EndAddressId { get; set; } + public decimal? StartRatedRangeKm { get; set; } + public decimal? EndRatedRangeKm { get; set; } + public int? StartPositionId { get; set; } + public int? EndPositionId { get; set; } + public int? StartGeofenceId { get; set; } + public int? EndGeofenceId { get; set; } + + public Car Car { get; set; } = null!; + public Address? EndAddress { get; set; } + public Geofence? EndGeofence { get; set; } + public Position? EndPosition { get; set; } + public Address? StartAddress { get; set; } + public Geofence? StartGeofence { get; set; } + public Position? StartPosition { get; set; } + public ICollection Positions { get; set; } + } +} diff --git a/SmartTeslaAmpSetter.Model/Entities/Geofence.cs b/SmartTeslaAmpSetter.Model/Entities/Geofence.cs new file mode 100644 index 000000000..74671b7ba --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Entities/Geofence.cs @@ -0,0 +1,26 @@ +namespace SmartTeslaAmpSetter.Model.Entities +{ + public class Geofence + { + public Geofence() + { + ChargingProcesses = new HashSet(); + DriveEndGeofences = new HashSet(); + DriveStartGeofences = new HashSet(); + } + + public int Id { get; set; } + public string Name { get; set; } = null!; + public decimal Latitude { get; set; } + public decimal Longitude { get; set; } + public short Radius { get; set; } + public DateTime InsertedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public decimal? CostPerUnit { get; set; } + public decimal? SessionFee { get; set; } + + public ICollection ChargingProcesses { get; set; } + public ICollection DriveEndGeofences { get; set; } + public ICollection DriveStartGeofences { get; set; } + } +} diff --git a/SmartTeslaAmpSetter.Model/Entities/Position.cs b/SmartTeslaAmpSetter.Model/Entities/Position.cs new file mode 100644 index 000000000..3a067d54c --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Entities/Position.cs @@ -0,0 +1,45 @@ +namespace SmartTeslaAmpSetter.Model.Entities +{ + public class Position + { + public Position() + { + ChargingProcesses = new HashSet(); + DriveEndPositions = new HashSet(); + DriveStartPositions = new HashSet(); + } + + public int Id { get; set; } + public DateTime Date { get; set; } + public decimal Latitude { get; set; } + public decimal Longitude { get; set; } + public short? Speed { get; set; } + public short? Power { get; set; } + public double? Odometer { get; set; } + public decimal? IdealBatteryRangeKm { get; set; } + public short? BatteryLevel { get; set; } + public decimal? OutsideTemp { get; set; } + public short? Elevation { get; set; } + public int? FanStatus { get; set; } + public decimal? DriverTempSetting { get; set; } + public decimal? PassengerTempSetting { get; set; } + public bool? IsClimateOn { get; set; } + public bool? IsRearDefrosterOn { get; set; } + public bool? IsFrontDefrosterOn { get; set; } + public short CarId { get; set; } + public int? DriveId { get; set; } + public decimal? InsideTemp { get; set; } + public bool? BatteryHeater { get; set; } + public bool? BatteryHeaterOn { get; set; } + public bool? BatteryHeaterNoPower { get; set; } + public decimal? EstBatteryRangeKm { get; set; } + public decimal? RatedBatteryRangeKm { get; set; } + public short? UsableBatteryLevel { get; set; } + + public Car Car { get; set; } = null!; + public Drive? Drive { get; set; } + public ICollection ChargingProcesses { get; set; } + public ICollection DriveEndPositions { get; set; } + public ICollection DriveStartPositions { get; set; } + } +} diff --git a/SmartTeslaAmpSetter.Model/Entities/SchemaMigration.cs b/SmartTeslaAmpSetter.Model/Entities/SchemaMigration.cs new file mode 100644 index 000000000..b713c8ff3 --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Entities/SchemaMigration.cs @@ -0,0 +1,8 @@ +namespace SmartTeslaAmpSetter.Model.Entities +{ + public class SchemaMigration + { + public long Version { get; set; } + public DateTime? InsertedAt { get; set; } + } +} diff --git a/SmartTeslaAmpSetter.Model/Entities/Setting.cs b/SmartTeslaAmpSetter.Model/Entities/Setting.cs new file mode 100644 index 000000000..93249311b --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Entities/Setting.cs @@ -0,0 +1,12 @@ +namespace SmartTeslaAmpSetter.Model.Entities +{ + public class Setting + { + public long Id { get; set; } + public DateTime InsertedAt { get; set; } + public DateTime UpdatedAt { get; set; } + public string? BaseUrl { get; set; } + public string? GrafanaUrl { get; set; } + public string Language { get; set; } = null!; + } +} diff --git a/SmartTeslaAmpSetter.Model/Entities/State.cs b/SmartTeslaAmpSetter.Model/Entities/State.cs new file mode 100644 index 000000000..d64f48b6e --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Entities/State.cs @@ -0,0 +1,12 @@ +namespace SmartTeslaAmpSetter.Model.Entities +{ + public class State + { + public int Id { get; set; } + public DateTime StartDate { get; set; } + public DateTime? EndDate { get; set; } + public short CarId { get; set; } + + public virtual Car Car { get; set; } = null!; + } +} diff --git a/SmartTeslaAmpSetter.Model/Entities/Token.cs b/SmartTeslaAmpSetter.Model/Entities/Token.cs new file mode 100644 index 000000000..6bc394571 --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Entities/Token.cs @@ -0,0 +1,11 @@ +namespace SmartTeslaAmpSetter.Model.Entities +{ + public class Token + { + public int Id { get; set; } + public string Access { get; set; } = null!; + public string Refresh { get; set; } = null!; + public DateTime InsertedAt { get; set; } + public DateTime UpdatedAt { get; set; } + } +} diff --git a/SmartTeslaAmpSetter.Model/Entities/Update.cs b/SmartTeslaAmpSetter.Model/Entities/Update.cs new file mode 100644 index 000000000..b04408831 --- /dev/null +++ b/SmartTeslaAmpSetter.Model/Entities/Update.cs @@ -0,0 +1,13 @@ +namespace SmartTeslaAmpSetter.Model.Entities +{ + public class Update + { + public int Id { get; set; } + public DateTime StartDate { get; set; } + public DateTime? EndDate { get; set; } + public string? Version { get; set; } + public short CarId { get; set; } + + public virtual Car Car { get; set; } = null!; + } +} diff --git a/SmartTeslaAmpSetter.Model/EntityFramework/DbConnectionStringHelper.cs b/SmartTeslaAmpSetter.Model/EntityFramework/DbConnectionStringHelper.cs new file mode 100644 index 000000000..338e603da --- /dev/null +++ b/SmartTeslaAmpSetter.Model/EntityFramework/DbConnectionStringHelper.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; +using SmartTeslaAmpSetter.Model.Contracts; +using SmartTeslaAmpSetter.Shared.Contracts; + +namespace SmartTeslaAmpSetter.Model.EntityFramework; + +public class DbConnectionStringHelper : IDbConnectionStringHelper +{ + private readonly ILogger _logger; + private readonly IConfigurationWrapper _configurationWrapper; + + public DbConnectionStringHelper(ILogger logger, IConfigurationWrapper configurationWrapper) + { + _logger = logger; + _configurationWrapper = configurationWrapper; + } + + public string GetConnectionString() + { + _logger.LogTrace("{method}()", nameof(GetConnectionString)); + var server = _configurationWrapper.TeslaMateDbServer(); + var port = _configurationWrapper.TeslaMateDbPort(); + var databaseName = _configurationWrapper.TeslaMateDbDatabaseName(); + var username = _configurationWrapper.TeslaMateDbUser(); + var password = _configurationWrapper.TeslaMateDbPassword(); + var connectionString = $"Host={server};Port={port};Database={databaseName};Username={username};Password={password}"; + _logger.LogTrace("ConnectionString: {connectionString}", connectionString); + return connectionString; + } +} \ No newline at end of file diff --git a/SmartTeslaAmpSetter.Model/EntityFramework/TeslamateContext.cs b/SmartTeslaAmpSetter.Model/EntityFramework/TeslamateContext.cs new file mode 100644 index 000000000..91bd0fc85 --- /dev/null +++ b/SmartTeslaAmpSetter.Model/EntityFramework/TeslamateContext.cs @@ -0,0 +1,748 @@ +using Microsoft.EntityFrameworkCore; +using SmartTeslaAmpSetter.Model.Contracts; +using SmartTeslaAmpSetter.Model.Entities; + +namespace SmartTeslaAmpSetter.Model.EntityFramework +{ + public class TeslamateContext : DbContext, ITeslamateContext + { + public TeslamateContext() + { + } + + public TeslamateContext(DbContextOptions options) + : base(options) + { + } + + public virtual DbSet
Addresses { get; set; } = null!; + public virtual DbSet Cars { get; set; } = null!; + public virtual DbSet CarSettings { get; set; } = null!; + public virtual DbSet Charges { get; set; } = null!; + public virtual DbSet ChargingProcesses { get; set; } = null!; + public virtual DbSet Drives { get; set; } = null!; + public virtual DbSet Geofences { get; set; } = null!; + public virtual DbSet Positions { get; set; } = null!; + public virtual DbSet SchemaMigrations { get; set; } = null!; + public virtual DbSet Settings { get; set; } = null!; + public virtual DbSet States { get; set; } = null!; + public virtual DbSet Tokens { get; set; } = null!; + public virtual DbSet Updates { get; set; } = null!; + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + if (!optionsBuilder.IsConfigured) + { + //optionsBuilder.UseNpgsql("Host=192.168.1.50;Port=5433;Database=teslamate;Username=teslamate;Password=secret"); + } + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.HasPostgresEnum("billing_type", new[] { "per_kwh", "per_minute" }) + .HasPostgresEnum("range", new[] { "ideal", "rated" }) + .HasPostgresEnum("states_status", new[] { "online", "offline", "asleep" }) + .HasPostgresEnum("unit_of_length", new[] { "km", "mi" }) + .HasPostgresEnum("unit_of_temperature", new[] { "C", "F" }) + .HasPostgresExtension("cube") + .HasPostgresExtension("earthdistance"); + + modelBuilder.Entity
(entity => + { + entity.ToTable("addresses"); + + entity.HasIndex(e => new { e.OsmId, e.OsmType }, "addresses_osm_id_osm_type_index") + .IsUnique(); + + entity.Property(e => e.Id).HasColumnName("id"); + + entity.Property(e => e.City) + .HasMaxLength(255) + .HasColumnName("city"); + + entity.Property(e => e.Country) + .HasMaxLength(255) + .HasColumnName("country"); + + entity.Property(e => e.County) + .HasMaxLength(255) + .HasColumnName("county"); + + entity.Property(e => e.DisplayName) + .HasMaxLength(512) + .HasColumnName("display_name"); + + entity.Property(e => e.HouseNumber) + .HasMaxLength(255) + .HasColumnName("house_number"); + + entity.Property(e => e.InsertedAt) + .HasColumnType("timestamp(0) without time zone") + .HasColumnName("inserted_at"); + + entity.Property(e => e.Latitude) + .HasPrecision(8, 6) + .HasColumnName("latitude"); + + entity.Property(e => e.Longitude) + .HasPrecision(9, 6) + .HasColumnName("longitude"); + + entity.Property(e => e.Name) + .HasMaxLength(255) + .HasColumnName("name"); + + entity.Property(e => e.Neighbourhood) + .HasMaxLength(255) + .HasColumnName("neighbourhood"); + + entity.Property(e => e.OsmId).HasColumnName("osm_id"); + + entity.Property(e => e.OsmType).HasColumnName("osm_type"); + + entity.Property(e => e.Postcode) + .HasMaxLength(255) + .HasColumnName("postcode"); + + entity.Property(e => e.Raw) + .HasColumnType("jsonb") + .HasColumnName("raw"); + + entity.Property(e => e.Road) + .HasMaxLength(255) + .HasColumnName("road"); + + entity.Property(e => e.State) + .HasMaxLength(255) + .HasColumnName("state"); + + entity.Property(e => e.StateDistrict) + .HasMaxLength(255) + .HasColumnName("state_district"); + + entity.Property(e => e.UpdatedAt) + .HasColumnType("timestamp(0) without time zone") + .HasColumnName("updated_at"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("cars"); + + entity.HasIndex(e => e.Eid, "cars_eid_index") + .IsUnique(); + + entity.HasIndex(e => e.SettingsId, "cars_settings_id_index") + .IsUnique(); + + entity.HasIndex(e => e.Vid, "cars_vid_index") + .IsUnique(); + + entity.HasIndex(e => e.Vin, "cars_vin_index") + .IsUnique(); + + entity.Property(e => e.Id).HasColumnName("id"); + + entity.Property(e => e.DisplayPriority) + .HasColumnName("display_priority") + .HasDefaultValueSql("1"); + + entity.Property(e => e.Efficiency).HasColumnName("efficiency"); + + entity.Property(e => e.Eid).HasColumnName("eid"); + + entity.Property(e => e.ExteriorColor).HasColumnName("exterior_color"); + + entity.Property(e => e.InsertedAt) + .HasColumnType("timestamp(0) without time zone") + .HasColumnName("inserted_at"); + + entity.Property(e => e.Model) + .HasMaxLength(255) + .HasColumnName("model"); + + entity.Property(e => e.Name).HasColumnName("name"); + + entity.Property(e => e.SettingsId).HasColumnName("settings_id"); + + entity.Property(e => e.SpoilerType).HasColumnName("spoiler_type"); + + entity.Property(e => e.TrimBadging).HasColumnName("trim_badging"); + + entity.Property(e => e.UpdatedAt) + .HasColumnType("timestamp(0) without time zone") + .HasColumnName("updated_at"); + + entity.Property(e => e.Vid).HasColumnName("vid"); + + entity.Property(e => e.Vin).HasColumnName("vin"); + + entity.Property(e => e.WheelType).HasColumnName("wheel_type"); + + entity.HasOne(d => d.Settings) + .WithOne(p => p.Car) + .HasForeignKey(d => d.SettingsId) + .HasConstraintName("cars_settings_id_fkey"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("car_settings"); + + entity.Property(e => e.Id).HasColumnName("id"); + + entity.Property(e => e.FreeSupercharging).HasColumnName("free_supercharging"); + + entity.Property(e => e.ReqNotUnlocked).HasColumnName("req_not_unlocked"); + + entity.Property(e => e.SuspendAfterIdleMin) + .HasColumnName("suspend_after_idle_min") + .HasDefaultValueSql("15"); + + entity.Property(e => e.SuspendMin) + .HasColumnName("suspend_min") + .HasDefaultValueSql("21"); + + entity.Property(e => e.UseStreamingApi) + .IsRequired() + .HasColumnName("use_streaming_api") + .HasDefaultValueSql("true"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("charges"); + + entity.HasIndex(e => e.ChargingProcessId, "charges_charging_process_id_index"); + + entity.HasIndex(e => e.Date, "charges_date_index"); + + entity.Property(e => e.Id).HasColumnName("id"); + + entity.Property(e => e.BatteryHeater).HasColumnName("battery_heater"); + + entity.Property(e => e.BatteryHeaterNoPower).HasColumnName("battery_heater_no_power"); + + entity.Property(e => e.BatteryHeaterOn).HasColumnName("battery_heater_on"); + + entity.Property(e => e.BatteryLevel).HasColumnName("battery_level"); + + entity.Property(e => e.ChargeEnergyAdded) + .HasPrecision(8, 2) + .HasColumnName("charge_energy_added"); + + entity.Property(e => e.ChargerActualCurrent).HasColumnName("charger_actual_current"); + + entity.Property(e => e.ChargerPhases).HasColumnName("charger_phases"); + + entity.Property(e => e.ChargerPilotCurrent).HasColumnName("charger_pilot_current"); + + entity.Property(e => e.ChargerPower).HasColumnName("charger_power"); + + entity.Property(e => e.ChargerVoltage).HasColumnName("charger_voltage"); + + entity.Property(e => e.ChargingProcessId).HasColumnName("charging_process_id"); + + entity.Property(e => e.ConnChargeCable) + .HasMaxLength(255) + .HasColumnName("conn_charge_cable"); + + entity.Property(e => e.Date) + .HasColumnType("timestamp without time zone") + .HasColumnName("date"); + + entity.Property(e => e.FastChargerBrand) + .HasMaxLength(255) + .HasColumnName("fast_charger_brand"); + + entity.Property(e => e.FastChargerPresent).HasColumnName("fast_charger_present"); + + entity.Property(e => e.FastChargerType) + .HasMaxLength(255) + .HasColumnName("fast_charger_type"); + + entity.Property(e => e.IdealBatteryRangeKm) + .HasPrecision(6, 2) + .HasColumnName("ideal_battery_range_km"); + + entity.Property(e => e.NotEnoughPowerToHeat).HasColumnName("not_enough_power_to_heat"); + + entity.Property(e => e.OutsideTemp) + .HasPrecision(4, 1) + .HasColumnName("outside_temp"); + + entity.Property(e => e.RatedBatteryRangeKm) + .HasPrecision(6, 2) + .HasColumnName("rated_battery_range_km"); + + entity.Property(e => e.UsableBatteryLevel).HasColumnName("usable_battery_level"); + + entity.HasOne(d => d.ChargingProcess) + .WithMany(p => p.Charges) + .HasForeignKey(d => d.ChargingProcessId) + .HasConstraintName("charges_charging_process_id_fkey"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("charging_processes"); + + entity.HasIndex(e => e.AddressId, "charging_processes_address_id_index"); + + entity.HasIndex(e => e.CarId, "charging_processes_car_id_index"); + + entity.HasIndex(e => e.PositionId, "charging_processes_position_id_index"); + + entity.Property(e => e.Id).HasColumnName("id"); + + entity.Property(e => e.AddressId).HasColumnName("address_id"); + + entity.Property(e => e.CarId).HasColumnName("car_id"); + + entity.Property(e => e.ChargeEnergyAdded) + .HasPrecision(8, 2) + .HasColumnName("charge_energy_added"); + + entity.Property(e => e.ChargeEnergyUsed) + .HasPrecision(8, 2) + .HasColumnName("charge_energy_used"); + + entity.Property(e => e.Cost) + .HasPrecision(6, 2) + .HasColumnName("cost"); + + entity.Property(e => e.DurationMin).HasColumnName("duration_min"); + + entity.Property(e => e.EndBatteryLevel).HasColumnName("end_battery_level"); + + entity.Property(e => e.EndDate) + .HasColumnType("timestamp without time zone") + .HasColumnName("end_date"); + + entity.Property(e => e.EndIdealRangeKm) + .HasPrecision(6, 2) + .HasColumnName("end_ideal_range_km"); + + entity.Property(e => e.EndRatedRangeKm) + .HasPrecision(6, 2) + .HasColumnName("end_rated_range_km"); + + entity.Property(e => e.GeofenceId).HasColumnName("geofence_id"); + + entity.Property(e => e.OutsideTempAvg) + .HasPrecision(4, 1) + .HasColumnName("outside_temp_avg"); + + entity.Property(e => e.PositionId).HasColumnName("position_id"); + + entity.Property(e => e.StartBatteryLevel).HasColumnName("start_battery_level"); + + entity.Property(e => e.StartDate) + .HasColumnType("timestamp without time zone") + .HasColumnName("start_date"); + + entity.Property(e => e.StartIdealRangeKm) + .HasPrecision(6, 2) + .HasColumnName("start_ideal_range_km"); + + entity.Property(e => e.StartRatedRangeKm) + .HasPrecision(6, 2) + .HasColumnName("start_rated_range_km"); + + entity.HasOne(d => d.Address) + .WithMany(p => p.ChargingProcesses) + .HasForeignKey(d => d.AddressId) + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("charging_processes_address_id_fkey"); + + entity.HasOne(d => d.Car) + .WithMany(p => p.ChargingProcesses) + .HasForeignKey(d => d.CarId) + .HasConstraintName("charging_processes_car_id_fkey"); + + entity.HasOne(d => d.Geofence) + .WithMany(p => p.ChargingProcesses) + .HasForeignKey(d => d.GeofenceId) + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("charging_processes_geofence_id_fkey"); + + entity.HasOne(d => d.Position) + .WithMany(p => p.ChargingProcesses) + .HasForeignKey(d => d.PositionId) + .OnDelete(DeleteBehavior.ClientSetNull) + .HasConstraintName("charging_processes_position_id_fkey"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("drives"); + + entity.HasIndex(e => e.EndGeofenceId, "drives_end_geofence_id_index"); + + entity.HasIndex(e => e.EndPositionId, "drives_end_position_id_index"); + + entity.HasIndex(e => e.StartGeofenceId, "drives_start_geofence_id_index"); + + entity.HasIndex(e => e.StartPositionId, "drives_start_position_id_index"); + + entity.HasIndex(e => e.CarId, "trips_car_id_index"); + + entity.HasIndex(e => e.EndAddressId, "trips_end_address_id_index"); + + entity.HasIndex(e => e.StartAddressId, "trips_start_address_id_index"); + + entity.Property(e => e.Id) + .HasColumnName("id") + .HasDefaultValueSql("nextval('trips_id_seq'::regclass)"); + + entity.Property(e => e.CarId).HasColumnName("car_id"); + + entity.Property(e => e.Distance).HasColumnName("distance"); + + entity.Property(e => e.DurationMin).HasColumnName("duration_min"); + + entity.Property(e => e.EndAddressId).HasColumnName("end_address_id"); + + entity.Property(e => e.EndDate) + .HasColumnType("timestamp without time zone") + .HasColumnName("end_date"); + + entity.Property(e => e.EndGeofenceId).HasColumnName("end_geofence_id"); + + entity.Property(e => e.EndIdealRangeKm) + .HasPrecision(6, 2) + .HasColumnName("end_ideal_range_km"); + + entity.Property(e => e.EndKm).HasColumnName("end_km"); + + entity.Property(e => e.EndPositionId).HasColumnName("end_position_id"); + + entity.Property(e => e.EndRatedRangeKm) + .HasPrecision(6, 2) + .HasColumnName("end_rated_range_km"); + + entity.Property(e => e.InsideTempAvg) + .HasPrecision(4, 1) + .HasColumnName("inside_temp_avg"); + + entity.Property(e => e.OutsideTempAvg) + .HasPrecision(4, 1) + .HasColumnName("outside_temp_avg"); + + entity.Property(e => e.PowerMax).HasColumnName("power_max"); + + entity.Property(e => e.PowerMin).HasColumnName("power_min"); + + entity.Property(e => e.SpeedMax).HasColumnName("speed_max"); + + entity.Property(e => e.StartAddressId).HasColumnName("start_address_id"); + + entity.Property(e => e.StartDate) + .HasColumnType("timestamp without time zone") + .HasColumnName("start_date"); + + entity.Property(e => e.StartGeofenceId).HasColumnName("start_geofence_id"); + + entity.Property(e => e.StartIdealRangeKm) + .HasPrecision(6, 2) + .HasColumnName("start_ideal_range_km"); + + entity.Property(e => e.StartKm).HasColumnName("start_km"); + + entity.Property(e => e.StartPositionId).HasColumnName("start_position_id"); + + entity.Property(e => e.StartRatedRangeKm) + .HasPrecision(6, 2) + .HasColumnName("start_rated_range_km"); + + entity.HasOne(d => d.Car) + .WithMany(p => p.Drives) + .HasForeignKey(d => d.CarId) + .HasConstraintName("drives_car_id_fkey"); + + entity.HasOne(d => d.EndAddress) + .WithMany(p => p.DriveEndAddresses) + .HasForeignKey(d => d.EndAddressId) + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("drives_end_address_id_fkey"); + + entity.HasOne(d => d.EndGeofence) + .WithMany(p => p.DriveEndGeofences) + .HasForeignKey(d => d.EndGeofenceId) + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("drives_end_geofence_id_fkey"); + + entity.HasOne(d => d.EndPosition) + .WithMany(p => p.DriveEndPositions) + .HasForeignKey(d => d.EndPositionId) + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("drives_end_position_id_fkey"); + + entity.HasOne(d => d.StartAddress) + .WithMany(p => p.DriveStartAddresses) + .HasForeignKey(d => d.StartAddressId) + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("drives_start_address_id_fkey"); + + entity.HasOne(d => d.StartGeofence) + .WithMany(p => p.DriveStartGeofences) + .HasForeignKey(d => d.StartGeofenceId) + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("drives_start_geofence_id_fkey"); + + entity.HasOne(d => d.StartPosition) + .WithMany(p => p.DriveStartPositions) + .HasForeignKey(d => d.StartPositionId) + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("drives_start_position_id_fkey"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("geofences"); + + entity.Property(e => e.Id).HasColumnName("id"); + + entity.Property(e => e.CostPerUnit) + .HasPrecision(6, 4) + .HasColumnName("cost_per_unit"); + + entity.Property(e => e.InsertedAt) + .HasColumnType("timestamp(0) without time zone") + .HasColumnName("inserted_at"); + + entity.Property(e => e.Latitude) + .HasPrecision(8, 6) + .HasColumnName("latitude"); + + entity.Property(e => e.Longitude) + .HasPrecision(9, 6) + .HasColumnName("longitude"); + + entity.Property(e => e.Name) + .HasMaxLength(255) + .HasColumnName("name"); + + entity.Property(e => e.Radius) + .HasColumnName("radius") + .HasDefaultValueSql("25"); + + entity.Property(e => e.SessionFee) + .HasPrecision(6, 2) + .HasColumnName("session_fee"); + + entity.Property(e => e.UpdatedAt) + .HasColumnType("timestamp(0) without time zone") + .HasColumnName("updated_at"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("positions"); + + entity.HasIndex(e => e.CarId, "positions_car_id_index"); + + entity.HasIndex(e => e.Date, "positions_date_index"); + + entity.HasIndex(e => e.DriveId, "positions_drive_id_index"); + + entity.Property(e => e.Id).HasColumnName("id"); + + entity.Property(e => e.BatteryHeater).HasColumnName("battery_heater"); + + entity.Property(e => e.BatteryHeaterNoPower).HasColumnName("battery_heater_no_power"); + + entity.Property(e => e.BatteryHeaterOn).HasColumnName("battery_heater_on"); + + entity.Property(e => e.BatteryLevel).HasColumnName("battery_level"); + + entity.Property(e => e.CarId).HasColumnName("car_id"); + + entity.Property(e => e.Date) + .HasColumnType("timestamp without time zone") + .HasColumnName("date"); + + entity.Property(e => e.DriveId).HasColumnName("drive_id"); + + entity.Property(e => e.DriverTempSetting) + .HasPrecision(4, 1) + .HasColumnName("driver_temp_setting"); + + entity.Property(e => e.Elevation).HasColumnName("elevation"); + + entity.Property(e => e.EstBatteryRangeKm) + .HasPrecision(6, 2) + .HasColumnName("est_battery_range_km"); + + entity.Property(e => e.FanStatus).HasColumnName("fan_status"); + + entity.Property(e => e.IdealBatteryRangeKm) + .HasPrecision(6, 2) + .HasColumnName("ideal_battery_range_km"); + + entity.Property(e => e.InsideTemp) + .HasPrecision(4, 1) + .HasColumnName("inside_temp"); + + entity.Property(e => e.IsClimateOn).HasColumnName("is_climate_on"); + + entity.Property(e => e.IsFrontDefrosterOn).HasColumnName("is_front_defroster_on"); + + entity.Property(e => e.IsRearDefrosterOn).HasColumnName("is_rear_defroster_on"); + + entity.Property(e => e.Latitude) + .HasPrecision(8, 6) + .HasColumnName("latitude"); + + entity.Property(e => e.Longitude) + .HasPrecision(9, 6) + .HasColumnName("longitude"); + + entity.Property(e => e.Odometer).HasColumnName("odometer"); + + entity.Property(e => e.OutsideTemp) + .HasPrecision(4, 1) + .HasColumnName("outside_temp"); + + entity.Property(e => e.PassengerTempSetting) + .HasPrecision(4, 1) + .HasColumnName("passenger_temp_setting"); + + entity.Property(e => e.Power).HasColumnName("power"); + + entity.Property(e => e.RatedBatteryRangeKm) + .HasPrecision(6, 2) + .HasColumnName("rated_battery_range_km"); + + entity.Property(e => e.Speed).HasColumnName("speed"); + + entity.Property(e => e.UsableBatteryLevel).HasColumnName("usable_battery_level"); + + entity.HasOne(d => d.Car) + .WithMany(p => p.Positions) + .HasForeignKey(d => d.CarId) + .HasConstraintName("positions_car_id_fkey"); + + entity.HasOne(d => d.Drive) + .WithMany(p => p.Positions) + .HasForeignKey(d => d.DriveId) + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("positions_drive_id_fkey"); + }); + + modelBuilder.Entity(entity => + { + entity.HasKey(e => e.Version) + .HasName("schema_migrations_pkey"); + + entity.ToTable("schema_migrations"); + + entity.Property(e => e.Version) + .ValueGeneratedNever() + .HasColumnName("version"); + + entity.Property(e => e.InsertedAt) + .HasColumnType("timestamp(0) without time zone") + .HasColumnName("inserted_at"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("settings"); + + entity.Property(e => e.Id).HasColumnName("id"); + + entity.Property(e => e.BaseUrl) + .HasMaxLength(255) + .HasColumnName("base_url"); + + entity.Property(e => e.GrafanaUrl) + .HasMaxLength(255) + .HasColumnName("grafana_url"); + + entity.Property(e => e.InsertedAt) + .HasColumnType("timestamp(0) without time zone") + .HasColumnName("inserted_at"); + + entity.Property(e => e.Language) + .HasColumnName("language") + .HasDefaultValueSql("'en'::text"); + + entity.Property(e => e.UpdatedAt) + .HasColumnType("timestamp(0) without time zone") + .HasColumnName("updated_at"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("states"); + + entity.HasIndex(e => e.CarId, "states_car_id_index"); + + entity.Property(e => e.Id).HasColumnName("id"); + + entity.Property(e => e.CarId).HasColumnName("car_id"); + + entity.Property(e => e.EndDate) + .HasColumnType("timestamp without time zone") + .HasColumnName("end_date"); + + entity.Property(e => e.StartDate) + .HasColumnType("timestamp without time zone") + .HasColumnName("start_date"); + + entity.HasOne(d => d.Car) + .WithMany(p => p.States) + .HasForeignKey(d => d.CarId) + .HasConstraintName("states_car_id_fkey"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("tokens"); + + entity.Property(e => e.Id).HasColumnName("id"); + + entity.Property(e => e.Access).HasColumnName("access"); + + entity.Property(e => e.InsertedAt) + .HasColumnType("timestamp(0) without time zone") + .HasColumnName("inserted_at"); + + entity.Property(e => e.Refresh).HasColumnName("refresh"); + + entity.Property(e => e.UpdatedAt) + .HasColumnType("timestamp(0) without time zone") + .HasColumnName("updated_at"); + }); + + modelBuilder.Entity(entity => + { + entity.ToTable("updates"); + + entity.HasIndex(e => e.CarId, "updates_car_id_index"); + + entity.Property(e => e.Id).HasColumnName("id"); + + entity.Property(e => e.CarId).HasColumnName("car_id"); + + entity.Property(e => e.EndDate) + .HasColumnType("timestamp without time zone") + .HasColumnName("end_date"); + + entity.Property(e => e.StartDate) + .HasColumnType("timestamp without time zone") + .HasColumnName("start_date"); + + entity.Property(e => e.Version) + .HasMaxLength(255) + .HasColumnName("version"); + + entity.HasOne(d => d.Car) + .WithMany(p => p.Updates) + .HasForeignKey(d => d.CarId) + .HasConstraintName("updates_car_id_fkey"); + }); + } + } +} diff --git a/SmartTeslaAmpSetter.Model/SmartTeslaAmpSetter.Model.csproj b/SmartTeslaAmpSetter.Model/SmartTeslaAmpSetter.Model.csproj new file mode 100644 index 000000000..12fefbaed --- /dev/null +++ b/SmartTeslaAmpSetter.Model/SmartTeslaAmpSetter.Model.csproj @@ -0,0 +1,21 @@ + + + + net6.0 + enable + enable + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + diff --git a/SmartTeslaAmpSetter.Tests/Services/GridService.cs b/SmartTeslaAmpSetter.Tests/Services/GridService.cs deleted file mode 100644 index f019e85a7..000000000 --- a/SmartTeslaAmpSetter.Tests/Services/GridService.cs +++ /dev/null @@ -1,26 +0,0 @@ -using Xunit; -using Xunit.Abstractions; - -namespace SmartTeslaAmpSetter.Tests.Services; - -public class GridService : TestBase -{ - public GridService(ITestOutputHelper outputHelper) - : base(outputHelper) - { - } - - [Theory] - [InlineData("384.8746")] - [InlineData("384")] - [InlineData("384.0")] - [InlineData("384.147")] - public void Can_extract_Integers_From_String(string value) - { - var gridService = Mock.Create(); - var intValue = gridService.GetIntegerFromString(value); - - Assert.Equal(384, intValue); - } - -} \ No newline at end of file diff --git a/SmartTeslaAmpSetter.Tests/Services/ChargeTimeUpdateService.cs b/SmartTeslaAmpSetter.Tests/Services/Server/ChargeTimeUpdateService.cs similarity index 87% rename from SmartTeslaAmpSetter.Tests/Services/ChargeTimeUpdateService.cs rename to SmartTeslaAmpSetter.Tests/Services/Server/ChargeTimeUpdateService.cs index 36059bc89..97e1e215d 100644 --- a/SmartTeslaAmpSetter.Tests/Services/ChargeTimeUpdateService.cs +++ b/SmartTeslaAmpSetter.Tests/Services/Server/ChargeTimeUpdateService.cs @@ -1,10 +1,11 @@ using System; +using SmartTeslaAmpSetter.Shared.Contracts; using SmartTeslaAmpSetter.Shared.Dtos.Settings; using SmartTeslaAmpSetter.Shared.TimeProviding; using Xunit; using Xunit.Abstractions; -namespace SmartTeslaAmpSetter.Tests.Services; +namespace SmartTeslaAmpSetter.Tests.Services.Server; public class ChargeTimeUpdateService : TestBase { @@ -38,7 +39,7 @@ public void Calculates_Correct_Charge_MaxSpeed_Charge_Time(int numberOfPhases) var dateTime = new DateTime(2022, 4, 1, 14, 0, 0); Mock.Mock().Setup(d => d.Now()).Returns(dateTime); - var chargingService = Mock.Create(); + var chargingService = Mock.Create(); chargingService.UpdateChargeTime(car); @@ -72,7 +73,7 @@ public void Handles_Plugged_Out_Car() var dateTime = new DateTime(2022, 4, 1, 14, 0, 0); Mock.Mock().Setup(d => d.Now()).Returns(dateTime); - var chargingService = Mock.Create(); + var chargingService = Mock.Create(); chargingService.UpdateChargeTime(car); @@ -102,7 +103,7 @@ public void Handles_Reaced_Minimum_Soc() var dateTime = new DateTime(2022, 4, 1, 14, 0, 0); Mock.Mock().Setup(d => d.Now()).Returns(dateTime); - var chargingService = Mock.Create(); + var chargingService = Mock.Create(); chargingService.UpdateChargeTime(car); diff --git a/SmartTeslaAmpSetter.Tests/Services/ChargingService.cs b/SmartTeslaAmpSetter.Tests/Services/Server/ChargingService.cs similarity index 94% rename from SmartTeslaAmpSetter.Tests/Services/ChargingService.cs rename to SmartTeslaAmpSetter.Tests/Services/Server/ChargingService.cs index 66eb1d306..ab6e677f8 100644 --- a/SmartTeslaAmpSetter.Tests/Services/ChargingService.cs +++ b/SmartTeslaAmpSetter.Tests/Services/Server/ChargingService.cs @@ -10,7 +10,7 @@ using Xunit.Abstractions; using CarState = SmartTeslaAmpSetter.Shared.Dtos.Settings.CarState; -namespace SmartTeslaAmpSetter.Tests.Services; +namespace SmartTeslaAmpSetter.Tests.Services.Server; public class ChargingService : TestBase { @@ -30,7 +30,7 @@ public ChargingService(ITestOutputHelper outputHelper) [InlineData(ChargeMode.PvOnly, 2, -10, true)] public void Does_autoenable_fullspeed_charge_if_needed(ChargeMode chargeMode, int fullSpeedChargeMinutesAfterLatestTime, int moreSocThanMinSoc, bool autofullSpeedCharge) { - var chargingService = Mock.Create(); + var chargingService = Mock.Create(); var currentTimeProvider = Mock.Create( new NamedParameter("dateTime", new DateTime(2022, 4, 1, 14, 0, 0))); var currentTime = currentTimeProvider.Now(); @@ -82,7 +82,7 @@ public void Does_autoenable_fullspeed_charge_if_needed(ChargeMode chargeMode, in [InlineData(true)] public void Enable_Full_Speed_Charge_Can_Handle_Null_Values(bool autoFullSpeedCharge) { - var chargingService = Mock.Create(); + var chargingService = Mock.Create(); var currentTimeProvider = Mock.Create( new NamedParameter("dateTime", new DateTime(2022, 4, 1, 14, 0, 0))); var currentTime = currentTimeProvider.Now(); @@ -105,7 +105,7 @@ public void Enable_Full_Speed_Charge_Can_Handle_Null_Values(bool autoFullSpeedCh [InlineData(true)] public void Disable_Full_Speed_Charge_Can_Handle_Null_Values(bool autoFullSpeedCharge) { - var chargingService = Mock.Create(); + var chargingService = Mock.Create(); var currentTimeProvider = Mock.Create( new NamedParameter("dateTime", new DateTime(2022, 4, 1, 14, 0, 0))); var currentTime = currentTimeProvider.Now(); @@ -182,7 +182,7 @@ public void Gets_relevant_car_IDs() }, }; Mock.Mock().Setup(s => s.Cars).Returns(cars); - var chargingService = Mock.Create(); + var chargingService = Mock.Create(); var relevantIds = chargingService.GetRelevantCarIds(geofence); @@ -249,7 +249,7 @@ public void Gets_irrelevant_cars() }, }; Mock.Mock().Setup(s => s.Cars).Returns(cars); - var chargingService = Mock.Create(); + var chargingService = Mock.Create(); var irrelevantCars = chargingService.GetIrrelevantCars(chargingService.GetRelevantCarIds(geofence)); diff --git a/SmartTeslaAmpSetter.Tests/Services/ConfigJsonService.cs b/SmartTeslaAmpSetter.Tests/Services/Server/ConfigJsonService.cs similarity index 86% rename from SmartTeslaAmpSetter.Tests/Services/ConfigJsonService.cs rename to SmartTeslaAmpSetter.Tests/Services/Server/ConfigJsonService.cs index 3ebf7a7c0..70c5be0a0 100644 --- a/SmartTeslaAmpSetter.Tests/Services/ConfigJsonService.cs +++ b/SmartTeslaAmpSetter.Tests/Services/Server/ConfigJsonService.cs @@ -6,7 +6,7 @@ using Xunit; using Xunit.Abstractions; -namespace SmartTeslaAmpSetter.Tests.Services; +namespace SmartTeslaAmpSetter.Tests.Services.Server; public class ConfigJsonService : TestBase { @@ -21,7 +21,7 @@ public void Adds_every_new_car() var newCarIds = new List() { 1, 2, 3, 4 }; var cars = new List(); - var configJsonService = Mock.Create(); + var configJsonService = Mock.Create(); configJsonService.AddNewCars(newCarIds, cars); Assert.Equal(newCarIds.Count, cars.Count); @@ -33,7 +33,7 @@ public void Sets_correct_default_values_on_new_cars() var newCarIds = new List() { 1, 2, 3, 4 }; var cars = new List(); - var configJsonService = Mock.Create(); + var configJsonService = Mock.Create(); configJsonService.AddNewCars(newCarIds, cars); foreach (var car in cars) @@ -54,7 +54,7 @@ public void Removes_old_cars() var newCarIds = new List() { 1, 2, 3, 4 }; var cars = new List(); - var configJsonService = Mock.Create(); + var configJsonService = Mock.Create(); configJsonService.AddNewCars(newCarIds, cars); configJsonService.RemoveOldCars(cars, new List() { 1, 3 }); @@ -69,7 +69,7 @@ public void Removes_old_cars() [InlineData("[{\"Id\":1,\"CarConfiguration\":{\"ChargeMode\":1,\"MinimumSoC\":0,\"LatestTimeToReachSoC\":\"2022-04-11T00:00:00\",\"MaximumAmpere\":16,\"MinimumAmpere\":1,\"UsableEnergy\":75}},{\"Id\":2,\"CarConfiguration\":{\"ChargeMode\":2,\"MinimumSoC\":45,\"LatestTimeToReachSoC\":\"2022-04-11T00:00:00\",\"MaximumAmpere\":16,\"MinimumAmpere\":1,\"UsableEnergy\":75}}]")] public void Deserializes_car_configuration(string configString) { - var configJsonService = Mock.Create(); + var configJsonService = Mock.Create(); var cars = configJsonService.DeserializeCarsFromConfigurationString(configString); Assert.Equal(2, cars.Count); diff --git a/SmartTeslaAmpSetter.Tests/Services/Server/GridService.cs b/SmartTeslaAmpSetter.Tests/Services/Server/GridService.cs new file mode 100644 index 000000000..10ff29fa7 --- /dev/null +++ b/SmartTeslaAmpSetter.Tests/Services/Server/GridService.cs @@ -0,0 +1,111 @@ +using SmartTeslaAmpSetter.Server.Contracts; +using SmartTeslaAmpSetter.Server.Enums; +using SmartTeslaAmpSetter.Shared.Contracts; +using Xunit; +using Xunit.Abstractions; + +namespace SmartTeslaAmpSetter.Tests.Services.Server; + +public class GridService : TestBase +{ + public GridService(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + [Theory] + [InlineData("384.8746")] + [InlineData("384")] + [InlineData("384.0")] + [InlineData("384.147")] + public void Can_extract_Integers_From_String(string value) + { + var gridService = Mock.Create(); + var intValue = gridService.GetIntegerFromString(value); + + Assert.Equal(384, intValue); + } + + [Theory] + [InlineData("384.8746")] + [InlineData("384")] + [InlineData("384.0")] + [InlineData("384.147")] + public void Can_Get_Integer_From_Plain_Result(string text) + { + var gridService = Mock.Create(); + var intValue = gridService.GetValueFromResult("", text, NodePatternType.None, true); + + Assert.Equal(384, intValue); + } + + [Theory] + [InlineData("384.8746")] + [InlineData("384")] + [InlineData("384.0")] + [InlineData("384.147")] + public void Can_Get_Integer_From_Json_Result(string text) + { + var json = string.Format( + "{{\"request\": {{\"method\": \"get\", \"key\": \"CO@13_3_0\"}}, \"code\": 0, \"type\": \"call\", \"data\": {{\"value\": {0}}}}}", text); + var gridService = Mock.Create(); + var intValue = gridService.GetValueFromResult("$.data.value", json, NodePatternType.Json, true); + + Assert.Equal(384, intValue); + } + + [Theory] + [InlineData("384.8746")] + [InlineData("384")] + [InlineData("384.0")] + [InlineData("384.147")] + public void Can_Get_Integer_From_Grid_Xml_Attribute_Result(string text) + { + var xml = string.Format( + "\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", text); + var gridService = Mock.Create(); + Mock.Mock().Setup(s => s.CurrentPowerToGridXmlAttributeHeaderName()).Returns("Type"); + Mock.Mock().Setup(s => s.CurrentPowerToGridXmlAttributeHeaderValue()).Returns("GridPower"); + Mock.Mock().Setup(s => s.CurrentPowerToGridXmlAttributeValueName()).Returns("Value"); + + var intValue = gridService.GetValueFromResult("Device/Measurements/Measurement", xml, NodePatternType.Xml, true); + + Assert.Equal(384, intValue); + } + + [Theory] + [InlineData("384.8746")] + [InlineData("384")] + [InlineData("384.0")] + [InlineData("384.147")] + public void Can_Get_Integer_From_Inverter_Xml_Attribute_Result(string text) + { + var xml = string.Format( + "\r\n\r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n \r\n", text); + var gridService = Mock.Create(); + Mock.Mock().Setup(s => s.CurrentInverterPowerXmlAttributeHeaderName()).Returns("Type"); + Mock.Mock().Setup(s => s.CurrentInverterPowerXmlAttributeHeaderValue()).Returns("AC_Power"); + Mock.Mock().Setup(s => s.CurrentInverterPowerXmlAttributeValueName()).Returns("Value"); + + var intValue = gridService.GetValueFromResult("Device/Measurements/Measurement", xml, NodePatternType.Xml, false); + + Assert.Equal(384, intValue); + } + + [Theory] + [InlineData("384.8746")] + [InlineData("384")] + [InlineData("384.0")] + [InlineData("384.147")] + public void Can_Get_Integer_From_Xml_Node_Result(string text) + { + var xml = string.Format( + "\r\n\r\n \r\n 1000\r\n {0}\r\n \r\n", text); + var gridService = Mock.Create(); + + var intValue = gridService.GetValueFromResult("Device/Measurements/GridPower", xml, NodePatternType.Xml, false); + + Assert.Equal(384, intValue); + } + +} \ No newline at end of file diff --git a/SmartTeslaAmpSetter.Tests/Services/MqttService.cs b/SmartTeslaAmpSetter.Tests/Services/Server/MqttService.cs similarity index 92% rename from SmartTeslaAmpSetter.Tests/Services/MqttService.cs rename to SmartTeslaAmpSetter.Tests/Services/Server/MqttService.cs index 9e472c167..62c49e5b5 100644 --- a/SmartTeslaAmpSetter.Tests/Services/MqttService.cs +++ b/SmartTeslaAmpSetter.Tests/Services/Server/MqttService.cs @@ -7,7 +7,7 @@ using Xunit; using Xunit.Abstractions; -namespace SmartTeslaAmpSetter.Tests.Services; +namespace SmartTeslaAmpSetter.Tests.Services.Server; public class MqttService : TestBase { @@ -37,7 +37,7 @@ public void ReducesActualCurrentToLastSetAmpIfDifferenceIsOneAndBelow5A(string v }; Mock.Mock().Setup(s => s.Cars).Returns(cars); - var mqttService = Mock.Create(); + var mqttService = Mock.Create(); var teslamateValue = new TeslaMateValue() { diff --git a/SmartTeslaAmpSetter.Tests/Services/TelegramService.cs b/SmartTeslaAmpSetter.Tests/Services/Server/TelegramService.cs similarity index 78% rename from SmartTeslaAmpSetter.Tests/Services/TelegramService.cs rename to SmartTeslaAmpSetter.Tests/Services/Server/TelegramService.cs index 7fae923a8..111aa054e 100644 --- a/SmartTeslaAmpSetter.Tests/Services/TelegramService.cs +++ b/SmartTeslaAmpSetter.Tests/Services/Server/TelegramService.cs @@ -1,7 +1,7 @@ using Xunit; using Xunit.Abstractions; -namespace SmartTeslaAmpSetter.Tests.Services; +namespace SmartTeslaAmpSetter.Tests.Services.Server; public class TelegramService : TestBase { @@ -16,7 +16,7 @@ public void BuildsCorrectRequestUri() var botKey = "0815:2asdf"; var channelId = "5236466"; var message = "Test"; - var telegramService = Mock.Create(); + var telegramService = Mock.Create(); var uri = telegramService.CreateRequestUri(message, botKey, channelId); diff --git a/SmartTeslaAmpSetter.Tests/Services/SolarEdgePlugin/CurrentValuesService.cs b/SmartTeslaAmpSetter.Tests/Services/SolarEdgePlugin/CurrentValuesService.cs new file mode 100644 index 000000000..89d8e53ee --- /dev/null +++ b/SmartTeslaAmpSetter.Tests/Services/SolarEdgePlugin/CurrentValuesService.cs @@ -0,0 +1,22 @@ +using Xunit; +using Xunit.Abstractions; + +namespace SmartTeslaAmpSetter.Tests.Services.SolarEdgePlugin; + +public class CurrentValuesService : TestBase +{ + public CurrentValuesService(ITestOutputHelper outputHelper) + : base(outputHelper) + { + } + + [Theory] + [InlineData("{\"siteCurrentPowerFlow\":{\"updateRefreshRate\":3,\"unit\":\"kW\",\"connections\":[{\"from\":\"LOAD\",\"to\":\"Grid\"},{\"from\":\"PV\",\"to\":\"Load\"},{\"from\":\"PV\",\"to\":\"Storage\"}],\"GRID\":{\"status\":\"Active\",\"currentPower\":1.09},\"LOAD\":{\"status\":\"Active\",\"currentPower\":0.25},\"PV\":{\"status\":\"Active\",\"currentPower\":6.34},\"STORAGE\":{\"status\":\"Charging\",\"currentPower\":5.0,\"chargeLevel\":93,\"critical\":false}}}")] + public void CanDeserializeCloudApiValue(string jsonString) + { + var currentValuesService = Mock.Create(); + + var value = currentValuesService.GetCloudApiValueFromString(jsonString); + Assert.NotNull(value); + } +} \ No newline at end of file diff --git a/SmartTeslaAmpSetter.Tests/SmartTeslaAmpSetter.Tests.csproj b/SmartTeslaAmpSetter.Tests/SmartTeslaAmpSetter.Tests.csproj index a6763c692..543e992d2 100644 --- a/SmartTeslaAmpSetter.Tests/SmartTeslaAmpSetter.Tests.csproj +++ b/SmartTeslaAmpSetter.Tests/SmartTeslaAmpSetter.Tests.csproj @@ -27,8 +27,13 @@ + + + + + diff --git a/SmartTeslaAmpSetter.Tests/Wrappers/ConfigurationWrapper.cs b/SmartTeslaAmpSetter.Tests/Wrappers/ConfigurationWrapper.cs index fd1c0f247..462f66127 100644 --- a/SmartTeslaAmpSetter.Tests/Wrappers/ConfigurationWrapper.cs +++ b/SmartTeslaAmpSetter.Tests/Wrappers/ConfigurationWrapper.cs @@ -14,11 +14,11 @@ public ConfigurationWrapper(ITestOutputHelper outputHelper) [Fact] public void Get_Not_Nullable_String() { - var configurationService = Mock.Create(); + var configurationService = Mock.Create(); var existingConfigValue = "TeslaMateApiBaseUrl"; var teslaMateApiBaseUrl = - configurationService.GetNotNullableConfigurationValue(existingConfigValue); + configurationService.GetNotNullableConfigurationValue(existingConfigValue); Assert.Equal("http://192.168.1.50:8097", teslaMateApiBaseUrl); } @@ -28,9 +28,9 @@ public void Get_Not_Nullable_String() [InlineData("notExisiting")] public void Throw_Exception_On_Null_String(string notExisitingConfigValue) { - var configurationService = Mock.Create(); + var configurationService = Mock.Create(); Assert.Throws( - () => configurationService.GetNotNullableConfigurationValue(notExisitingConfigValue)); + () => configurationService.GetNotNullableConfigurationValue(notExisitingConfigValue)); } [Theory] @@ -38,8 +38,8 @@ public void Throw_Exception_On_Null_String(string notExisitingConfigValue) [InlineData("notExisiting")] public void Returns_Null_On_Non_Exisiting_Values(string notExisitingConfigValue) { - var configurationService = Mock.Create(); - var value = configurationService.GetNullableConfigurationValue(notExisitingConfigValue); + var configurationService = Mock.Create(); + var value = configurationService.GetNullableConfigurationValue(notExisitingConfigValue); Assert.Null(value); } @@ -51,7 +51,7 @@ public void Returns_Null_On_Non_Exisiting_Values(string notExisitingConfigValue) [InlineData("notExisiting")] public void Get_TimeSpan_From_Minutes(string configName) { - var configurationService = Mock.Create(); + var configurationService = Mock.Create(); var timespan = configurationService.GetMinutesConfigurationValueIfGreaterThanMinumum(configName, TimeSpan.FromMinutes(1)); @@ -81,7 +81,7 @@ public void Get_TimeSpan_From_Minutes(string configName) [InlineData("notExisiting")] public void Get_TimeSpan_From_Seconds(string configName) { - var configurationService = Mock.Create(); + var configurationService = Mock.Create(); var minimum = TimeSpan.FromSeconds(1); var timespan = configurationService.GetSecondsConfigurationValueIfGreaterThanMinumum(configName, minimum); diff --git a/SmartTeslaAmpSetter.sln b/SmartTeslaAmpSetter.sln index eef653ee3..3ca90167f 100644 --- a/SmartTeslaAmpSetter.sln +++ b/SmartTeslaAmpSetter.sln @@ -11,7 +11,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmartTeslaAmpSetter.Shared" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugins.SmaEnergymeter", "Plugins.SmaEnergymeter\Plugins.SmaEnergymeter.csproj", "{6F085517-A87C-4590-87B9-1ED0F926BD8D}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartTeslaAmpSetter.Tests", "SmartTeslaAmpSetter.Tests\SmartTeslaAmpSetter.Tests.csproj", "{98CE240C-CC51-4658-BD0B-5BEE43297B9F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SmartTeslaAmpSetter.Tests", "SmartTeslaAmpSetter.Tests\SmartTeslaAmpSetter.Tests.csproj", "{98CE240C-CC51-4658-BD0B-5BEE43297B9F}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Plugins.SolarEdge", "Plugins.SolarEdge\Plugins.SolarEdge.csproj", "{9F3C7BDB-B856-4C09-BD17-784312A9B095}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SmartTeslaAmpSetter.Model", "SmartTeslaAmpSetter.Model\SmartTeslaAmpSetter.Model.csproj", "{8247068B-4A75-40FB-B5BE-1C557CE20AC9}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -39,6 +43,14 @@ Global {98CE240C-CC51-4658-BD0B-5BEE43297B9F}.Debug|Any CPU.Build.0 = Debug|Any CPU {98CE240C-CC51-4658-BD0B-5BEE43297B9F}.Release|Any CPU.ActiveCfg = Release|Any CPU {98CE240C-CC51-4658-BD0B-5BEE43297B9F}.Release|Any CPU.Build.0 = Release|Any CPU + {9F3C7BDB-B856-4C09-BD17-784312A9B095}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9F3C7BDB-B856-4C09-BD17-784312A9B095}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9F3C7BDB-B856-4C09-BD17-784312A9B095}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9F3C7BDB-B856-4C09-BD17-784312A9B095}.Release|Any CPU.Build.0 = Release|Any CPU + {8247068B-4A75-40FB-B5BE-1C557CE20AC9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8247068B-4A75-40FB-B5BE-1C557CE20AC9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8247068B-4A75-40FB-B5BE-1C557CE20AC9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8247068B-4A75-40FB-B5BE-1C557CE20AC9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/SmartTeslaAmpSetter/Client/Pages/CarSettings.razor b/SmartTeslaAmpSetter/Client/Pages/CarSettings.razor index f473a7e05..5676dcace 100644 --- a/SmartTeslaAmpSetter/Client/Pages/CarSettings.razor +++ b/SmartTeslaAmpSetter/Client/Pages/CarSettings.razor @@ -1,7 +1,7 @@ @page "/CarSettings" @using SmartTeslaAmpSetter.Shared.Dtos -@inject HttpClient _httpClient -@inject IToastService _toastService +@inject HttpClient HttpClient +@inject IToastService ToastService

CarSettings

@@ -49,7 +49,7 @@ else protected override async Task OnInitializedAsync() { - _carBasicConfigurations = await _httpClient.GetFromJsonAsync>("/api/Config/GetCarBasicConfigurations"); + _carBasicConfigurations = await HttpClient.GetFromJsonAsync>("/api/Config/GetCarBasicConfigurations"); foreach (var carBasicConfiguration in _carBasicConfigurations!) { @@ -60,14 +60,14 @@ else private async Task UpdateCarConfiguration(int carId, CarBasicConfiguration carBasicConfiguration) { _saveButtonTexts[carId] = _buttonLoadingText; - var result = await _httpClient.PutAsJsonAsync($"api/Config/UpdateCarBasicConfiguration?carId={carId}", carBasicConfiguration); + var result = await HttpClient.PutAsJsonAsync($"api/Config/UpdateCarBasicConfiguration?carId={carId}", carBasicConfiguration); if (result.IsSuccessStatusCode) { - _toastService.ShowSuccess("Car Configuration updated"); + ToastService.ShowSuccess("Car Configuration updated"); } else { - _toastService.ShowError("Error updating car configuration"); + ToastService.ShowError("Error updating car configuration"); } _saveButtonTexts[carId] = _saveButtonDefaultText; } diff --git a/SmartTeslaAmpSetter/Client/Pages/Index.razor b/SmartTeslaAmpSetter/Client/Pages/Index.razor index 6131cd954..e12fbbf00 100644 --- a/SmartTeslaAmpSetter/Client/Pages/Index.razor +++ b/SmartTeslaAmpSetter/Client/Pages/Index.razor @@ -96,6 +96,14 @@ else @car.CarState.Name + + + Pilot Current + + + @car.CarState.ChargerPilotCurrent + + StateString @@ -216,8 +224,6 @@ else private readonly string _chargeModeChangeButtonDefaultText = "Change"; private readonly string _saveButtonDefaultText = "Save"; - private string _refreshButtonText = "Refresh"; - private string _refreshButtonDefaultText = "Refresh"; private readonly string _buttonLoadingText = "..."; protected override async Task OnInitializedAsync() @@ -281,14 +287,4 @@ else } this.StateHasChanged(); } - - private async void RefreshSettings() - { - _refreshButtonText = _buttonLoadingText; - _settings = await HttpClient.GetFromJsonAsync("api/Config/GetSettings"); - this.StateHasChanged(); - ToastService.ShowSuccess("Refreshed"); - _refreshButtonText = _refreshButtonDefaultText; - this.StateHasChanged(); - } } \ No newline at end of file diff --git a/SmartTeslaAmpSetter/Server/Contracts/ICarDbUpdateService.cs b/SmartTeslaAmpSetter/Server/Contracts/ICarDbUpdateService.cs new file mode 100644 index 000000000..a3429f4ab --- /dev/null +++ b/SmartTeslaAmpSetter/Server/Contracts/ICarDbUpdateService.cs @@ -0,0 +1,6 @@ +namespace SmartTeslaAmpSetter.Server.Contracts; + +public interface ICarDbUpdateService +{ + Task UpdateCarsFromDatabase(); +} \ No newline at end of file diff --git a/SmartTeslaAmpSetter/Server/Contracts/IConfigurationWrapper.cs b/SmartTeslaAmpSetter/Server/Contracts/IConfigurationWrapper.cs deleted file mode 100644 index 677a9fde2..000000000 --- a/SmartTeslaAmpSetter/Server/Contracts/IConfigurationWrapper.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace SmartTeslaAmpSetter.Server.Contracts; - -public interface IConfigurationWrapper -{ - string ConfigFileLocation(); - TimeSpan ChargingValueJobUpdateIntervall(); - TimeSpan PvValueJobUpdateIntervall(); - string MqqtClientId(); - string MosquitoServer(); - string CurrentPowerToGridUrl(); - string? CurrentInverterPowerUrl(); - string? CurrentPowerToGridJsonPattern(); - bool CurrentPowerToGridInvertValue(); - string TeslaMateApiBaseUrl(); - List CarPriorities(); - string GeoFence(); - TimeSpan TimeUntilSwitchOn(); - TimeSpan TimespanUntilSwitchOff(); - int PowerBuffer(); - string? TelegramBotKey(); - string? TelegramChannelId(); -} \ No newline at end of file diff --git a/SmartTeslaAmpSetter/Server/Enums/NodePatternType.cs b/SmartTeslaAmpSetter/Server/Enums/NodePatternType.cs new file mode 100644 index 000000000..eea434870 --- /dev/null +++ b/SmartTeslaAmpSetter/Server/Enums/NodePatternType.cs @@ -0,0 +1,8 @@ +namespace SmartTeslaAmpSetter.Server.Enums; + +public enum NodePatternType +{ + Json, + Xml, + None, +} \ No newline at end of file diff --git a/SmartTeslaAmpSetter/Server/Enums/PvValueType.cs b/SmartTeslaAmpSetter/Server/Enums/PvValueType.cs new file mode 100644 index 000000000..ce64807ba --- /dev/null +++ b/SmartTeslaAmpSetter/Server/Enums/PvValueType.cs @@ -0,0 +1,7 @@ +namespace SmartTeslaAmpSetter.Server.Enums; + +public enum PvValueType +{ + Inverter, + Grid, +} \ No newline at end of file diff --git a/SmartTeslaAmpSetter/Server/Program.cs b/SmartTeslaAmpSetter/Server/Program.cs index d30583115..bc7662501 100644 --- a/SmartTeslaAmpSetter/Server/Program.cs +++ b/SmartTeslaAmpSetter/Server/Program.cs @@ -1,16 +1,20 @@ +using Microsoft.EntityFrameworkCore; using MQTTnet; using Quartz; using Quartz.Impl; using Quartz.Spi; using Serilog; +using SmartTeslaAmpSetter.Model.Contracts; +using SmartTeslaAmpSetter.Model.EntityFramework; using SmartTeslaAmpSetter.Server.Contracts; using SmartTeslaAmpSetter.Server.Scheduling; using SmartTeslaAmpSetter.Server.Services; -using SmartTeslaAmpSetter.Server.Wrappers; +using SmartTeslaAmpSetter.Shared.Contracts; using SmartTeslaAmpSetter.Shared.Dtos; using SmartTeslaAmpSetter.Shared.Dtos.Contracts; using SmartTeslaAmpSetter.Shared.Dtos.Settings; using SmartTeslaAmpSetter.Shared.TimeProviding; +using SmartTeslaAmpSetter.Shared.Wrappers; var builder = WebApplication.CreateBuilder(args); @@ -26,13 +30,13 @@ var mqttFactory = new MqttFactory(); var mqttClient = mqttFactory.CreateMqttClient(); - builder.Services .AddTransient() .AddTransient() .AddTransient() .AddTransient() .AddTransient() + .AddTransient() .AddTransient() .AddTransient() .AddTransient() @@ -51,6 +55,14 @@ .AddTransient() .AddTransient() .AddTransient() + .AddTransient() + .AddDbContext((provider, options) => + { + options.UseNpgsql(provider.GetRequiredService().GetConnectionString()); + options.EnableSensitiveDataLogging(); + options.EnableDetailedErrors(); + }, ServiceLifetime.Transient, ServiceLifetime.Transient) + .AddTransient() ; builder.Host.UseSerilog((context, configuration) => configuration diff --git a/SmartTeslaAmpSetter/Server/Scheduling/CarDbUpdateJob.cs b/SmartTeslaAmpSetter/Server/Scheduling/CarDbUpdateJob.cs new file mode 100644 index 000000000..68090b67e --- /dev/null +++ b/SmartTeslaAmpSetter/Server/Scheduling/CarDbUpdateJob.cs @@ -0,0 +1,22 @@ +using Quartz; +using SmartTeslaAmpSetter.Server.Contracts; + +namespace SmartTeslaAmpSetter.Server.Scheduling; + +[DisallowConcurrentExecution] +public class CarDbUpdateJob : IJob +{ + private readonly ILogger _logger; + private readonly ICarDbUpdateService _carDbUpdateService; + + public CarDbUpdateJob(ILogger logger, ICarDbUpdateService carDbUpdateService) + { + _logger = logger; + _carDbUpdateService = carDbUpdateService; + } + public async Task Execute(IJobExecutionContext context) + { + _logger.LogTrace("Executing job to update cars from database"); + await _carDbUpdateService.UpdateCarsFromDatabase().ConfigureAwait(false); + } +} \ No newline at end of file diff --git a/SmartTeslaAmpSetter/Server/Scheduling/JobManager.cs b/SmartTeslaAmpSetter/Server/Scheduling/JobManager.cs index 852bfd39e..90203cf39 100644 --- a/SmartTeslaAmpSetter/Server/Scheduling/JobManager.cs +++ b/SmartTeslaAmpSetter/Server/Scheduling/JobManager.cs @@ -1,6 +1,6 @@ using Quartz; using Quartz.Spi; -using SmartTeslaAmpSetter.Server.Contracts; +using SmartTeslaAmpSetter.Shared.Contracts; namespace SmartTeslaAmpSetter.Server.Scheduling; @@ -34,10 +34,11 @@ public async void StartJobs() var configJsonUpdateJob = JobBuilder.Create().Build(); var chargeTimeUpdateJob = JobBuilder.Create().Build(); var pvValueJob = JobBuilder.Create().Build(); + var carDbUpdateJob = JobBuilder.Create().Build(); var jobIntervall = _configurationWrapper.ChargingValueJobUpdateIntervall(); - var defaultTrigger = + var chargingValueTrigger = TriggerBuilder.Create().WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever((int)jobIntervall.TotalSeconds)).Build(); var updateJsonTrigger = TriggerBuilder.Create() @@ -52,12 +53,16 @@ public async void StartJobs() var pvValueTrigger = TriggerBuilder.Create() .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever((int)pvValueJobIntervall.TotalSeconds)).Build(); + var carDbUpdateTrigger = TriggerBuilder.Create() + .WithSchedule(SimpleScheduleBuilder.RepeatSecondlyForever(10)).Build(); + var triggersAndJobs = new Dictionary> { - {chargingValueJob, new HashSet { defaultTrigger }}, + {chargingValueJob, new HashSet { chargingValueTrigger }}, {configJsonUpdateJob, new HashSet {updateJsonTrigger}}, {chargeTimeUpdateJob, new HashSet {chargeTimeUpdateTrigger}}, {pvValueJob, new HashSet {pvValueTrigger}}, + {carDbUpdateJob, new HashSet {carDbUpdateTrigger}}, }; await _scheduler.ScheduleJobs(triggersAndJobs, false).ConfigureAwait(false); diff --git a/SmartTeslaAmpSetter/Server/Services/CarDbUpdateService.cs b/SmartTeslaAmpSetter/Server/Services/CarDbUpdateService.cs new file mode 100644 index 000000000..f2383884f --- /dev/null +++ b/SmartTeslaAmpSetter/Server/Services/CarDbUpdateService.cs @@ -0,0 +1,47 @@ +using Microsoft.EntityFrameworkCore; +using SmartTeslaAmpSetter.Model.Contracts; +using SmartTeslaAmpSetter.Server.Contracts; +using SmartTeslaAmpSetter.Shared.Dtos.Contracts; + +namespace SmartTeslaAmpSetter.Server.Services; + +public class CarDbUpdateService : ICarDbUpdateService +{ + private readonly ILogger _logger; + private readonly ISettings _settings; + private readonly ITeslamateContext _teslamateContext; + private readonly ITelegramService _telegramService; + + public CarDbUpdateService(ILogger logger, ISettings settings, ITeslamateContext teslamateContext, ITelegramService telegramService) + { + _logger = logger; + _settings = settings; + _teslamateContext = teslamateContext; + _telegramService = telegramService; + } + + public async Task UpdateCarsFromDatabase() + { + _logger.LogTrace("{method}()", nameof(UpdateCarsFromDatabase)); + foreach (var car in _settings.Cars) + { + try + { + var pilotCurrent = await _teslamateContext.Charges + .Where(c => c.ChargingProcess.CarId == car.Id) + .OrderByDescending(c => c.Date) + .Select(c => c.ChargerPilotCurrent) + .FirstOrDefaultAsync(); + _logger.LogTrace("Pilot Current for var {car} is {pilotCurrent}", car.Id, pilotCurrent); + car.CarState.ChargerPilotCurrent = pilotCurrent; + } + catch (Exception exception) + { + _logger.LogError(exception, "Error while trying to get pilot current from database. Retrying in one minute."); + await Task.Delay(TimeSpan.FromMinutes(1)); + } + + + } + } +} \ No newline at end of file diff --git a/SmartTeslaAmpSetter/Server/Services/ChargeTimeUpdateService.cs b/SmartTeslaAmpSetter/Server/Services/ChargeTimeUpdateService.cs index b06ce3b4f..a0192e6b0 100644 --- a/SmartTeslaAmpSetter/Server/Services/ChargeTimeUpdateService.cs +++ b/SmartTeslaAmpSetter/Server/Services/ChargeTimeUpdateService.cs @@ -1,5 +1,6 @@ using System.Runtime.CompilerServices; using SmartTeslaAmpSetter.Server.Contracts; +using SmartTeslaAmpSetter.Shared.Contracts; using SmartTeslaAmpSetter.Shared.Dtos.Contracts; using SmartTeslaAmpSetter.Shared.Dtos.Settings; using SmartTeslaAmpSetter.Shared.TimeProviding; diff --git a/SmartTeslaAmpSetter/Server/Services/ChargingService.cs b/SmartTeslaAmpSetter/Server/Services/ChargingService.cs index 38328567a..9d4d9af00 100644 --- a/SmartTeslaAmpSetter/Server/Services/ChargingService.cs +++ b/SmartTeslaAmpSetter/Server/Services/ChargingService.cs @@ -1,7 +1,7 @@ using System.Runtime.CompilerServices; using SmartTeslaAmpSetter.Server.Contracts; +using SmartTeslaAmpSetter.Shared.Contracts; using SmartTeslaAmpSetter.Shared.Dtos.Contracts; -using SmartTeslaAmpSetter.Shared.Dtos.Settings; using SmartTeslaAmpSetter.Shared.Enums; using SmartTeslaAmpSetter.Shared.TimeProviding; using Car = SmartTeslaAmpSetter.Shared.Dtos.Settings.Car; @@ -12,7 +12,6 @@ namespace SmartTeslaAmpSetter.Server.Services; public class ChargingService : IChargingService { private readonly ILogger _logger; - private readonly IGridService _gridService; private readonly ISettings _settings; private readonly IDateTimeProvider _dateTimeProvider; private readonly ITelegramService _telegramService; @@ -20,12 +19,11 @@ public class ChargingService : IChargingService private readonly IConfigurationWrapper _configurationWrapper; private readonly IPvValueService _pvValueService; - public ChargingService(ILogger logger, IGridService gridService, + public ChargingService(ILogger logger, ISettings settings, IDateTimeProvider dateTimeProvider, ITelegramService telegramService, ITeslaService teslaService, IConfigurationWrapper configurationWrapper, IPvValueService pvValueService) { _logger = logger; - _gridService = gridService; _settings = settings; _dateTimeProvider = dateTimeProvider; _telegramService = telegramService; @@ -182,6 +180,21 @@ private async Task ChangeCarAmp(Car car, int ampToChange) var maxAmpPerCar = car.CarConfiguration.MaximumAmpere; _logger.LogDebug("Min amp for car: {amp}", minAmpPerCar); _logger.LogDebug("Max amp for car: {amp}", maxAmpPerCar); + if (car.CarState.ChargerPilotCurrent != null && maxAmpPerCar > car.CarState.ChargerPilotCurrent) + { + _logger.LogWarning("Charging speed of {carID} id reduced to {amp}", car.Id, car.CarState.ChargerPilotCurrent); + maxAmpPerCar = (int)car.CarState.ChargerPilotCurrent; + if (!car.CarState.ReducedChargeSpeedWarning) + { + car.CarState.ReducedChargeSpeedWarning = true; + await _telegramService.SendMessage($"Charging of {car.CarState.Name} is reduced to {car.CarState.ChargerPilotCurrent} due to chargelimit of wallbox.").ConfigureAwait(false); + } + } + else if(car.CarState.ReducedChargeSpeedWarning) + { + car.CarState.ReducedChargeSpeedWarning = false; + await _telegramService.SendMessage($"Charging speed of {car.CarState.Name} is regained.").ConfigureAwait(false); + } EnableFullSpeedChargeIfMinimumSocNotReachable(car); DisableFullSpeedChargeIfMinimumSocReachedOrMinimumSocReachable(car); diff --git a/SmartTeslaAmpSetter/Server/Services/ConfigJsonService.cs b/SmartTeslaAmpSetter/Server/Services/ConfigJsonService.cs index 5ae4b600c..75e0390c9 100644 --- a/SmartTeslaAmpSetter/Server/Services/ConfigJsonService.cs +++ b/SmartTeslaAmpSetter/Server/Services/ConfigJsonService.cs @@ -3,6 +3,7 @@ using Newtonsoft.Json; using SmartTeslaAmpSetter.Server.Contracts; using SmartTeslaAmpSetter.Shared; +using SmartTeslaAmpSetter.Shared.Contracts; using SmartTeslaAmpSetter.Shared.Dtos.Contracts; using SmartTeslaAmpSetter.Shared.Dtos.Settings; using SmartTeslaAmpSetter.Shared.Enums; diff --git a/SmartTeslaAmpSetter/Server/Services/ConfigService.cs b/SmartTeslaAmpSetter/Server/Services/ConfigService.cs index e372b394f..273e01765 100644 --- a/SmartTeslaAmpSetter/Server/Services/ConfigService.cs +++ b/SmartTeslaAmpSetter/Server/Services/ConfigService.cs @@ -11,13 +11,11 @@ public class ConfigService : IConfigService { private readonly ILogger _logger; private readonly ISettings _settings; - private readonly IChargingService _chargingService; - public ConfigService(ILogger logger, ISettings settings, IChargingService chargingService) + public ConfigService(ILogger logger, ISettings settings) { _logger = logger; _settings = settings; - _chargingService = chargingService; } public ISettings GetSettings() diff --git a/SmartTeslaAmpSetter/Server/Services/GridService.cs b/SmartTeslaAmpSetter/Server/Services/GridService.cs index 6185b921f..37dfefdc2 100644 --- a/SmartTeslaAmpSetter/Server/Services/GridService.cs +++ b/SmartTeslaAmpSetter/Server/Services/GridService.cs @@ -1,6 +1,9 @@ using System.Globalization; +using System.Xml; using Newtonsoft.Json.Linq; using SmartTeslaAmpSetter.Server.Contracts; +using SmartTeslaAmpSetter.Server.Enums; +using SmartTeslaAmpSetter.Shared.Contracts; namespace SmartTeslaAmpSetter.Server.Services; @@ -30,42 +33,40 @@ public GridService(ILogger logger, ITelegramService telegramService if (!response.IsSuccessStatusCode) { _logger.LogError("Could not get current overage. {statusCode}, {reasonPhrase}", response.StatusCode, response.ReasonPhrase); + await _telegramService.SendMessage( + $"Getting current grid power did result in statuscode {response.StatusCode} with reason {response.ReasonPhrase}"); return null; } var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); + var pattern = ""; var jsonPattern = _configurationWrapper.CurrentPowerToGridJsonPattern(); - + var xmlPattern = _configurationWrapper.CurrentPowerToGridXmlPattern(); + NodePatternType nodePatternType; if (jsonPattern != null) { - _logger.LogDebug("Extract overage value from {result} with {jsonPattern}", result, jsonPattern); - result = (JObject.Parse(result).SelectToken(jsonPattern) ?? - throw new InvalidOperationException("Extracted Json Value is null")).Value(); + nodePatternType = NodePatternType.Json; + pattern = jsonPattern; } - - try + else if (xmlPattern != null) { - var overage = GetIntegerFromString(result); - if (_configurationWrapper.CurrentPowerToGridInvertValue()) - { - overage = -overage; - } - return overage ; + nodePatternType = NodePatternType.Xml; + pattern = xmlPattern; } - catch (Exception) + else { - throw new InvalidCastException($"Could not parse result {result} from uri {requestUri} to integer"); + nodePatternType = NodePatternType.None; } - } + var overage = GetValueFromResult(pattern, result, nodePatternType, true); + if (_configurationWrapper.CurrentPowerToGridInvertValue()) + { + overage = -overage; + } - internal int GetIntegerFromString(string? inputString) - { - _logger.LogTrace("{method}({param})", nameof(GetIntegerFromString), inputString); - return (int) double.Parse(inputString ?? throw new ArgumentNullException(nameof(inputString)), CultureInfo.InvariantCulture); + return overage; } - public async Task GetCurrentInverterPower() { _logger.LogTrace("{method}()", nameof(GetCurrentInverterPower)); @@ -88,13 +89,96 @@ await _telegramService.SendMessage( } var result = await response.Content.ReadAsStringAsync().ConfigureAwait(false); - try + var pattern = ""; + var jsonPattern = _configurationWrapper.CurrentInverterPowerJsonPattern(); + var xmlPattern = _configurationWrapper.CurrentInverterPowerXmlPattern(); + NodePatternType nodePatternType; + if (jsonPattern != null) { - return GetIntegerFromString(result); + nodePatternType = NodePatternType.Json; + pattern = jsonPattern; } - catch (Exception) + else if (xmlPattern != null) { - throw new InvalidCastException($"Could not parse result {result} from uri {requestUri} to integer"); + nodePatternType = NodePatternType.Xml; + pattern = xmlPattern; } + else + { + nodePatternType = NodePatternType.None; + } + + return GetValueFromResult(pattern, result, nodePatternType, false); } + + /// + /// + /// + /// + /// + /// + /// true if grid meter value is requested, false if inverter value is requested + /// + /// + /// + internal int GetValueFromResult(string pattern, string result, NodePatternType patternType, bool isGridValue) + { + switch (patternType) + { + case NodePatternType.Json: + _logger.LogDebug("Extract overage value from json {result} with {pattern}", result, pattern); + result = (JObject.Parse(result).SelectToken(pattern) ?? + throw new InvalidOperationException("Could not find token by pattern")).Value() ?? throw new InvalidOperationException("Extracted Json Value is null"); + break; + case NodePatternType.Xml: + _logger.LogDebug("Extract overage value from xml {result} with {pattern}", result, pattern); + var xmlDocument = new XmlDocument(); + xmlDocument.LoadXml(result); + var nodes = xmlDocument.SelectNodes(pattern) ?? throw new InvalidOperationException("Could not find any nodes by pattern"); + switch (nodes.Count) + { + case < 1: + throw new InvalidOperationException($"Could not find any nodes with pattern {pattern}"); + case > 2: + var xmlAttributeHeaderName = (isGridValue + ? _configurationWrapper.CurrentPowerToGridXmlAttributeHeaderName() + : _configurationWrapper.CurrentInverterPowerXmlAttributeHeaderName()) + ?? throw new InvalidOperationException("Could not get xmlAttributeHeaderName"); + + var xmlAttributeHeaderValue = (isGridValue + ? _configurationWrapper.CurrentPowerToGridXmlAttributeHeaderValue() + : _configurationWrapper.CurrentInverterPowerXmlAttributeHeaderValue()) + ?? throw new InvalidOperationException("Could not get xmlAttributeHeaderValue"); + + var xmlAttributeValueName = (isGridValue + ? _configurationWrapper.CurrentPowerToGridXmlAttributeValueName() + : _configurationWrapper.CurrentInverterPowerXmlAttributeValueName()) + ?? throw new InvalidOperationException("Could not get xmlAttributeValueName"); + + for (int i = 0; i < nodes.Count; i++) + { + if (nodes[i]?.Attributes?[xmlAttributeHeaderName]?.Value == xmlAttributeHeaderValue) + { + result = nodes[i]?.Attributes?[xmlAttributeValueName]?.Value ?? "0"; + break; + } + } + break; + case 1: + result = nodes[0]?.LastChild?.Value ?? "0"; + break; + } + break; + } + + return GetIntegerFromString(result); + } + + internal int GetIntegerFromString(string? inputString) + { + _logger.LogTrace("{method}({param})", nameof(GetIntegerFromString), inputString); + return (int)double.Parse(inputString ?? throw new ArgumentNullException(nameof(inputString)), CultureInfo.InvariantCulture); + } + + } \ No newline at end of file diff --git a/SmartTeslaAmpSetter/Server/Services/MqttService.cs b/SmartTeslaAmpSetter/Server/Services/MqttService.cs index de647112c..73975b1b5 100644 --- a/SmartTeslaAmpSetter/Server/Services/MqttService.cs +++ b/SmartTeslaAmpSetter/Server/Services/MqttService.cs @@ -1,8 +1,8 @@ using MQTTnet; using MQTTnet.Client; using SmartTeslaAmpSetter.Server.Contracts; +using SmartTeslaAmpSetter.Shared.Contracts; using SmartTeslaAmpSetter.Shared.Dtos.Contracts; -using SmartTeslaAmpSetter.Shared.Dtos.Settings; using CarState = SmartTeslaAmpSetter.Shared.Enums.CarState; namespace SmartTeslaAmpSetter.Server.Services; diff --git a/SmartTeslaAmpSetter/Server/Services/PvValueService.cs b/SmartTeslaAmpSetter/Server/Services/PvValueService.cs index 43cc2c16c..2bf40def7 100644 --- a/SmartTeslaAmpSetter/Server/Services/PvValueService.cs +++ b/SmartTeslaAmpSetter/Server/Services/PvValueService.cs @@ -1,6 +1,6 @@ using SmartTeslaAmpSetter.Server.Contracts; +using SmartTeslaAmpSetter.Shared.Contracts; using SmartTeslaAmpSetter.Shared.Dtos.Contracts; -using SmartTeslaAmpSetter.Shared.Dtos.Settings; namespace SmartTeslaAmpSetter.Server.Services; diff --git a/SmartTeslaAmpSetter/Server/Services/TelegramService.cs b/SmartTeslaAmpSetter/Server/Services/TelegramService.cs index ffed182ba..e24c976f6 100644 --- a/SmartTeslaAmpSetter/Server/Services/TelegramService.cs +++ b/SmartTeslaAmpSetter/Server/Services/TelegramService.cs @@ -1,5 +1,6 @@ using System.Net; using SmartTeslaAmpSetter.Server.Contracts; +using SmartTeslaAmpSetter.Shared.Contracts; namespace SmartTeslaAmpSetter.Server.Services; diff --git a/SmartTeslaAmpSetter/Server/Services/TeslamateApiService.cs b/SmartTeslaAmpSetter/Server/Services/TeslamateApiService.cs index a365456f9..5989ef495 100644 --- a/SmartTeslaAmpSetter/Server/Services/TeslamateApiService.cs +++ b/SmartTeslaAmpSetter/Server/Services/TeslamateApiService.cs @@ -1,8 +1,8 @@ using System.Text; using Newtonsoft.Json; using SmartTeslaAmpSetter.Server.Contracts; +using SmartTeslaAmpSetter.Shared.Contracts; using SmartTeslaAmpSetter.Shared.Dtos.Contracts; -using SmartTeslaAmpSetter.Shared.Dtos.Settings; using CarState = SmartTeslaAmpSetter.Shared.Enums.CarState; namespace SmartTeslaAmpSetter.Server.Services; diff --git a/SmartTeslaAmpSetter/Server/SmartTeslaAmpSetter.Server.csproj b/SmartTeslaAmpSetter/Server/SmartTeslaAmpSetter.Server.csproj index 801f8ef6c..20adf705c 100644 --- a/SmartTeslaAmpSetter/Server/SmartTeslaAmpSetter.Server.csproj +++ b/SmartTeslaAmpSetter/Server/SmartTeslaAmpSetter.Server.csproj @@ -28,6 +28,7 @@ + @@ -36,6 +37,7 @@ + @@ -46,5 +48,9 @@ + + + + diff --git a/SmartTeslaAmpSetter/Server/appsettings.Development.json b/SmartTeslaAmpSetter/Server/appsettings.Development.json index ae4ad1e96..38c3927bc 100644 --- a/SmartTeslaAmpSetter/Server/appsettings.Development.json +++ b/SmartTeslaAmpSetter/Server/appsettings.Development.json @@ -18,7 +18,7 @@ "Name": "Console", "Args": { "outputTemplate": "[{Timestamp:HH:mm:ss.fff} {Level:u3} {SourceContext}] {Message:lj}{NewLine}{Exception}" - } + } } ], "Enrich": [ @@ -28,6 +28,15 @@ "AllowedHosts": "*", "CurrentPowerToGridUrl": "http://192.168.1.50:5007/api/ChargingLog/GetAverageGridPowerOfLastXseconds", //"CurrentPowerToGridJsonPattern": "$.data.value", + //"CurrentPowerToGridXmlPattern": "$.data.value", + //"CurrentInverterPowerJsonPattern": "pattern", + //"CurrentInverterPowerXmlPattern": "pattern", + //"CurrentPowerToGridXmlAttributeHeaderName": "Type", + //"CurrentPowerToGridXmlAttributeHeaderValue": "GridPower", + //"CurrentPowerToGridXmlAttributeValueName": "Value", + //"CurrentInverterPowerAttributeHeaderName": "Type", + //"CurrentInverterPowerAttributeHeaderValue": "GridPower", + //"CurrentInverterPowerAttributeValueName": "Value", //"CurrentPowerToGridInvertValue": false, "CurrentInverterPowerUrl": "http://192.168.1.50:5007/api/ChargingLog/GetAverageInverterPowerOfLastXseconds", "TeslaMateApiBaseUrl": "http://192.168.1.50:8097", @@ -39,5 +48,10 @@ "MinutesUntilSwitchOff": 5, "PowerBuffer": 0, "MqqtClientId": "SmartTeslaAmpSetterDevelopment", - "MosquitoServer": "192.168.1.50" + "MosquitoServer": "192.168.1.50", + "TeslaMateDbServer": "192.168.1.50", + "TeslaMateDbPort": "5433", + "TeslaMateDbDatabaseName": "teslamate", + "TeslaMateDbUser": "teslamate", + "TeslaMateDbPassword": "secret" } \ No newline at end of file diff --git a/SmartTeslaAmpSetter/Server/appsettings.json b/SmartTeslaAmpSetter/Server/appsettings.json index cb1da4fb5..214c2db4d 100644 --- a/SmartTeslaAmpSetter/Server/appsettings.json +++ b/SmartTeslaAmpSetter/Server/appsettings.json @@ -31,10 +31,24 @@ "UpdateIntervalSeconds": 30, "PvValueUpdateIntervalSeconds": 1, "MqqtClientId": "SmartTeslaAmpSetter", - "MosquitoServer": "mosquitto" + "MosquitoServer": "mosquitto", + "TeslaMateDbServer": "database", + "TeslaMateDbPort": "5432", + "TeslaMateDbDatabaseName": "teslamate", + "TeslaMateDbUser": "teslamate", + "TeslaMateDbPassword": "secret" //"CurrentPowerToGridUrl": "http://192.168.1.50:5007/api/ChargingLog/GetAverageGridPowerOfLastXseconds", //"CurrentInverterPowerUrl": "http://192.168.1.50:5007/api/ChargingLog/GetAverageInverterPowerOfLastXseconds", //"CurrentPowerToGridJsonPattern": "pattern", + //"CurrentPowerToGridXmlPattern": "pattern", + //"CurrentPowerToGridXmlAttributeHeaderName": "Type", + //"CurrentPowerToGridXmlAttributeHeaderValue": "GridPower", + //"CurrentPowerToGridXmlAttributeValueName": "Value", + //"CurrentInverterPowerJsonPattern": "pattern", + //"CurrentInverterPowerXmlPattern": "pattern", + //"CurrentInverterPowerAttributeHeaderName": "Type", + //"CurrentInverterPowerAttributeHeaderValue": "GridPower", + //"CurrentInverterPowerAttributeValueName": "Value", //"CurrentPowerToGridInvertValue": false, //"TeslaMateApiBaseUrl": "http://192.168.1.50:8097", //"CarPriorities": "1|2", diff --git a/SmartTeslaAmpSetter/Shared/Contracts/IConfigurationWrapper.cs b/SmartTeslaAmpSetter/Shared/Contracts/IConfigurationWrapper.cs new file mode 100644 index 000000000..340223d92 --- /dev/null +++ b/SmartTeslaAmpSetter/Shared/Contracts/IConfigurationWrapper.cs @@ -0,0 +1,36 @@ +namespace SmartTeslaAmpSetter.Shared.Contracts; + +public interface IConfigurationWrapper +{ + string ConfigFileLocation(); + TimeSpan ChargingValueJobUpdateIntervall(); + TimeSpan PvValueJobUpdateIntervall(); + string MqqtClientId(); + string MosquitoServer(); + string CurrentPowerToGridUrl(); + string? CurrentInverterPowerUrl(); + string? CurrentPowerToGridJsonPattern(); + bool CurrentPowerToGridInvertValue(); + string TeslaMateApiBaseUrl(); + List CarPriorities(); + string GeoFence(); + TimeSpan TimeUntilSwitchOn(); + TimeSpan TimespanUntilSwitchOff(); + int PowerBuffer(); + string? TelegramBotKey(); + string? TelegramChannelId(); + string? CurrentInverterPowerJsonPattern(); + string? CurrentPowerToGridXmlPattern(); + string? CurrentInverterPowerXmlPattern(); + string? CurrentPowerToGridXmlAttributeHeaderName(); + string? CurrentPowerToGridXmlAttributeHeaderValue(); + string? CurrentPowerToGridXmlAttributeValueName(); + string? CurrentInverterPowerXmlAttributeHeaderName(); + string? CurrentInverterPowerXmlAttributeHeaderValue(); + string? CurrentInverterPowerXmlAttributeValueName(); + string TeslaMateDbServer(); + int TeslaMateDbPort(); + string TeslaMateDbDatabaseName(); + string TeslaMateDbUser(); + string TeslaMateDbPassword(); +} \ No newline at end of file diff --git a/SmartTeslaAmpSetter/Shared/TimeProviding/IDateTimeProvider.cs b/SmartTeslaAmpSetter/Shared/Contracts/IDateTimeProvider.cs similarity index 51% rename from SmartTeslaAmpSetter/Shared/TimeProviding/IDateTimeProvider.cs rename to SmartTeslaAmpSetter/Shared/Contracts/IDateTimeProvider.cs index 6d9cc1428..ab619c5d8 100644 --- a/SmartTeslaAmpSetter/Shared/TimeProviding/IDateTimeProvider.cs +++ b/SmartTeslaAmpSetter/Shared/Contracts/IDateTimeProvider.cs @@ -1,4 +1,4 @@ -namespace SmartTeslaAmpSetter.Shared.TimeProviding; +namespace SmartTeslaAmpSetter.Shared.Contracts; public interface IDateTimeProvider { diff --git a/SmartTeslaAmpSetter/Shared/Dtos/Settings/CarState.cs b/SmartTeslaAmpSetter/Shared/Dtos/Settings/CarState.cs index cd7c51a53..7d8917688 100644 --- a/SmartTeslaAmpSetter/Shared/Dtos/Settings/CarState.cs +++ b/SmartTeslaAmpSetter/Shared/Dtos/Settings/CarState.cs @@ -18,6 +18,7 @@ public class CarState public int? ChargerVoltage { get; set; } public int? ChargerActualCurrent { get; set; } + public int? ChargerPilotCurrent { get; set; } public bool? PluggedIn { get; set; } public bool? ClimateOn { get; set; } public int? ChargingPowerAtHome { get; set; } @@ -33,4 +34,5 @@ public int? ChargingPower public string? StateString { get; set; } public Enums.CarState? State { get; set; } public bool? Healthy { get; set; } + public bool ReducedChargeSpeedWarning { get; set; } } \ No newline at end of file diff --git a/SmartTeslaAmpSetter/Shared/SmartTeslaAmpSetter.Shared.csproj b/SmartTeslaAmpSetter/Shared/SmartTeslaAmpSetter.Shared.csproj index 9ac5160ee..987f14931 100644 --- a/SmartTeslaAmpSetter/Shared/SmartTeslaAmpSetter.Shared.csproj +++ b/SmartTeslaAmpSetter/Shared/SmartTeslaAmpSetter.Shared.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -15,6 +15,9 @@ + + + diff --git a/SmartTeslaAmpSetter/Shared/TimeProviding/DateTimeProvider.cs b/SmartTeslaAmpSetter/Shared/TimeProviding/DateTimeProvider.cs index 18ba5bae1..ff7b82f1e 100644 --- a/SmartTeslaAmpSetter/Shared/TimeProviding/DateTimeProvider.cs +++ b/SmartTeslaAmpSetter/Shared/TimeProviding/DateTimeProvider.cs @@ -1,4 +1,6 @@ -namespace SmartTeslaAmpSetter.Shared.TimeProviding; +using SmartTeslaAmpSetter.Shared.Contracts; + +namespace SmartTeslaAmpSetter.Shared.TimeProviding; public class DateTimeProvider : IDateTimeProvider { diff --git a/SmartTeslaAmpSetter/Shared/TimeProviding/FakeDateTimeProvider.cs b/SmartTeslaAmpSetter/Shared/TimeProviding/FakeDateTimeProvider.cs index 4d4c475ea..bc2f0950a 100644 --- a/SmartTeslaAmpSetter/Shared/TimeProviding/FakeDateTimeProvider.cs +++ b/SmartTeslaAmpSetter/Shared/TimeProviding/FakeDateTimeProvider.cs @@ -1,4 +1,6 @@ -namespace SmartTeslaAmpSetter.Shared.TimeProviding; +using SmartTeslaAmpSetter.Shared.Contracts; + +namespace SmartTeslaAmpSetter.Shared.TimeProviding; public class FakeDateTimeProvider : IDateTimeProvider { diff --git a/SmartTeslaAmpSetter/Server/Wrappers/ConfigurationWrapper.cs b/SmartTeslaAmpSetter/Shared/Wrappers/ConfigurationWrapper.cs similarity index 54% rename from SmartTeslaAmpSetter/Server/Wrappers/ConfigurationWrapper.cs rename to SmartTeslaAmpSetter/Shared/Wrappers/ConfigurationWrapper.cs index 88975e77e..2756cd09e 100644 --- a/SmartTeslaAmpSetter/Server/Wrappers/ConfigurationWrapper.cs +++ b/SmartTeslaAmpSetter/Shared/Wrappers/ConfigurationWrapper.cs @@ -1,8 +1,10 @@ using System.Runtime.CompilerServices; -using SmartTeslaAmpSetter.Server.Contracts; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using SmartTeslaAmpSetter.Shared.Contracts; [assembly: InternalsVisibleTo("SmartTeslaAmpSetter.Tests")] -namespace SmartTeslaAmpSetter.Server.Wrappers; +namespace SmartTeslaAmpSetter.Shared.Wrappers; public class ConfigurationWrapper : IConfigurationWrapper { @@ -18,7 +20,7 @@ public ConfigurationWrapper(ILogger logger, IConfiguration public string ConfigFileLocation() { var environmentVariableName = "ConfigFileLocation"; - var value = GetNotNullableConfigurationValue(environmentVariableName); + var value = GetNotNullableConfigurationValue(environmentVariableName); _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); return value; } @@ -54,7 +56,7 @@ public TimeSpan PvValueJobUpdateIntervall() public string MqqtClientId() { var environmentVariableName = "MqqtClientId"; - var value = GetNotNullableConfigurationValue(environmentVariableName); + var value = GetNotNullableConfigurationValue(environmentVariableName); _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); return value; } @@ -62,7 +64,47 @@ public string MqqtClientId() public string MosquitoServer() { var environmentVariableName = "MosquitoServer"; - var value = GetNotNullableConfigurationValue(environmentVariableName); + var value = GetNotNullableConfigurationValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + + public string TeslaMateDbServer() + { + var environmentVariableName = "TeslaMateDbServer"; + var value = _configuration.GetValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + + public int TeslaMateDbPort() + { + var environmentVariableName = "TeslaMateDbPort"; + var value = _configuration.GetValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + + public string TeslaMateDbDatabaseName() + { + var environmentVariableName = "TeslaMateDbDatabaseName"; + var value = _configuration.GetValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + + public string TeslaMateDbUser() + { + var environmentVariableName = "TeslaMateDbUser"; + var value = _configuration.GetValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + + public string TeslaMateDbPassword() + { + var environmentVariableName = "TeslaMateDbPassword"; + var value = _configuration.GetValue(environmentVariableName); _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); return value; } @@ -70,7 +112,7 @@ public string MosquitoServer() public string CurrentPowerToGridUrl() { var environmentVariableName = "CurrentPowerToGridUrl"; - var value = GetNotNullableConfigurationValue(environmentVariableName); + var value = GetNotNullableConfigurationValue(environmentVariableName); _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); return value; } @@ -78,7 +120,7 @@ public string CurrentPowerToGridUrl() public string? CurrentInverterPowerUrl() { var environmentVariableName = "CurrentInverterPowerUrl"; - var value = GetNullableConfigurationValue(environmentVariableName); + var value = GetNullableConfigurationValue(environmentVariableName); _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); return value; } @@ -91,6 +133,78 @@ public string CurrentPowerToGridUrl() return value; } + public string? CurrentPowerToGridXmlPattern() + { + var environmentVariableName = "CurrentPowerToGridXmlPattern"; + var value = _configuration.GetValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + + public string? CurrentPowerToGridXmlAttributeHeaderName() + { + var environmentVariableName = "CurrentPowerToGridXmlAttributeHeaderName"; + var value = _configuration.GetValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + + public string? CurrentPowerToGridXmlAttributeHeaderValue() + { + var environmentVariableName = "CurrentPowerToGridXmlAttributeHeaderValue"; + var value = _configuration.GetValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + + public string? CurrentPowerToGridXmlAttributeValueName() + { + var environmentVariableName = "CurrentPowerToGridXmlAttributeValueName"; + var value = _configuration.GetValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + + public string? CurrentInverterPowerJsonPattern() + { + var environmentVariableName = "CurrentInverterPowerJsonPattern"; + var value = _configuration.GetValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + + public string? CurrentInverterPowerXmlPattern() + { + var environmentVariableName = "CurrentInverterPowerXmlPattern"; + var value = _configuration.GetValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + + public string? CurrentInverterPowerXmlAttributeHeaderName() + { + var environmentVariableName = "CurrentInverterPowerXmlAttributeHeaderName"; + var value = _configuration.GetValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + + public string? CurrentInverterPowerXmlAttributeHeaderValue() + { + var environmentVariableName = "CurrentInverterPowerXmlAttributeHeaderValue"; + var value = _configuration.GetValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + + public string? CurrentInverterPowerXmlAttributeValueName() + { + var environmentVariableName = "CurrentInverterPowerXmlAttributeValueName"; + var value = _configuration.GetValue(environmentVariableName); + _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); + return value; + } + public bool CurrentPowerToGridInvertValue() { var environmentVariableName = "CurrentPowerToGridInvertValue"; @@ -102,7 +216,7 @@ public bool CurrentPowerToGridInvertValue() public string TeslaMateApiBaseUrl() { var environmentVariableName = "TeslaMateApiBaseUrl"; - var value = GetNotNullableConfigurationValue(environmentVariableName); + var value = GetNotNullableConfigurationValue(environmentVariableName); _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); return value; } @@ -110,7 +224,7 @@ public string TeslaMateApiBaseUrl() public List CarPriorities() { var environmentVariableName = "CarPriorities"; - var rawValue = GetNotNullableConfigurationValue(environmentVariableName); + var rawValue = GetNotNullableConfigurationValue(environmentVariableName); var value = rawValue.Split("|").Select(id => Convert.ToInt32(id)).ToList(); _logger.LogDebug("Config value extracted: [{key}]: {@value}", environmentVariableName, value); return value; @@ -119,7 +233,7 @@ public List CarPriorities() public string GeoFence() { var environmentVariableName = "GeoFence"; - var value = GetNotNullableConfigurationValue(environmentVariableName); + var value = GetNotNullableConfigurationValue(environmentVariableName); _logger.LogDebug("Config value extracted: [{key}]: {value}", environmentVariableName, value); return value; } @@ -166,10 +280,10 @@ public int PowerBuffer() return value; } - internal string GetNotNullableConfigurationValue(string environmentVariableName) + internal T GetNotNullableConfigurationValue(string environmentVariableName) { - var value = GetNullableConfigurationValue(environmentVariableName); - if (string.IsNullOrEmpty(value)) + var value = GetNullableConfigurationValue(environmentVariableName); + if (value == null) { var exception = new NullReferenceException($"Configuration value {environmentVariableName} is null or empty"); @@ -180,9 +294,9 @@ internal string GetNotNullableConfigurationValue(string environmentVariableName) return value; } - internal string? GetNullableConfigurationValue(string environmentVariableName) + internal T? GetNullableConfigurationValue(string environmentVariableName) { - return _configuration.GetValue(environmentVariableName); + return _configuration.GetValue(environmentVariableName); } internal TimeSpan GetSecondsConfigurationValueIfGreaterThanMinumum(string environmentVariableName, TimeSpan minimum)