chore: added smappee api
This commit is contained in:
36
smappee-api/ChargerOptions.cs
Normal file
36
smappee-api/ChargerOptions.cs
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
|
||||||
|
namespace options;
|
||||||
|
|
||||||
|
internal class ChargerOptions
|
||||||
|
{
|
||||||
|
public const string Name = "CHARGER";
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[ConfigurationKeyName("URL")]
|
||||||
|
public required Uri Url { get; init; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[ConfigurationKeyName("CLIENT_ID")]
|
||||||
|
public required string ClientId { get; init; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[ConfigurationKeyName("CLIENT_SECRET")]
|
||||||
|
public required string ClientSecret { get; init; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[ConfigurationKeyName("USERNAME")]
|
||||||
|
public required string UserName { get; init; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[ConfigurationKeyName("PASSWORD")]
|
||||||
|
public required string Password { get; init; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[ConfigurationKeyName("STATION")]
|
||||||
|
public required string ChargingStation { get; init; }
|
||||||
|
|
||||||
|
[Required]
|
||||||
|
[ConfigurationKeyName("CONNECTOR")]
|
||||||
|
public required int Connector { get; init; }
|
||||||
|
}
|
||||||
43
smappee-api/Program.cs
Normal file
43
smappee-api/Program.cs
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
using enums;
|
||||||
|
using interfaces;
|
||||||
|
using options;
|
||||||
|
using services;
|
||||||
|
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
builder.Services.AddHttpClient();
|
||||||
|
builder.Services.AddRedaction();
|
||||||
|
builder.Services.AddExtendedHttpClientLogging(
|
||||||
|
builder.Configuration.GetSection("HttpClientLogging"));
|
||||||
|
|
||||||
|
builder
|
||||||
|
.Services.AddOptions<ChargerOptions>()
|
||||||
|
.Bind(builder.Configuration.GetSection(ChargerOptions.Name))
|
||||||
|
.ValidateDataAnnotations();
|
||||||
|
builder.Services.AddSingleton<IBearerTokenService, SmappeeTokenService>();
|
||||||
|
builder.Services.AddScoped<IChargerService, SmappeeChargerService>();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
|
||||||
|
app.MapPost(
|
||||||
|
"/enable",
|
||||||
|
async (IChargerService chargerService) =>
|
||||||
|
{
|
||||||
|
await chargerService.ExecuteRequest(ChargerRequests.Enable);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.WithName("Enable");
|
||||||
|
|
||||||
|
app.MapPost(
|
||||||
|
"/disable",
|
||||||
|
() =>
|
||||||
|
async (IChargerService chargerService) =>
|
||||||
|
{
|
||||||
|
await chargerService.ExecuteRequest(ChargerRequests.Disable);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.WithName("Disable");
|
||||||
|
|
||||||
|
app.Run();
|
||||||
23
smappee-api/Properties/launchSettings.json
Normal file
23
smappee-api/Properties/launchSettings.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||||
|
"profiles": {
|
||||||
|
"http": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "http://localhost:5057",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"https": {
|
||||||
|
"commandName": "Project",
|
||||||
|
"dotnetRunMessages": true,
|
||||||
|
"launchBrowser": false,
|
||||||
|
"applicationUrl": "https://localhost:7028;http://localhost:5057",
|
||||||
|
"environmentVariables": {
|
||||||
|
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
smappee-api/appsettings.Development.json
Normal file
32
smappee-api/appsettings.Development.json
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Information"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"CHARGER": {
|
||||||
|
"URL": "https://app1pub.smappee.net",
|
||||||
|
"CLIENT_ID": "5700002178",
|
||||||
|
"CLIENT_SECRET": "cPcRKc26ul",
|
||||||
|
"USERNAME": "serruysw@gmail.com",
|
||||||
|
"PASSWORD": "uAfUhnBXkZeqs$@7ca7P3M9QE6z0Dg1N0cbz&",
|
||||||
|
"STATION": "6230003897",
|
||||||
|
"CONNECTOR": 1
|
||||||
|
},
|
||||||
|
"HttpClientLogging": {
|
||||||
|
"LogRequestStart": false,
|
||||||
|
"LogBody": true,
|
||||||
|
"BodySizeLimit": 32768,
|
||||||
|
"BodyReadTimeout": "00:00:01",
|
||||||
|
"RequestHeadersDataClasses": {
|
||||||
|
"User-Agent": "None",
|
||||||
|
"Content-Type": "None"
|
||||||
|
},
|
||||||
|
"ResponseHeadersDataClasses": {
|
||||||
|
"Content-Type": "None"
|
||||||
|
},
|
||||||
|
"RequestPathLoggingMode": "Formatted",
|
||||||
|
"RequestPathParameterRedactionMode": "Strict"
|
||||||
|
}
|
||||||
|
}
|
||||||
9
smappee-api/appsettings.json
Normal file
9
smappee-api/appsettings.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"Logging": {
|
||||||
|
"LogLevel": {
|
||||||
|
"Default": "Information",
|
||||||
|
"Microsoft.AspNetCore": "Warning"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"AllowedHosts": "*"
|
||||||
|
}
|
||||||
7
smappee-api/enums/ChargerRequests.cs
Normal file
7
smappee-api/enums/ChargerRequests.cs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
namespace enums;
|
||||||
|
|
||||||
|
internal enum ChargerRequests
|
||||||
|
{
|
||||||
|
Enable,
|
||||||
|
Disable
|
||||||
|
}
|
||||||
5
smappee-api/interfaces/IBearerTokenService.cs
Normal file
5
smappee-api/interfaces/IBearerTokenService.cs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
namespace interfaces;
|
||||||
|
|
||||||
|
internal interface IBearerTokenService {
|
||||||
|
ValueTask<string> GetToken(bool forceNewToken);
|
||||||
|
}
|
||||||
8
smappee-api/interfaces/IChargerService.cs
Normal file
8
smappee-api/interfaces/IChargerService.cs
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
using enums;
|
||||||
|
|
||||||
|
namespace interfaces;
|
||||||
|
|
||||||
|
internal interface IChargerService
|
||||||
|
{
|
||||||
|
Task ExecuteRequest(ChargerRequests request);
|
||||||
|
}
|
||||||
51
smappee-api/services/SmappeeChargerService.cs
Normal file
51
smappee-api/services/SmappeeChargerService.cs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
using enums;
|
||||||
|
using interfaces;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using options;
|
||||||
|
|
||||||
|
namespace services;
|
||||||
|
|
||||||
|
internal class SmappeeChargerService : IChargerService
|
||||||
|
{
|
||||||
|
private readonly ChargerOptions options;
|
||||||
|
private readonly IBearerTokenService tokenService;
|
||||||
|
private readonly IHttpClientFactory httpClientFactory;
|
||||||
|
|
||||||
|
public SmappeeChargerService(
|
||||||
|
IOptions<ChargerOptions> options,
|
||||||
|
IBearerTokenService tokenService,
|
||||||
|
IHttpClientFactory httpClientFactory
|
||||||
|
)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(tokenService);
|
||||||
|
this.options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
this.tokenService = tokenService;
|
||||||
|
this.httpClientFactory =
|
||||||
|
httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ExecuteRequest(ChargerRequests request)
|
||||||
|
{
|
||||||
|
var token = await tokenService.GetToken(false);
|
||||||
|
var httpClient = httpClientFactory.CreateClient("smappee");
|
||||||
|
httpClient.BaseAddress = options.Url;
|
||||||
|
httpClient.DefaultRequestHeaders.Add("Authorization", $"Bearer {token}");
|
||||||
|
var response = request switch
|
||||||
|
{
|
||||||
|
ChargerRequests.Enable => await httpClient.PutAsJsonAsync<SmappeeRequestBody>(
|
||||||
|
$"/dev/v3/chargingstations/${options.ChargingStation}/connectors/{options.Connector}/mode",
|
||||||
|
new SmappeeRequestBody("NORMAL", new Limit("AMPERE", 6))
|
||||||
|
),
|
||||||
|
ChargerRequests.Disable => await httpClient.PutAsJsonAsync(
|
||||||
|
"/dev/v3/chargingstations",
|
||||||
|
new SmappeeRequestBody("PAUSED", null)
|
||||||
|
),
|
||||||
|
_ => throw new NotImplementedException(),
|
||||||
|
};
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
record SmappeeRequestBody(string mode, Limit? limit);
|
||||||
|
|
||||||
|
record Limit(string unit, int value);
|
||||||
|
}
|
||||||
73
smappee-api/services/SmappeeTokenService.cs
Normal file
73
smappee-api/services/SmappeeTokenService.cs
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
using interfaces;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using options;
|
||||||
|
|
||||||
|
namespace services;
|
||||||
|
|
||||||
|
internal class SmappeeTokenService : IBearerTokenService
|
||||||
|
{
|
||||||
|
private readonly ChargerOptions options;
|
||||||
|
private readonly IHttpClientFactory httpClientFactory;
|
||||||
|
|
||||||
|
private TokenData? tokenData = null;
|
||||||
|
|
||||||
|
public SmappeeTokenService(
|
||||||
|
IOptions<ChargerOptions> options,
|
||||||
|
IHttpClientFactory httpClientFactory
|
||||||
|
)
|
||||||
|
{
|
||||||
|
this.options = options?.Value ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
this.httpClientFactory =
|
||||||
|
httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask<string> GetToken(bool forceNewToken)
|
||||||
|
{
|
||||||
|
if (
|
||||||
|
tokenData == null
|
||||||
|
|| tokenData.Expiration < DateTime.UtcNow.AddSeconds(-10)
|
||||||
|
|| forceNewToken
|
||||||
|
)
|
||||||
|
{
|
||||||
|
tokenData = await RequestNewToken();
|
||||||
|
}
|
||||||
|
return tokenData.Token;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<TokenData> RequestNewToken()
|
||||||
|
{
|
||||||
|
var httpClient = httpClientFactory.CreateClient("smappeeTokenService");
|
||||||
|
httpClient.BaseAddress = options.Url;
|
||||||
|
|
||||||
|
var result = await httpClient.PostAsJsonAsync<TokenRequestBody>(
|
||||||
|
"/dev/v3/oauth2/token",
|
||||||
|
new TokenRequestBody(
|
||||||
|
"password",
|
||||||
|
options.ClientId,
|
||||||
|
options.ClientSecret,
|
||||||
|
options.UserName,
|
||||||
|
options.Password
|
||||||
|
)
|
||||||
|
);
|
||||||
|
result.EnsureSuccessStatusCode();
|
||||||
|
var tokenResponse =
|
||||||
|
await result.Content.ReadFromJsonAsync<TokenResponse>()
|
||||||
|
?? throw new InvalidOperationException("Token could not be fetched");
|
||||||
|
return new TokenData(
|
||||||
|
tokenResponse.access_token,
|
||||||
|
DateTime.UtcNow.AddSeconds(tokenResponse.expires_in)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
record TokenRequestBody(
|
||||||
|
string grant_type,
|
||||||
|
string client_id,
|
||||||
|
string client_secret,
|
||||||
|
string username,
|
||||||
|
string password
|
||||||
|
);
|
||||||
|
|
||||||
|
record TokenResponse(string access_token, int expires_in);
|
||||||
|
|
||||||
|
record TokenData(string Token, DateTime Expiration);
|
||||||
|
}
|
||||||
16
smappee-api/smappee-api.csproj
Normal file
16
smappee-api/smappee-api.csproj
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<RootNamespace>smappee_api</RootNamespace>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Compliance.Redaction" Version="10.9.0" />
|
||||||
|
<PackageReference Include="Microsoft.Extensions.Http.Diagnostics" Version="10.9.0" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
6
smappee-api/smappee-api.http
Normal file
6
smappee-api/smappee-api.http
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@smappee_api_HostAddress = http://localhost:5057
|
||||||
|
|
||||||
|
GET {{smappee_api_HostAddress}}/weatherforecast/
|
||||||
|
Accept: application/json
|
||||||
|
|
||||||
|
###
|
||||||
Reference in New Issue
Block a user