This commit is contained in:
Sina Blattmann 2023-06-19 17:24:19 +02:00
commit b649abb7f8
20 changed files with 542 additions and 129 deletions

Binary file not shown.

View File

@ -7,7 +7,7 @@ public static class Ssh
{ {
const String ConfigFile = "/data/innovenergy/openvpn/installation-name"; const String ConfigFile = "/data/innovenergy/openvpn/installation-name";
public static Task<Int32> Interactive(String host, String installationName, String user, String port) public static Task<Int32> Interactive(String host, String? installationName, String user, String port)
{ {
var fixInstallationName = $"echo '{installationName}' > {ConfigFile}; exec bash -l"; var fixInstallationName = $"echo '{installationName}' > {ConfigFile}; exec bash -l";

View File

@ -1,89 +1,156 @@
using System.Web; using CliWrap;
using CliWrap.Buffered;
using HandlebarsDotNet; using HandlebarsDotNet;
using InnovEnergy.App.RemoteSupportConsole;
using InnovEnergy.App.VrmGrabber.Database; using InnovEnergy.App.VrmGrabber.Database;
using InnovEnergy.Lib.Victron.VictronVRM; using InnovEnergy.App.VrmGrabber.DataTypes;
using InnovEnergy.Lib.Utils;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using FILE=System.IO.File; using FILE=System.IO.File;
using VrmInstallation = InnovEnergy.Lib.Victron.VictronVRM.Installation;
namespace InnovEnergy.App.VrmGrabber; namespace InnovEnergy.App.VrmGrabber;
public record Install( public record InstallationToHtmlInterface(
String Name, String Name,
String Ip, String Ip,
UInt64 Vrm, Int64 Vrm,
String Identifier, String Identifier,
String Serial, String Serial,
String EscapedName String EscapedName,
String Online,
String LastSeen,
String NumBatteries,
String BatteryVersion,
String ServerIp = "10.2.0.1", //TODO MAKE ME DYNAMIC
String FirmwareVersion = "AF09" //Todo automatically grab newest version?
); );
[Controller] [Controller]
public class Controller : ControllerBase public class Controller : ControllerBase
{ {
//Todo automatically grab newest version?
private const String FirmwareVersion = "AF09";
[HttpGet] [HttpGet]
[Route("/")] [Route("/")]
[Produces("text/html")] [Produces("text/html")]
public ActionResult Index() public ActionResult Index()
{ {
var instList = Db.InstallationsAndDetails.Keys.ToList(); const String source = @"<head>
if (instList.Count == 0) return new ContentResult <style>
tbody {
background-color: #e4f0f5;
}
tbody tr:nth-child(odd) {
background-color: #ECE9E9;
}
th, td { /* cell */
padding: 0.75rem;
font-size: 0.9375rem;
}
th { /* header cell */
font-weight: 700;
text-align: left;
color: #272838;
border-bottom: 2px solid #EB9486;
position: sticky;
top: 0;
background-color: #F9F8F8;
}
table {
border-collapse: collapse;
width: 100%;
border: 2px solid rgb(200, 200, 200);
letter-spacing: 1px;
font-family: sans-serif;
font-size: 0.8rem;
position: absolute; top: 0; bottom: 0; left: 0; right: 0;
}
thead th {
border: 1px solid rgb(190, 190, 190);
padding: 5px 10px;
position: sticky;
position: -webkit-sticky;
top: 0px;
background: white;
z-index: 999;
}
td {
text-align: left;
}
#managerTable {
overflow: hidden;
}</style></head>
<div id='managerTable'>
<table>
<tbody>
<tr>
<th>Name</th>
<th>Gui</th>
<th>VRM</th>
<th>Grafana</th>
<th>Identifier</th>
<th>Last Seen</th>
<th>Serial</th>
<th>#Batteries</th>
<th>Firmware-Version</th>
<th>Update</th>
</tr>
{{#inst}}
{{> installations}}
{{/inst}}
</tbody>
</table>
<div id='managerTable'>";
const String partialSource = @"<tr><td>{{Name}}</td>
<td><a target='_blank' href=http://{{Ip}}>{{online}} {{Ip}}</a></td>
<td><a target='_blank' href=https://vrm.victronenergy.com/installation/{{Vrm}}/dashboard>VRM</a></td>
<td><a target='_blank' href='https://salidomo.innovenergy.ch/d/ENkNRQXmk/installation?refresh=5s&orgId=1&var-Installation={{EscapedName}}&kiosk=tv'>Grafana</a></td>
<td>{{Identifier}}</td>
<td>{{LastSeen}}</td>
<td>{{Serial}}</td>
<td>{{NumBatteries}}</td>
<td>{{BatteryVersion}}</td>
<td><a target='_blank' href=http://{{ServerIp}}/UpdateBatteryFirmware/{{Ip}}/{{NumBatteries}}>⬆️{{FirmwareVersion}}</a></td>
</tr>";
var installationsInDb = Db.Installations.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase).ToList();
if (installationsInDb.Count == 0) return new ContentResult
{ {
ContentType = "text/html", ContentType = "text/html",
Content = "<p>Please wait page is still loading</p>" Content = "<p>Please wait page is still loading</p>"
}; };
String source = @"<head>
<style>
tbody {
background-color: #e4f0f5;
}
table {
border-collapse: collapse;
border: 2px solid rgb(200, 200, 200);
letter-spacing: 1px;
font-family: sans-serif;
font-size: 0.8rem;
position: absolute; top: 0; bottom: 0; left: 0; right: 0;
}
td,
th {
border: 1px solid rgb(190, 190, 190);
padding: 5px 10px;
}
td {
text-align: left;
}</style></head>
<table>
<tbody>
{{#inst}}
{{> installations}}
{{/inst}}
</tbody>
</table>";
String partialSource =
@"<tr><td>{{Name}}</td>
<td><a target='_blank' href=http://{{Ip}}>{{Ip}}</a></td>
<td><a target='_blank' href=https://vrm.victronenergy.com/installation/{{Vrm}}/dashboard>VRM</a></td>
<td>{{Identifier}}</td>
<td>{{Serial}}</td>
<td><a target='_blank' href='https://salidomo.innovenergy.ch/d/ENkNRQXmk/installation?refresh=5s&orgId=1&var-Installation={{EscapedName}}&kiosk=tv'>Grafana</a></td></tr>";
Handlebars.RegisterTemplate("installations", partialSource); Handlebars.RegisterTemplate("installations", partialSource);
var template = Handlebars.Compile(source); var template = Handlebars.Compile(source);
var insts = instList.Select(i => var installsForHtml = installationsInDb.Select(i => new InstallationToHtmlInterface(
{ i.Name,
return new Install(i.Name, Ip(i), i.IdSite, i.Identifier, Serial(i), HttpUtility.UrlEncode(i.Name)); i.Ip,
}); i.Vrm,
i.Identifier,
i.Serial,
i.EscapedName,
i.Online,
DateTimeOffset.FromUnixTimeSeconds(Convert.ToInt64(i.LastSeen)).ToString(),
i.NumberOfBatteries,
i.BatteryFirmwareVersion));
var data = new var data = new
{ {
inst = insts inst = installsForHtml,
}; };
var result = template(data); var result = template(data);
@ -95,17 +162,68 @@ td {
}; };
} }
private String Ip(Installation installation)
{
return VpnInfo.LookUpIp(installation.Identifier, Serial(installation)).Result ?? "Unknown"; [HttpGet("updatebatteryfirmware/{installationIp}/{numberOfBatteries}")]
public async Task<String> UpdateBatteryFirmware(String installationIp, String numberOfBatteries)
{
//We need the DeviceName of the battery (ttyUSB?)
var pathToBattery = await Db.ExecuteBufferedAsyncCommandOnIp(installationIp, "dbus-send --system --dest=com.victronenergy.system --type=method_call --print-reply /ServiceMapping/com_victronenergy_battery_1 com.victronenergy.BusItem.GetText");
var split = pathToBattery.Split('"');
var split2 = pathToBattery.Split(' ');
if (split.Length < 2 || split2.Length < 1)
{
Console.WriteLine(pathToBattery + " Split failed ");
return "Update failed";
}
if (split[1] == "Failed" || split2[0] == "Error") return "Update failed";
await UpdateVrmTagsToNewFirmware(installationIp);
SendNewBatteryFirmware(installationIp);
for (var batteryId = 2; batteryId <= Int64.Parse(numberOfBatteries) + 1; batteryId++)
{
var batteryTtyName = split[1].Split(".").Last();
var localCommand = $"/opt/innovenergy/scripts/upload-bms-firmware {batteryTtyName} {batteryId} /opt/innovenergy/bms-firmware/{FirmwareVersion}.bin";
#pragma warning disable CS4014
Db.ExecuteBufferedAsyncCommandOnIp(installationIp, localCommand);
#pragma warning restore CS4014
// Console.WriteLine(remoteUpdateCommandResult);
}
return "Battery update is successfully initiated! You can close this page now.";
} }
private String Serial(Installation installation) private static async Task UpdateVrmTagsToNewFirmware(String installationIp)
{ {
return Db.InstallationsAndDetails[installation].MachineSerial() ?? "Unknown"; var vrmInstallation = await FindVrmInstallationByIp(installationIp);
var tags = await vrmInstallation.GetTags();
async void RemoveTag(String t) => await vrmInstallation.RemoveTags(t);
tags.Where(tag => tag.StartsWith("FM-"))
.Do(RemoveTag);
await vrmInstallation.AddTags("FM-" + FirmwareVersion);
} }
private static async Task<VrmInstallation> FindVrmInstallationByIp(String installationIp)
{
var installationId = Db.Installations.Where(i => i.Ip == installationIp).Select(i => i.Vrm).First();
var vrmAccount = await Db.GetVrmAccount();
return await vrmAccount.GetInstallation(installationId!);
}
private static void SendNewBatteryFirmware(String installationIp)
{
Cli.Wrap("scp")
.WithArguments($@"{FirmwareVersion}.bin")
.AppendArgument($@"root@{installationIp}:/opt/innovenergy/bms-firmware/{FirmwareVersion}.bin")
.WithValidation(CommandResultValidation.None).ExecuteBufferedAsync();
}
// [HttpGet(nameof(GetInstallation))] // [HttpGet(nameof(GetInstallation))]
// [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")] // [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
// public Object GetInstallation(UInt64 serialNumber) // public Object GetInstallation(UInt64 serialNumber)
@ -123,9 +241,3 @@ td {
// } // }
} }
// installation Name, ip (link uf gui), idSite (vrm link), identifier , machineserial (HQ...)

View File

@ -1,17 +1,42 @@
using InnovEnergy.Lib.Victron.VictronVRM; using InnovEnergy.Lib.Victron.VictronVRM;
using SQLite;
namespace InnovEnergy.App.VrmGrabber.DataTypes; namespace InnovEnergy.App.VrmGrabber.DataTypes;
public class Installation : TreeNode public class Installation
{ {
public Installation(String? argName, String? argIp, Int64 argVrm, String? argIdentifier, String? serial, String? urlEncode, String? online, String? lastSeen, String details, String numberOfBatteries, String batteryFirmwareVersion)
{
Name = argName;
Ip = argIp;
Vrm = argVrm;
Identifier = argIdentifier;
Serial = serial;
EscapedName = urlEncode;
Online = online;
LastSeen = lastSeen;
Details = details;
NumberOfBatteries = numberOfBatteries;
BatteryFirmwareVersion = batteryFirmwareVersion;
}
public Installation() public Installation()
{ {
} }
public String Name { get; set; } public String? Name { get; set;}
// Settings public String? Ip { get; set;}
public IReadOnlyList<Detail> Notes { get; set; } public Int64 Vrm { get; set;}
[PrimaryKey]
public String? Identifier { get; set;}
public String? Serial { get; set;}
public String? EscapedName { get; set;}
public String? Online { get; set;}
public String? LastSeen { get; set;}
public String? NumberOfBatteries { get; set;}
public String? BatteryFirmwareVersion { get; set;}
public String? Details { get; set; } //JSON
} }

View File

@ -1,5 +1,6 @@
using InnovEnergy.App.VrmGrabber.DataTypes; using InnovEnergy.App.VrmGrabber.DataTypes;
namespace InnovEnergy.App.VrmGrabber.Database; namespace InnovEnergy.App.VrmGrabber.Database;

View File

@ -1,62 +1,264 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Reactive.Concurrency; using System.Runtime.InteropServices;
using System.Reactive.Linq; using System.Web;
using System.Reactive.Threading.Tasks; using CliWrap;
using CliWrap.Buffered;
using InnovEnergy.App.RemoteSupportConsole;
using InnovEnergy.Lib.Utils; using InnovEnergy.Lib.Utils;
using InnovEnergy.Lib.Victron.VictronVRM; using InnovEnergy.Lib.Victron.VictronVRM;
using SQLite; using SQLite;
using static System.Text.Json.JsonSerializer; using static System.Text.Json.JsonSerializer;
using static InnovEnergy.App.VrmGrabber.Database.Systemd;
using Installation = InnovEnergy.App.VrmGrabber.DataTypes.Installation; using Installation = InnovEnergy.App.VrmGrabber.DataTypes.Installation;
using VrmInstallation = InnovEnergy.Lib.Victron.VictronVRM.Installation;
using FILE=System.IO.File;
namespace InnovEnergy.App.VrmGrabber.Database; namespace InnovEnergy.App.VrmGrabber.Database;
public static class Systemd
{
[DllImport("libsystemd.so.0", CharSet = CharSet.Unicode)]
public static extern Int32 sd_notify(Int32 unsetEnvironment, String state);
}
public class InstallationDetails
{
public InstallationDetails(String? ip, IReadOnlyList<Detail> details)
{
Details = details;
Ip = ip;
}
public IReadOnlyList<Detail>? Details { get; set; }
public String? Ip { get; set; }
}
public static partial class Db public static partial class Db
{ {
public static Dictionary<Lib.Victron.VictronVRM.Installation, IReadOnlyList<Detail>> InstallationsAndDetails; public static Dictionary<Installation, InstallationDetails> InstallationsAndDetails;
internal const String DbPath = "./db.sqlite";
private const String DbPath = "./db.sqlite";
private static SQLiteConnection Connection { get; } = new SQLiteConnection(DbPath); private static SQLiteConnection Connection { get; } = new SQLiteConnection(DbPath);
public static TableQuery<Installation> Installations => Connection.Table<Installation>();
public static TableQuery<Installation> Installations => Connection.Table<Installation>();
public static void Init() public static void Init()
{ {
// used to force static constructor // used to force static constructor
} }
static Db() static Db()
{ {
// on startup create/migrate tables // on startup create/migrate tables
Connection.RunInTransaction(() => Connection.RunInTransaction(() => { Connection.CreateTable<Installation>(); });
{ }
// Connection.CreateTable<Installation>();
});
InstallationsAndDetails = new Dictionary<Lib.Victron.VictronVRM.Installation, IReadOnlyList<Detail>>(); public static async Task UpdateDetailsAndInstallations()
{
Observable.Interval(TimeSpan.FromMinutes(5)) sd_notify(0, "READY=1");
.ObserveOn(TaskPoolScheduler.Default) do {
.SubscribeOn(TaskPoolScheduler.Default) await UpdateInstallationsAndDetailsFromVrm(0);
.StartWith(0) // Do it right away (on startup) }
.Select(UpdateInstallationsAndDetailsFromVrm) while (true) ;
.Select(t => t.ToObservable())
.Concat()
.Subscribe(d => InstallationsAndDetails = d, exception => exception.WriteLine());
} }
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")] [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
private static async Task<Dictionary<Lib.Victron.VictronVRM.Installation, IReadOnlyList<Detail>>> UpdateInstallationsAndDetailsFromVrm(Int64 _) private static async Task UpdateInstallationsAndDetailsFromVrm(Int32 _)
{
var user = await GetVrmAccount();
var readOnlyInstallations = await user.GetInstallations();
var installations = readOnlyInstallations.ToList();
installations.Shuffle();
foreach (var installation in installations)
{
Console.WriteLine(installation.Name);
sd_notify(0, "WATCHDOG=1");
var details = await GetInstallationDetails(installation);
var ip = Ip(details);
var updatedInstallation = new Installation(
installation.Name,
ip[0],
(Int64)installation.IdSite,
installation.Identifier,
details.Details.MachineSerial() ?? "Unknown",
HttpUtility.UrlEncode(installation.Name),
ip[1],
details.Details.Last().Json["timestamp"].ToString(),
Serialize(details.Details),
await NumberOfBatteries(ip[0], ip[1]),
await BatteryFirmwareVersion(ip[0], ip[1]));
if (ip[0] != "Unknown")
await UpdateInstallationName(installation, ip[0]);
if (GetInstallationByIdentifier(installation.Identifier) == null)
{
Create(updatedInstallation);
}
else
{
Update(updatedInstallation);
}
}
}
public static async Task<VrmAccount> GetVrmAccount()
{ {
var fileContent = await File.ReadAllTextAsync("./token.json"); var fileContent = await File.ReadAllTextAsync("./token.json");
var acc = Deserialize<AccToken>(fileContent); var acc = Deserialize<AccToken>(fileContent);
var user = VrmAccount.Token(acc.idUser, acc.token); var user = VrmAccount.Token(acc!.idUser, acc.token);
var installations = await user.GetInstallations(); return user;
return installations }
.Do(i=>i.Name.WriteLine())
.ToDictionary(i => i, i => i.GetDetails().Result); private static async Task<String> BatteryFirmwareVersion(String? ip, String? online)
{
if (ip is null or "Unknown" || online == "❌") return "Unknown";
var pathToBattery = await ExecuteBufferedAsyncCommandOnIp(ip, "dbus-send --system --dest=com.victronenergy.system --type=method_call --print-reply /ServiceMapping/com_victronenergy_battery_1 com.victronenergy.BusItem.GetText");
if (pathToBattery.Split('"')[1].StartsWith("Error")) return "Unknown";
var command = $"dbus-send --system --dest={pathToBattery.Split('"')[1]} --type=method_call --print-reply /FirmwareVersion com.victronenergy.BusItem.GetText";
var returnString = await ExecuteBufferedAsyncCommandOnIp(ip, command);
var returnStringShortened = returnString.Split('"')[1];
return returnStringShortened.Length > 5 ? "Unknown" : returnStringShortened;
}
private static async Task<String> NumberOfBatteries(String? ip, String? online)
{
if (ip is null or "Unknown" || online == "❌") return "Failed";
var pathToBattery = await ExecuteBufferedAsyncCommandOnIp(ip, "dbus-send --system --dest=com.victronenergy.system --type=method_call --print-reply /ServiceMapping/com_victronenergy_battery_1 com.victronenergy.BusItem.GetText");
if (pathToBattery.Split('"')[1].StartsWith("Error")) return "Failed";
var cmd = await ExecuteBufferedAsyncCommandOnIp(ip,$"dbus-send --system --dest={pathToBattery.Split('"')[1]} --type=method_call --print-reply /NbOfBatteries com.victronenergy.BusItem.GetText" );
var cmdResult = cmd.Split('"')[1];
return cmdResult; //No Batteries can be found
}
public static async Task<String> ExecuteBufferedAsyncCommandOnIp(String? ip, String command)
{
if (ip is null or "Unknown") return "Failed";
var cmd = await Cli.Wrap("ssh")
.WithArguments($@"root@{ip}")
.AppendArgument("-o StrictHostKeyChecking=no")
.AppendArgument(command)
.WithValidation(CommandResultValidation.None).ExecuteBufferedAsync();
return cmd.ExitCode == 0 ? cmd.StandardOutput : cmd.StandardError;
}
private static String?[] Ip(InstallationDetails details)
{
var online = "❌";
var lookup = details.Ip;
if (lookup == "Unknown")
{
var serial = details.Details.MachineSerial() ?? "Unknown";
if (serial != "Unknown" && FILE.Exists($@"/etc/openvpn/server/Salino/ccd/{serial}"))
lookup = FILE.ReadAllText($@"/etc/openvpn/server/Salino/ccd/{serial}").Split(' ')[1];
}
else
{
online = "✅";
}
return new[] { lookup, online };
}
private static async Task<InstallationDetails> GetInstallationDetails(VrmInstallation i)
{
await Task.Delay(1000);
try
{
var details = await i.GetDetails();
var ip = await VpnInfo.LookUpIp(i.Identifier, details.MachineSerial()) ?? "Unknown";
var installationDetails = new InstallationDetails(ip, details);
return installationDetails;
}
catch (Exception e)
{
Console.WriteLine(e);
}
return new InstallationDetails("Unknown", Array.Empty<Detail>());
}
private static async Task UpdateInstallationName(VrmInstallation installation, String? ip)
{
var oldNameInFileRequest = await Cli.Wrap("ssh")
.WithArguments($@"root@{ip}")
.AppendArgument("-o StrictHostKeyChecking=accept-new")
.AppendArgument("cat /data/innovenergy/openvpn/installation-name")
.WithValidation(CommandResultValidation.None).ExecuteBufferedAsync();
var oldNameInFileWithoutNewLine = oldNameInFileRequest.StandardOutput.TrimEnd();
if (oldNameInFileRequest.ExitCode == 0 && oldNameInFileWithoutNewLine != installation.Name)
{
var overwriteNameCommand = Cli.Wrap("ssh")
.WithArguments($@"root@{ip}")
.AppendArgument($"echo '{installation.Name}' > /data/innovenergy/openvpn/installation-name");
var overwriteNameResponse = await overwriteNameCommand
.WithValidation(CommandResultValidation.None).ExecuteBufferedAsync();
if (overwriteNameResponse.ExitCode != 0)
{
Console.WriteLine(overwriteNameResponse.StandardError);
Console.WriteLine("Renaming did not work");
}
else
{
var rebootAfterRename = await Cli.Wrap("ssh")
.WithArguments($@"root@{ip}")
.AppendArgument("reboot")
.WithValidation(CommandResultValidation.None).ExecuteBufferedAsync();
if (rebootAfterRename.ExitCode != 0)
{
Console.WriteLine(overwriteNameResponse.StandardError);
Console.WriteLine("Rebooting did not work");
}
else
{
Console.WriteLine($"Renamed and rebooted {installation.Name}");
}
}
}
}
private static Boolean RunTransaction(Func<Boolean> func)
{
var savepoint = Connection.SaveTransactionPoint();
var success = false;
try
{
success = func();
}
finally
{
if (success)
Connection.Release(savepoint);
else
Connection.RollbackTo(savepoint);
}
return success;
} }
} }
@ -65,3 +267,9 @@ public class AccToken
public UInt64 idUser { get; init;} public UInt64 idUser { get; init;}
public String token { get; init;} public String token { get; init;}
} }
/*
dbus-send --system --dest=com.victronenergy.battery.ttyUSB1 --print-reply /FirmwareVersion \
org.freedesktop.DBus.Properties.Get string:com.victronenergy.battery.ttyUSB1
*/

View File

@ -1,13 +1,18 @@
using InnovEnergy.App.VrmGrabber.DataTypes; using InnovEnergy.App.VrmGrabber.DataTypes;
namespace InnovEnergy.App.VrmGrabber.Database; namespace InnovEnergy.App.VrmGrabber.Database;
public static partial class Db public static partial class Db
{ {
public static Boolean Delete(Installation installation) public static Boolean Delete(Installation installation)
{ {
return Installations.Delete(i => i.Id == installation.Id) > 0; return RunTransaction(DeleteInstallationAndItsDependencies);
Boolean DeleteInstallationAndItsDependencies()
{
return Installations.Delete(i => i.Identifier == installation.Identifier) > 0;
}
} }
} }

View File

@ -6,9 +6,9 @@ namespace InnovEnergy.App.VrmGrabber.Database;
public static partial class Db public static partial class Db
{ {
public static Installation? GetInstallationById(Int64? id) public static Installation? GetInstallationByIdentifier(String? identifier)
{ {
return Installations return Installations
.FirstOrDefault(i => i.Id == id); .FirstOrDefault(i => i.Identifier == identifier);
} }
} }

View File

@ -0,0 +1,13 @@
using InnovEnergy.App.VrmGrabber.DataTypes;
namespace InnovEnergy.App.VrmGrabber.Database;
public static partial class Db
{
public static Boolean Update(Installation installation)
{
return Connection.Update(installation) > 0;
}
}

View File

@ -5,26 +5,26 @@ namespace InnovEnergy.App.VrmGrabber;
public static class Program public static class Program
{ {
public static void Main(String[] args) public static async Task Main(String[] args)
{ {
var updateTask = Db.UpdateDetailsAndInstallations();
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers(); builder.Services.AddControllers();
builder.Services.AddSwaggerGen(c => // builder.Services.AddSwaggerGen(c =>
{ // {
c.SwaggerDoc("v1", OpenApiInfo); // c.UseAllOfToExtendReferenceSchemas();
c.UseAllOfToExtendReferenceSchemas(); // c.SupportNonNullableReferenceTypes();
c.SupportNonNullableReferenceTypes(); // });
});
var app = builder.Build(); var app = builder.Build();
app.UseSwagger(); // app.UseSwagger();
app.UseSwaggerUI(); // app.UseSwaggerUI();
app.UseHttpsRedirection(); // app.UseHttpsRedirection();
app.MapControllers(); app.MapControllers();
// app.MapGet("/", () => Controller.Index()); // app.MapGet("/", () => Controller.Index());
app.Run(); var webTask = app.RunAsync();
await Task.WhenAll(webTask, updateTask);
} }
private static OpenApiInfo OpenApiInfo { get; } = new OpenApiInfo private static OpenApiInfo OpenApiInfo { get; } = new OpenApiInfo

View File

@ -0,0 +1,17 @@
namespace InnovEnergy.App.VrmGrabber;
public static class ShuffleClass
{
private static readonly Random Rng = new Random();
public static void Shuffle<T>(this IList<T> list)
{
var n = list.Count;
while (n > 1) {
n--;
var k = Rng.Next(n + 1);
(list[k], list[n]) = (list[n], list[k]);
}
}
}

View File

@ -38,4 +38,10 @@
<_ContentIncludedByDefault Remove="wwwroot\index.html" /> <_ContentIncludedByDefault Remove="wwwroot\index.html" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<None Update="db.sqlite">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project> </Project>

Binary file not shown.

View File

@ -188,8 +188,8 @@ Global
{B816BB44-E97E-4E02-B80A-BEDB5B923A96}.Release|Any CPU.Build.0 = Release|Any CPU {B816BB44-E97E-4E02-B80A-BEDB5B923A96}.Release|Any CPU.Build.0 = Release|Any CPU
{4F9BB20B-8030-48AB-A37B-23796459D516}.Release|Any CPU.ActiveCfg = Release|Any CPU {4F9BB20B-8030-48AB-A37B-23796459D516}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4F9BB20B-8030-48AB-A37B-23796459D516}.Release|Any CPU.Build.0 = Release|Any CPU {4F9BB20B-8030-48AB-A37B-23796459D516}.Release|Any CPU.Build.0 = Release|Any CPU
{4F9BB20B-8030-48AB-A37B-23796459D516}.Debug|Any CPU.ActiveCfg = Release-Server|linux-arm {4F9BB20B-8030-48AB-A37B-23796459D516}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{4F9BB20B-8030-48AB-A37B-23796459D516}.Debug|Any CPU.Build.0 = Release-Server|linux-arm {4F9BB20B-8030-48AB-A37B-23796459D516}.Debug|Any CPU.Build.0 = Debug|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}

View File

@ -11,7 +11,7 @@ public static class JsonNodeAccessors
public static T Get<T>(this JsonNode n, String propName) => n[propName]!.GetValue<T>(); public static T Get<T>(this JsonNode n, String propName) => n[propName]!.GetValue<T>();
public static String GetString(this JsonNode n, String propName) => n.Get<String>(propName); public static String? GetString(this JsonNode n, String propName) => n.TryGetString(propName);
public static Boolean GetBoolean(this JsonNode n, String propName) => n.Get<Boolean>(propName); public static Boolean GetBoolean(this JsonNode n, String propName) => n.Get<Boolean>(propName);
public static UInt32 GetUInt32(this JsonNode n, String propName) => n.Get<UInt32>(propName); public static UInt32 GetUInt32(this JsonNode n, String propName) => n.Get<UInt32>(propName);
@ -43,7 +43,7 @@ public static class JsonNodeAccessors
{ {
try try
{ {
return n.TryGet<String>(propName); return n.TryGet<String>(propName) ?? "";
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -118,7 +118,7 @@ public static class StringUtils
return String.Join("", strings); return String.Join("", strings);
} }
public static String JoinWith(this IEnumerable<String> strings, String separator) public static String JoinWith(this IEnumerable<String?> strings, String separator)
{ {
return String.Join(separator, strings); return String.Join(separator, strings);
} }

View File

@ -13,6 +13,7 @@ public readonly partial record struct Installation(VrmAccount VrmAccount, JsonNo
public UnixTime Created => Json.GetUInt32("syscreated").Apply(UnixTime.FromTicks); public UnixTime Created => Json.GetUInt32("syscreated").Apply(UnixTime.FromTicks);
// Settings // Settings
public String Name => Json.GetString("name"); public String Name => Json.GetString("name");
public String Notes => Json.GetString("notes"); public String Notes => Json.GetString("notes");
public String PhoneNumber => Json.GetString("phonenumber"); public String PhoneNumber => Json.GetString("phonenumber");

View File

@ -31,6 +31,16 @@ public static class Requests
.WithHeader("X-Authorization", account.Auth); .WithHeader("X-Authorization", account.Auth);
} }
public static IFlurlRequest SpecificInstallationRequest(this VrmAccount account, UInt64 installationId)
{
return ApiRoot
.AppendPathSegment("users")
.AppendPathSegment(account.UserId)
.AppendPathSegment("installations")
.AppendPathSegment(installationId)
.WithHeader("X-Authorization", account.Auth);
}
public static IFlurlRequest AllInstallationsRequest(this VrmAccount account) public static IFlurlRequest AllInstallationsRequest(this VrmAccount account)
{ {
return ApiRoot return ApiRoot

View File

@ -50,6 +50,20 @@ public class VrmAccount : IDisposable
.ToArray(vrmReply.Records.Count); .ToArray(vrmReply.Records.Count);
} }
public async Task<Installation> GetInstallation(Int64 installationId)
{
var reply = await this.SpecificInstallationRequest((UInt64)installationId).TryGetJson();
var vrmReply = new Reply(reply);
if (!vrmReply.Success)
throw new Exception(nameof(GetInstallations) + " failed");
return vrmReply
.Records
.Select(r => new Installation(this, r!))
.First(i => i.IdSite == (UInt64)installationId);
}
public void Dispose() public void Dispose()
{ {
if (IsLoggedIn) if (IsLoggedIn)

View File

@ -84,6 +84,7 @@ def init_modbus(tty):
def failed(response): def failed(response):
# type: (ModbusResponse) -> bool # type: (ModbusResponse) -> bool
# Todo 'ModbusIOException' object has no attribute 'function_code'
return response.function_code > 0x80 return response.function_code > 0x80