diff --git a/OCPP.Core.Management/Controllers/HomeController.ChargePoint.cs b/OCPP.Core.Management/Controllers/HomeController.ChargePoint.cs index c4c2fc97..6f29c0e0 100644 --- a/OCPP.Core.Management/Controllers/HomeController.ChargePoint.cs +++ b/OCPP.Core.Management/Controllers/HomeController.ChargePoint.cs @@ -20,10 +20,14 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using Azure.Core.Pipeline; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Logging; +using Newtonsoft.Json; using OCPP.Core.Database; using OCPP.Core.Management.Models; @@ -32,7 +36,7 @@ namespace OCPP.Core.Management.Controllers public partial class HomeController : BaseController { [Authorize] - public IActionResult ChargePoint(string Id, ChargePointViewModel cpvm) + public async Task ChargePoint(string Id, ChargePointViewModel cpvm) { try { @@ -186,6 +190,59 @@ public IActionResult ChargePoint(string Id, ChargePointViewModel cpvm) cpvm.Username = currentChargePoint.Username; cpvm.Password = currentChargePoint.Password; cpvm.ClientCertThumb = currentChargePoint.ClientCertThumb; + + // Attempt to get configuration information from the charge point + try + { + string serverApiUrl = base.Config.GetValue("ServerApiUrl"); + string apiKeyConfig = base.Config.GetValue("ApiKey"); + if(!string.IsNullOrEmpty(serverApiUrl)) + { + using var httpClient = new System.Net.Http.HttpClient(); + if(!serverApiUrl.EndsWith('/')) + serverApiUrl += "/"; + + var uri = new Uri(serverApiUrl + "GetConfiguration/" + Id); + + // API-Key authentication? + if (!string.IsNullOrWhiteSpace(apiKeyConfig)) + { + httpClient.DefaultRequestHeaders.Add("X-API-Key", apiKeyConfig); + } + else + { + Logger.LogWarning("GetConfiguration: No API-Key configured!"); + } + + var response = await httpClient.GetAsync(uri); + if (response.IsSuccessStatusCode) + { + string jsonResult = await response.Content.ReadAsStringAsync(); + if(!string.IsNullOrEmpty(jsonResult)) + { + dynamic jsonObject = JsonConvert.DeserializeObject(jsonResult); + Logger.LogInformation("GetConfiguration: Result of API request is '{0}'", jsonResult); + foreach(var kv in jsonObject.configurationKey) + { + Logger.LogTrace("GetConfiguration: {0}:{1} ({2})", (string)kv.key, (string)kv.value, (bool)kv.@readonly); + cpvm.DeviceConfiguration.Add((string)kv.key, ((string)kv.value, (bool) kv.@readonly)); + } + } + else + { + Logger.LogError("GetConfiguration: Result is empty"); + } + } + else + { + Logger.LogError("GetConfiguration: Error received {0}: {1}", response.StatusCode, await response.Content.ReadAsStringAsync()); + } + } + } + catch(Exception exp) + { + Logger.LogError(exp, "GetConfiguration call failed."); + } } string viewName = (!string.IsNullOrEmpty(cpvm.ChargePointId) || Id == "@") ? "ChargePointDetail" : "ChargePointList"; diff --git a/OCPP.Core.Management/Models/ChargePointViewModel.cs b/OCPP.Core.Management/Models/ChargePointViewModel.cs index 0c8b292b..c54d198e 100644 --- a/OCPP.Core.Management/Models/ChargePointViewModel.cs +++ b/OCPP.Core.Management/Models/ChargePointViewModel.cs @@ -50,5 +50,7 @@ public class ChargePointViewModel [StringLength(100)] public string ClientCertThumb { get; set; } + + public Dictionary DeviceConfiguration { get; set; } = new Dictionary(); } } diff --git a/OCPP.Core.Management/Resources/Views.Home.ChargePointDetail.de.resx b/OCPP.Core.Management/Resources/Views.Home.ChargePointDetail.de.resx index 87a25b56..154ba38b 100644 --- a/OCPP.Core.Management/Resources/Views.Home.ChargePointDetail.de.resx +++ b/OCPP.Core.Management/Resources/Views.Home.ChargePointDetail.de.resx @@ -186,6 +186,9 @@ Management + + Aktuelle Gerätekonfiguration + Neustart diff --git a/OCPP.Core.Management/Resources/Views.Home.ChargePointDetail.resx b/OCPP.Core.Management/Resources/Views.Home.ChargePointDetail.resx index a02d417f..074be5b4 100644 --- a/OCPP.Core.Management/Resources/Views.Home.ChargePointDetail.resx +++ b/OCPP.Core.Management/Resources/Views.Home.ChargePointDetail.resx @@ -186,6 +186,9 @@ Management + + Current Device Configuration + Restart diff --git a/OCPP.Core.Management/Views/Home/ChargePointDetail.cshtml b/OCPP.Core.Management/Views/Home/ChargePointDetail.cshtml index 2ed073aa..2855b6da 100644 --- a/OCPP.Core.Management/Views/Home/ChargePointDetail.cshtml +++ b/OCPP.Core.Management/Views/Home/ChargePointDetail.cshtml @@ -167,7 +167,28 @@ } +
+ @if(Model.DeviceConfiguration?.Count > 0) + { +

@Localizer["TitleDeviceConfiguration"]

+
+
+
+
Key
+
Value
+
Writable
+
+ @foreach(var config in Model.DeviceConfiguration) + { +
+
@config.Key
+
@config.Value.Item1
+
+
+ } +
+ } @section scripts { @if (!string.IsNullOrWhiteSpace(Model.ChargePointId)) diff --git a/OCPP.Core.Server/ControllerOCPP16.GetConfiguration.cs b/OCPP.Core.Server/ControllerOCPP16.GetConfiguration.cs new file mode 100644 index 00000000..af29e568 --- /dev/null +++ b/OCPP.Core.Server/ControllerOCPP16.GetConfiguration.cs @@ -0,0 +1,30 @@ +using Microsoft.Extensions.Logging; +using OCPP.Core.Server.Messages_OCPP16; +using System; + +namespace OCPP.Core.Server +{ + public partial class ControllerOCPP16 + { + public void HandleGetConfiguration(OCPPMessage msgIn, OCPPMessage msgOut) + { + Logger.LogInformation("GetConfiguration answer: ChargePointId={0} / MsgType={1} / ErrCode={2}", ChargePointStatus.Id, msgIn.MessageType, msgIn.ErrorCode); + + try + { + var getConfigurationResponse = DeserializeMessage(msgIn); + Logger.LogInformation("GetConfiguration => KeyCount: {0}", getConfigurationResponse.ConfigurationKey?.Count); + WriteMessageLog(ChargePointStatus?.Id, null, msgOut.Action, getConfigurationResponse.ConfigurationKey?.ToString(), msgIn.ErrorCode); + + if(msgOut.TaskCompletionSource != null) + { + msgOut.TaskCompletionSource.SetResult(msgIn.JsonPayload); + } + } + catch(Exception exp) + { + Logger.LogError(exp, "GetConfiguration => Exception: {0}", exp.Message); + } + } + } +} diff --git a/OCPP.Core.Server/ControllerOCPP16.cs b/OCPP.Core.Server/ControllerOCPP16.cs index f91b0534..82eaa11e 100644 --- a/OCPP.Core.Server/ControllerOCPP16.cs +++ b/OCPP.Core.Server/ControllerOCPP16.cs @@ -136,6 +136,10 @@ public void ProcessAnswer(OCPPMessage msgIn, OCPPMessage msgOut) HandleClearChargingProfile(msgIn, msgOut); break; + case "GetConfiguration": + HandleGetConfiguration(msgIn, msgOut); + break; + default: WriteMessageLog(ChargePointStatus.Id, null, msgIn.Action, msgIn.JsonPayload, "Unknown answer"); break; diff --git a/OCPP.Core.Server/Messages_OCPP16/GetConfigurationRequest.cs b/OCPP.Core.Server/Messages_OCPP16/GetConfigurationRequest.cs new file mode 100644 index 00000000..074f72c0 --- /dev/null +++ b/OCPP.Core.Server/Messages_OCPP16/GetConfigurationRequest.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace OCPP.Core.Server.Messages_OCPP16 +{ + public class GetConfigurationRequest + { + [JsonProperty("key")] + public ICollection Key { get; set; } + } +} diff --git a/OCPP.Core.Server/Messages_OCPP16/GetConfigurationResponse.cs b/OCPP.Core.Server/Messages_OCPP16/GetConfigurationResponse.cs new file mode 100644 index 00000000..5808b10d --- /dev/null +++ b/OCPP.Core.Server/Messages_OCPP16/GetConfigurationResponse.cs @@ -0,0 +1,20 @@ +using Newtonsoft.Json; +using System.Collections.Generic; + +namespace OCPP.Core.Server.Messages_OCPP16 +{ + public class GetConfigurationResponse + { + [JsonProperty("configurationKey")] + public ICollection ConfigurationKey { get; set; } + [JsonProperty("unknownKey")] + public ICollection UnknownKey { get; set; } + } + + public class OcppKeyValue + { + public string Key { get; set; } + public bool ReadOnly { get; set; } + public string Value { get; set; } + } +} diff --git a/OCPP.Core.Server/OCPPMiddleware.OCPP16.cs b/OCPP.Core.Server/OCPPMiddleware.OCPP16.cs index 2c8d9454..e7ca64e3 100644 --- a/OCPP.Core.Server/OCPPMiddleware.OCPP16.cs +++ b/OCPP.Core.Server/OCPPMiddleware.OCPP16.cs @@ -340,6 +340,43 @@ private async Task ClearChargingProfile16(ChargePointStatus chargePointStatus, H await apiCallerContext.Response.WriteAsync(apiResult); } + /// + /// Sends a GetConfiguration message to the chargepoint + /// + private async Task GetConfiguration16(ChargePointStatus chargePointStatus, HttpContext apiCallerContext, OCPPCoreContext dbContext, string urlConnectorId) + { + ILogger logger = _logFactory.CreateLogger("OCPPMiddleware.OCPP16"); + ControllerOCPP16 controller16 = new ControllerOCPP16(_configuration, _logFactory, chargePointStatus, dbContext); + + // By default, don't specify any keys, if a reduced set is required then specify keys in object. + var getConfigRequest = new GetConfigurationRequest(); + + logger.LogTrace("OCPPMiddleware.OCPP16 => GetConfiguration16: ChargePoint='{0}'", chargePointStatus.Id); + + string jsonGetConfigRequest = JsonConvert.SerializeObject(getConfigRequest); + + OCPPMessage msgOut = new OCPPMessage() + { + MessageType = "2", + Action = "GetConfiguration", + UniqueId = Guid.NewGuid().ToString("N"), + JsonPayload = JsonConvert.SerializeObject(getConfigRequest), + TaskCompletionSource = new TaskCompletionSource() + }; + + _requestQueue.Add(msgOut.UniqueId, msgOut); + + await SendOcpp16Message(msgOut, logger, chargePointStatus); + + string apiResult = await msgOut.TaskCompletionSource.Task; + + logger.LogTrace("OCPPMiddleware.OCPP16 => GetConfiguration16: Response='{0}'", apiResult); + + apiCallerContext.Response.StatusCode = 200; + apiCallerContext.Response.ContentType = "application/json"; + await apiCallerContext.Response.WriteAsync(apiResult); + } + private async Task SendOcpp16Message(OCPPMessage msg, ILogger logger, ChargePointStatus chargePointStatus) { // Send raw outgoing messages to extensions diff --git a/OCPP.Core.Server/OCPPMiddleware.cs b/OCPP.Core.Server/OCPPMiddleware.cs index f99ab2b6..ee3b8bfb 100644 --- a/OCPP.Core.Server/OCPPMiddleware.cs +++ b/OCPP.Core.Server/OCPPMiddleware.cs @@ -33,6 +33,7 @@ using System.Net.WebSockets; using System.Reflection; using System.Security.Cryptography.X509Certificates; +using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; @@ -565,6 +566,46 @@ public async Task Invoke(HttpContext context, OCPPCoreContext dbContext) context.Response.StatusCode = (int)HttpStatusCode.BadRequest; } } + else if (cmd == "GetConfiguration") + { + if(!string.IsNullOrEmpty(urlChargePointId)) + { + try + { + ChargePointStatus status = null; + if(_chargePointStatusDict.TryGetValue(urlChargePointId, out status)) + { + if(status.Protocol == Protocol_OCPP16) + { + // OCPP V1.6 + await GetConfiguration16(status, context, dbContext, urlConnectorId); + } + else + { + throw new NotImplementedException("GetConfiguration not yet implemented for OCPP 2.0"); + } + } + else + { + // Chargepoint offline + _logger.LogError("OCPPMiddleware GetConfiguration => Chargepoint offline: {0}", urlChargePointId); + context.Response.StatusCode = (int)HttpStatusCode.NotFound; + } + } + catch(Exception exp) + { + _logger.LogError(exp, "OCPPMiddleware GetConfiguration => Error: {0}", exp.Message); + context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; + var msg = Encoding.UTF8.GetBytes(exp.Message); + await context.Response.Body.WriteAsync(msg, 0, msg.Length); + } + } + else + { + _logger.LogError("OCPPMiddleware GetConfiguration => Missing chargepoint ID"); + context.Response.StatusCode = (int)HttpStatusCode.BadRequest; + } + } else { // Unknown action/function