Add Data Collector App

This commit is contained in:
Noe 2025-09-01 13:07:26 +02:00
parent 076dcda4a2
commit 203908152c
4 changed files with 311 additions and 0 deletions

View File

@ -0,0 +1,271 @@
using System.Text;
using Microsoft.AspNetCore.Mvc;
namespace DataCollectorWebApp;
public class LoginResponse
{
public string Token { get; set; }
public object User { get; set; } // or a User class if needed
public bool AccessToSalimax { get; set; }
public bool AccessToSalidomo { get; set; }
public bool AccessToSodiohome { get; set; }
public bool AccessToSodistoreMax { get; set; }
}
public class Installation
{
//Each installation has 2 roles, a read role and a write role.
//There are 2 keys per role a public key and a secret
//Product can be 0 or 1, 0 for Salimax, 1 for Salidomo
public String Name { get; set; }
public String Location { get; set; }
public String Region { get; set; } = "";
public String Country { get; set; } = "";
public String VpnIp { get; set; } = "";
public String InstallationName { get; set; } = "";
public String S3Region { get; set; } = "sos-ch-dk-2";
public String S3Provider { get; set; } = "exo.io";
public String S3WriteKey { get; set; } = "";
public String S3Key { get; set; } = "";
public String S3WriteSecret { get; set; } = "";
public String S3Secret { get; set; } = "";
public int S3BucketId { get; set; } = 0;
public String ReadRoleId { get; set; } = "";
public String WriteRoleId { get; set; } = "";
public Boolean TestingMode { get; set; } = false;
public int Status { get; set; } = -1;
public int Product { get; set; } = 0;
public int Device { get; set; } = 0;
public string SerialNumber { get; set; } = "";
public String OrderNumbers { get; set; }
public String VrmLink { get; set; } = "";
}
[Controller]
public class InstallationsController : Controller
{
[HttpGet]
[Route("/Installations")]
[Produces("text/html")]
public async Task<IActionResult> Index()
{
const string HtmlHeader = @"
<html>
<head>
<title>Inesco Energy Installations Overview</title>
<style>
body {
font-family: sans-serif;
margin: 2rem;
}
table {
border-collapse: collapse;
width: 100%;
font-size: 0.9rem;
}
th, td {
padding: 0.75rem;
border: 1px solid rgb(200, 200, 200);
text-align: left;
}
thead th {
background-color: #f0f0f0;
font-weight: bold;
border-bottom: 2px solid #EB9486;
}
tbody tr:nth-child(odd) {
background-color: #f9f9f9;
}
a {
color: #0645AD;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
.status-circle {
display: inline-block;
width: 15px;
height: 15px;
border-radius: 50%;
margin-left: 50%;
}
.status-0 { background-color: #4CAF50; } /* Green */
.status-1 { background-color: #FF9800; } /* Orange */
.status-2 { background-color: #F44336; } /* Red */
.centered {
text-align: center;
vertical-align: middle;
}
.offline-symbol {
font-size: 1.2rem;
color: #F44336; /* Gray */
display: inline-block;
line-height: 1;
width: 15px;
height: 15px;
border-radius: 50%;
margin-left: 50%;
}
.status-cell {
text-align: center; /* center horizontally */
vertical-align: middle; /* center vertically */
}
</style>
</head>
<body>
<h1>Installation Overview</h1>
<table>
<thead>
<tr>
<th class=""centered"">Name</th>
<th class=""centered"">Product</th>
<th class=""centered"">Location</th>
<th class=""centered"">VPN IP</th>
<th class=""centered"">Status</th>
</tr>
</thead>
<tbody>
";
const string HtmlFooter = @"
</tbody>
</table>
</body>
</html>
";
string GetProductName(int productId)
{
return productId switch
{
0 => "Salimax",
1 => "Salidomo",
2 => "SodioHome",
3 => "SodistoreMax",
};
}
string GetStatusHtml(int status)
{
if (status == -1)
{
return "<span class='offline-symbol' title='Offline'>&times;</span>";
}
var statusClass = $"status-{status}";
var title = status switch
{
0 => "Online",
1 => "Warning",
2 => "Error",
_ => "Unknown"
};
return $"<span class='status-circle {statusClass}' title='{title}'></span>";
}
string BuildRowHtml(Installation i) => $@"
<tr>
<td>{i.Name}</td>
<td>{GetProductName(i.Product)}</td>
<td>{i.Location}</td>
<td>{i.VpnIp}</td>
<td>{GetStatusHtml(i.Status)}</td>
</tr>
";
var installations = await FetchInstallationsFromApi();
var sb = new StringBuilder();
sb.Append(HtmlHeader);
foreach (var i in installations)
{
sb.Append(BuildRowHtml(i));
}
sb.Append(HtmlFooter);
return Content(sb.ToString(), "text/html");
}
public async Task<List<Installation>?> FetchInstallationsFromApi()
{
var username = "baumgartner@innov.energy";
var password = "1234";
using var http = new HttpClient { BaseAddress = new Uri("https://monitor.inesco.energy/api/") };
// Step 1: Login
var loginResponse = await http.PostAsync($"Login?username={username}&password={password}", null);
if (!loginResponse.IsSuccessStatusCode)
{
Console.WriteLine("Login failed with status code {StatusCode}", loginResponse.StatusCode);
return null;
}
var loginData = await loginResponse.Content.ReadFromJsonAsync<LoginResponse>();
if (loginData?.Token is null)
{
Console.WriteLine("Login succeeded but token was missing");
return null;
}
var token = loginData.Token;
Console.WriteLine($"Token: {token}");
var installations = new List<Installation>();
var getInstallationsRequestResponse = await http.GetAsync($"GetAllInstallationsFromProduct?product=0&authToken={token}");
var newInstallations= await getInstallationsRequestResponse.Content.ReadFromJsonAsync<List<Installation>>();
if (newInstallations != null)
{
installations.AddRange(newInstallations);
}
getInstallationsRequestResponse = await http.GetAsync($"GetAllInstallationsFromProduct?product=1&authToken={token}");
newInstallations= await getInstallationsRequestResponse.Content.ReadFromJsonAsync<List<Installation>>();
if (newInstallations != null)
{
installations.AddRange(newInstallations);
}
getInstallationsRequestResponse = await http.GetAsync($"GetAllInstallationsFromProduct?product=2&authToken={token}");
newInstallations= await getInstallationsRequestResponse.Content.ReadFromJsonAsync<List<Installation>>();
if (newInstallations != null)
{
installations.AddRange(newInstallations);
}
getInstallationsRequestResponse = await http.GetAsync($"GetAllInstallationsFromProduct?product=3&authToken={token}");
newInstallations= await getInstallationsRequestResponse.Content.ReadFromJsonAsync<List<Installation>>();
if (newInstallations != null)
{
installations.AddRange(newInstallations);
}
//Console.WriteLine("Installations retrieved ",installations);
return installations;
}
}

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.OpenApi" Version="1.6.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,19 @@
using Microsoft.AspNetCore.Builder;
namespace InnovEnergy.App.DataCollectorWebApp;
public static class Program
{
public static async Task Main(string[] args)
{
Console.WriteLine("Starting DataCollectorWebApp");
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var app = builder.Build();
app.MapControllers();
await app.RunAsync();
}
}

View File

@ -103,6 +103,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrowattCommunication", "App
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WITGrowatt4-15K", "Lib\Devices\WITGrowatt4-15K\WITGrowatt4-15K.csproj", "{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WITGrowatt4-15K", "Lib\Devices\WITGrowatt4-15K\WITGrowatt4-15K.csproj", "{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataCollectorWebApp", "DataCollectorWebApp\DataCollectorWebApp.csproj", "{6069D487-DBAB-4253-BFA1-CF994B84BE49}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
@ -274,6 +276,10 @@ Global
{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Debug|Any CPU.Build.0 = Debug|Any CPU {44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Debug|Any CPU.Build.0 = Debug|Any CPU
{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Release|Any CPU.ActiveCfg = Release|Any CPU {44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Release|Any CPU.ActiveCfg = Release|Any CPU
{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Release|Any CPU.Build.0 = Release|Any CPU {44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Release|Any CPU.Build.0 = Release|Any CPU
{6069D487-DBAB-4253-BFA1-CF994B84BE49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6069D487-DBAB-4253-BFA1-CF994B84BE49}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6069D487-DBAB-4253-BFA1-CF994B84BE49}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6069D487-DBAB-4253-BFA1-CF994B84BE49}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(NestedProjects) = preSolution GlobalSection(NestedProjects) = preSolution
{CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A} {CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A}
@ -321,5 +327,6 @@ Global
{39B83793-49DB-4940-9C25-A7F944607407} = {145597B4-3E30-45E6-9F72-4DD43194539A} {39B83793-49DB-4940-9C25-A7F944607407} = {145597B4-3E30-45E6-9F72-4DD43194539A}
{DC0BE34A-368F-46DC-A081-70C9A1EFE9C0} = {145597B4-3E30-45E6-9F72-4DD43194539A} {DC0BE34A-368F-46DC-A081-70C9A1EFE9C0} = {145597B4-3E30-45E6-9F72-4DD43194539A}
{44DD9E5E-2AD3-4579-A47D-7A40FD28D369} = {4931A385-24DC-4E78-BFF4-356F8D6D5183} {44DD9E5E-2AD3-4579-A47D-7A40FD28D369} = {4931A385-24DC-4E78-BFF4-356F8D6D5183}
{6069D487-DBAB-4253-BFA1-CF994B84BE49} = {145597B4-3E30-45E6-9F72-4DD43194539A}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal