diff --git a/csharp/App/Backend/db.sqlite b/csharp/App/Backend/db.sqlite deleted file mode 100644 index 984f315b5..000000000 Binary files a/csharp/App/Backend/db.sqlite and /dev/null differ diff --git a/csharp/App/RemoteSupportConsole/Ssh.cs b/csharp/App/RemoteSupportConsole/Ssh.cs index d9acc9662..dd4fe7a4e 100644 --- a/csharp/App/RemoteSupportConsole/Ssh.cs +++ b/csharp/App/RemoteSupportConsole/Ssh.cs @@ -7,7 +7,7 @@ public static class Ssh { const String ConfigFile = "/data/innovenergy/openvpn/installation-name"; - public static Task Interactive(String host, String installationName, String user, String port) + public static Task Interactive(String host, String? installationName, String user, String port) { var fixInstallationName = $"echo '{installationName}' > {ConfigFile}; exec bash -l"; diff --git a/csharp/App/VrmGrabber/Controller.cs b/csharp/App/VrmGrabber/Controller.cs index 653aede9b..2cac382e7 100644 --- a/csharp/App/VrmGrabber/Controller.cs +++ b/csharp/App/VrmGrabber/Controller.cs @@ -1,89 +1,156 @@ -using System.Web; +using CliWrap; +using CliWrap.Buffered; using HandlebarsDotNet; -using InnovEnergy.App.RemoteSupportConsole; using InnovEnergy.App.VrmGrabber.Database; -using InnovEnergy.Lib.Victron.VictronVRM; +using InnovEnergy.App.VrmGrabber.DataTypes; +using InnovEnergy.Lib.Utils; using Microsoft.AspNetCore.Mvc; using FILE=System.IO.File; +using VrmInstallation = InnovEnergy.Lib.Victron.VictronVRM.Installation; namespace InnovEnergy.App.VrmGrabber; -public record Install( +public record InstallationToHtmlInterface( String Name, String Ip, - UInt64 Vrm, + Int64 Vrm, String Identifier, 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] public class Controller : ControllerBase { + + //Todo automatically grab newest version? + private const String FirmwareVersion = "AF09"; + + [HttpGet] [Route("/")] [Produces("text/html")] public ActionResult Index() { - var instList = Db.InstallationsAndDetails.Keys.ToList(); - if (instList.Count == 0) return new ContentResult + const String source = @" + + +
+ + + + + + + + + + + + + + + {{#inst}} + {{> installations}} + {{/inst}} + +
NameGuiVRMGrafanaIdentifierLast SeenSerial#BatteriesFirmware-VersionUpdate
+
"; + + + + const String partialSource = @"{{Name}} + {{online}} {{Ip}} + VRM + Grafana + {{Identifier}} + {{LastSeen}} + {{Serial}} + {{NumBatteries}} + {{BatteryVersion}} + ⬆️{{FirmwareVersion}} + "; + + var installationsInDb = Db.Installations.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase).ToList(); + if (installationsInDb.Count == 0) return new ContentResult { ContentType = "text/html", Content = "

Please wait page is still loading

" }; - String source = @" - - - - - {{#inst}} - {{> installations}} - {{/inst}} - -
"; - - String partialSource = - @"{{Name}} - {{Ip}} - VRM - {{Identifier}} - {{Serial}} - Grafana"; - Handlebars.RegisterTemplate("installations", partialSource); var template = Handlebars.Compile(source); - var insts = instList.Select(i => - { - return new Install(i.Name, Ip(i), i.IdSite, i.Identifier, Serial(i), HttpUtility.UrlEncode(i.Name)); - }); - - + var installsForHtml = installationsInDb.Select(i => new InstallationToHtmlInterface( + 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 { - inst = insts + inst = installsForHtml, }; 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 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 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))] // [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] // public Object GetInstallation(UInt64 serialNumber) @@ -123,9 +241,3 @@ td { // } } - - - -// installation Name, ip (link uf gui), idSite (vrm link), identifier , machineserial (HQ...) - - diff --git a/csharp/App/VrmGrabber/DataTypes/Installation.cs b/csharp/App/VrmGrabber/DataTypes/Installation.cs index 2d118add8..977ce1d59 100644 --- a/csharp/App/VrmGrabber/DataTypes/Installation.cs +++ b/csharp/App/VrmGrabber/DataTypes/Installation.cs @@ -1,17 +1,42 @@ using InnovEnergy.Lib.Victron.VictronVRM; +using SQLite; 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 String Name { get; set; } - // Settings - public IReadOnlyList Notes { get; set; } + public String? Name { get; set;} + public String? Ip { 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 } \ No newline at end of file diff --git a/csharp/App/VrmGrabber/Database/Create.cs b/csharp/App/VrmGrabber/Database/Create.cs index b8d7dc25f..9283b6a70 100644 --- a/csharp/App/VrmGrabber/Database/Create.cs +++ b/csharp/App/VrmGrabber/Database/Create.cs @@ -1,5 +1,6 @@ using InnovEnergy.App.VrmGrabber.DataTypes; + namespace InnovEnergy.App.VrmGrabber.Database; diff --git a/csharp/App/VrmGrabber/Database/Db.cs b/csharp/App/VrmGrabber/Database/Db.cs index 41f91a55c..0684c8d09 100644 --- a/csharp/App/VrmGrabber/Database/Db.cs +++ b/csharp/App/VrmGrabber/Database/Db.cs @@ -1,62 +1,264 @@ using System.Diagnostics.CodeAnalysis; -using System.Reactive.Concurrency; -using System.Reactive.Linq; -using System.Reactive.Threading.Tasks; +using System.Runtime.InteropServices; +using System.Web; +using CliWrap; +using CliWrap.Buffered; +using InnovEnergy.App.RemoteSupportConsole; using InnovEnergy.Lib.Utils; using InnovEnergy.Lib.Victron.VictronVRM; using SQLite; using static System.Text.Json.JsonSerializer; +using static InnovEnergy.App.VrmGrabber.Database.Systemd; using Installation = InnovEnergy.App.VrmGrabber.DataTypes.Installation; +using VrmInstallation = InnovEnergy.Lib.Victron.VictronVRM.Installation; +using FILE=System.IO.File; namespace InnovEnergy.App.VrmGrabber.Database; - -public static partial class Db +public static class Systemd { - public static Dictionary> InstallationsAndDetails; - internal const String DbPath = "./db.sqlite"; + [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 details) + { + Details = details; + Ip = ip; + } + + public IReadOnlyList? Details { get; set; } + public String? Ip { get; set; } +} + +public static partial class Db +{ + public static Dictionary InstallationsAndDetails; + + + private const String DbPath = "./db.sqlite"; - private static SQLiteConnection Connection { get; } = new SQLiteConnection(DbPath); - public static TableQuery Installations => Connection.Table(); + + public static TableQuery Installations => Connection.Table(); + public static void Init() { // used to force static constructor } - + + static Db() { // on startup create/migrate tables - Connection.RunInTransaction(() => - { - // Connection.CreateTable(); - }); - - InstallationsAndDetails = new Dictionary>(); - - Observable.Interval(TimeSpan.FromMinutes(5)) - .ObserveOn(TaskPoolScheduler.Default) - .SubscribeOn(TaskPoolScheduler.Default) - .StartWith(0) // Do it right away (on startup) - .Select(UpdateInstallationsAndDetailsFromVrm) - .Select(t => t.ToObservable()) - .Concat() - .Subscribe(d => InstallationsAndDetails = d, exception => exception.WriteLine()); + Connection.RunInTransaction(() => { Connection.CreateTable(); }); + } + + public static async Task UpdateDetailsAndInstallations() + { + sd_notify(0, "READY=1"); + do { + await UpdateInstallationsAndDetailsFromVrm(0); + } + while (true) ; } [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "")] - private static async Task>> 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 GetVrmAccount() { var fileContent = await File.ReadAllTextAsync("./token.json"); - var acc = Deserialize(fileContent); - var user = VrmAccount.Token(acc.idUser, acc.token); - var installations = await user.GetInstallations(); - return installations - .Do(i=>i.Name.WriteLine()) - .ToDictionary(i => i, i => i.GetDetails().Result); + var user = VrmAccount.Token(acc!.idUser, acc.token); + return user; + } + + private static async Task 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 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 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 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()); + } + + 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 func) + { + var savepoint = Connection.SaveTransactionPoint(); + var success = false; + + try + { + success = func(); + } + finally + { + if (success) + Connection.Release(savepoint); + else + Connection.RollbackTo(savepoint); + } + + return success; } } @@ -64,4 +266,10 @@ public class AccToken { public UInt64 idUser { get; init;} public String token { get; init;} -} \ No newline at end of file +} + + +/* +dbus-send --system --dest=com.victronenergy.battery.ttyUSB1 --print-reply /FirmwareVersion \ +org.freedesktop.DBus.Properties.Get string:com.victronenergy.battery.ttyUSB1 + */ diff --git a/csharp/App/VrmGrabber/Database/Delete.cs b/csharp/App/VrmGrabber/Database/Delete.cs index e15fa4b31..3dd008e4e 100644 --- a/csharp/App/VrmGrabber/Database/Delete.cs +++ b/csharp/App/VrmGrabber/Database/Delete.cs @@ -1,13 +1,18 @@ using InnovEnergy.App.VrmGrabber.DataTypes; + namespace InnovEnergy.App.VrmGrabber.Database; -public static partial class Db +public static partial class Db { - 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; + } } } \ No newline at end of file diff --git a/csharp/App/VrmGrabber/Database/Read.cs b/csharp/App/VrmGrabber/Database/Read.cs index 4ef6c174c..9f7643862 100644 --- a/csharp/App/VrmGrabber/Database/Read.cs +++ b/csharp/App/VrmGrabber/Database/Read.cs @@ -6,9 +6,9 @@ namespace InnovEnergy.App.VrmGrabber.Database; public static partial class Db { - public static Installation? GetInstallationById(Int64? id) + public static Installation? GetInstallationByIdentifier(String? identifier) { return Installations - .FirstOrDefault(i => i.Id == id); + .FirstOrDefault(i => i.Identifier == identifier); } } \ No newline at end of file diff --git a/csharp/App/VrmGrabber/Database/Update.cs b/csharp/App/VrmGrabber/Database/Update.cs new file mode 100644 index 000000000..723378685 --- /dev/null +++ b/csharp/App/VrmGrabber/Database/Update.cs @@ -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; + } +} \ No newline at end of file diff --git a/csharp/App/VrmGrabber/Program.cs b/csharp/App/VrmGrabber/Program.cs index 02a2a9a6b..693f847cc 100644 --- a/csharp/App/VrmGrabber/Program.cs +++ b/csharp/App/VrmGrabber/Program.cs @@ -5,26 +5,26 @@ namespace InnovEnergy.App.VrmGrabber; 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); builder.Services.AddControllers(); - builder.Services.AddSwaggerGen(c => - { - c.SwaggerDoc("v1", OpenApiInfo); - c.UseAllOfToExtendReferenceSchemas(); - c.SupportNonNullableReferenceTypes(); - }); + // builder.Services.AddSwaggerGen(c => + // { + // c.UseAllOfToExtendReferenceSchemas(); + // c.SupportNonNullableReferenceTypes(); + // }); var app = builder.Build(); - app.UseSwagger(); - app.UseSwaggerUI(); - app.UseHttpsRedirection(); + // app.UseSwagger(); + // app.UseSwaggerUI(); + // app.UseHttpsRedirection(); app.MapControllers(); // app.MapGet("/", () => Controller.Index()); - app.Run(); + var webTask = app.RunAsync(); + await Task.WhenAll(webTask, updateTask); } private static OpenApiInfo OpenApiInfo { get; } = new OpenApiInfo diff --git a/csharp/App/VrmGrabber/ShuffleClass.cs b/csharp/App/VrmGrabber/ShuffleClass.cs new file mode 100644 index 000000000..a52e40cdf --- /dev/null +++ b/csharp/App/VrmGrabber/ShuffleClass.cs @@ -0,0 +1,17 @@ +namespace InnovEnergy.App.VrmGrabber; + +public static class ShuffleClass +{ + private static readonly Random Rng = new Random(); + + public static void Shuffle(this IList list) + { + var n = list.Count; + while (n > 1) { + n--; + var k = Rng.Next(n + 1); + (list[k], list[n]) = (list[n], list[k]); + } + } + +} \ No newline at end of file diff --git a/csharp/App/VrmGrabber/VrmGrabber.csproj b/csharp/App/VrmGrabber/VrmGrabber.csproj index 92c7a0262..fac230318 100644 --- a/csharp/App/VrmGrabber/VrmGrabber.csproj +++ b/csharp/App/VrmGrabber/VrmGrabber.csproj @@ -37,5 +37,11 @@ <_ContentIncludedByDefault Remove="wwwroot\index.html" /> + + + + PreserveNewest + + diff --git a/csharp/App/VrmGrabber/db.sqlite b/csharp/App/VrmGrabber/db.sqlite index 8294c39d2..e779f5916 100644 Binary files a/csharp/App/VrmGrabber/db.sqlite and b/csharp/App/VrmGrabber/db.sqlite differ diff --git a/csharp/InnovEnergy.sln b/csharp/InnovEnergy.sln index 3a0401858..640f8ab8f 100644 --- a/csharp/InnovEnergy.sln +++ b/csharp/InnovEnergy.sln @@ -188,8 +188,8 @@ Global {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.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.Build.0 = 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 = Debug|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A} diff --git a/csharp/Lib/Utils/JsonNodeAccessors.cs b/csharp/Lib/Utils/JsonNodeAccessors.cs index 723673111..31cbb17a9 100644 --- a/csharp/Lib/Utils/JsonNodeAccessors.cs +++ b/csharp/Lib/Utils/JsonNodeAccessors.cs @@ -11,7 +11,7 @@ public static class JsonNodeAccessors public static T Get(this JsonNode n, String propName) => n[propName]!.GetValue(); - public static String GetString(this JsonNode n, String propName) => n.Get(propName); + public static String? GetString(this JsonNode n, String propName) => n.TryGetString(propName); public static Boolean GetBoolean(this JsonNode n, String propName) => n.Get(propName); public static UInt32 GetUInt32(this JsonNode n, String propName) => n.Get(propName); @@ -43,7 +43,7 @@ public static class JsonNodeAccessors { try { - return n.TryGet(propName); + return n.TryGet(propName) ?? ""; } catch (Exception e) { diff --git a/csharp/Lib/Utils/StringUtils.cs b/csharp/Lib/Utils/StringUtils.cs index 46372e381..b98fbf53c 100644 --- a/csharp/Lib/Utils/StringUtils.cs +++ b/csharp/Lib/Utils/StringUtils.cs @@ -118,7 +118,7 @@ public static class StringUtils return String.Join("", strings); } - public static String JoinWith(this IEnumerable strings, String separator) + public static String JoinWith(this IEnumerable strings, String separator) { return String.Join(separator, strings); } diff --git a/csharp/Lib/Victron/VictronVRM/Installation.cs b/csharp/Lib/Victron/VictronVRM/Installation.cs index 82fcd85ce..fc9781da5 100644 --- a/csharp/Lib/Victron/VictronVRM/Installation.cs +++ b/csharp/Lib/Victron/VictronVRM/Installation.cs @@ -13,6 +13,7 @@ public readonly partial record struct Installation(VrmAccount VrmAccount, JsonNo public UnixTime Created => Json.GetUInt32("syscreated").Apply(UnixTime.FromTicks); // Settings + public String Name => Json.GetString("name"); public String Notes => Json.GetString("notes"); public String PhoneNumber => Json.GetString("phonenumber"); diff --git a/csharp/Lib/Victron/VictronVRM/Requests.cs b/csharp/Lib/Victron/VictronVRM/Requests.cs index 5ec6c4214..99684e724 100644 --- a/csharp/Lib/Victron/VictronVRM/Requests.cs +++ b/csharp/Lib/Victron/VictronVRM/Requests.cs @@ -31,6 +31,16 @@ public static class Requests .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) { return ApiRoot diff --git a/csharp/Lib/Victron/VictronVRM/VrmAccount.cs b/csharp/Lib/Victron/VictronVRM/VrmAccount.cs index a5266ee4b..3f2f4689b 100644 --- a/csharp/Lib/Victron/VictronVRM/VrmAccount.cs +++ b/csharp/Lib/Victron/VictronVRM/VrmAccount.cs @@ -49,6 +49,20 @@ public class VrmAccount : IDisposable .Select(r => new Installation(this, r!)) .ToArray(vrmReply.Records.Count); } + + public async Task 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() { diff --git a/firmware/opt/innovenergy/scripts/upload-bms-firmware b/firmware/opt/innovenergy/scripts/upload-bms-firmware index 70975deea..e341d7f64 100755 --- a/firmware/opt/innovenergy/scripts/upload-bms-firmware +++ b/firmware/opt/innovenergy/scripts/upload-bms-firmware @@ -84,6 +84,7 @@ def init_modbus(tty): def failed(response): # type: (ModbusResponse) -> bool + # Todo 'ModbusIOException' object has no attribute 'function_code' return response.function_code > 0x80