using CliWrap; using HandlebarsDotNet; using InnovEnergy.App.VrmGrabber.Database; using InnovEnergy.Lib.Utils; using Microsoft.AspNetCore.Mvc; using VrmInstallation = InnovEnergy.Lib.Victron.VictronVRM.Installation; namespace InnovEnergy.App.VrmGrabber; public record InstallationToHtmlInterface( String Name, String Ip, Int64 Vrm, String Identifier, String Serial, String EscapedName, String Online, String LastSeen, String NumBatteries, String BatteryVersion, String BatteryUpdateStatus, String ServerIp = "10.2.0.1", //TODO MAKE ME DYNAMIC String FirmwareVersion = "AF09", //Todo automatically grab newest version? String NodeRedFiles = "NodeRedFiles" ); [Controller] public class Controller : ControllerBase { //Todo automatically grab newest version? private const String FirmwareVersion = "AF09"; [HttpGet] [Route("/")] [Produces("text/html")] public ActionResult Index() { const String source = @"
{{#inst}} {{> installations}} {{/inst}}
Name This site is updated once per day! Gui VRM Grafana Identifier Last Seen Serial #Batteries Firmware-Version Update Last Update Status Upload Node Red Files
"; const String partialSource = @"{{Name}} {{online}} {{Ip}} VRM Grafana {{Identifier}} {{LastSeen}} {{Serial}} {{NumBatteries}} {{BatteryVersion}} ⬆️{{FirmwareVersion}} {{BatteryUpdateStatus}} ⬆️{{NodeRedFiles}} "; 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

" }; Handlebars.RegisterTemplate("installations", partialSource); var template = Handlebars.Compile(source); 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, i.BatteryUpdateStatus)); var data = new { inst = installsForHtml, }; var result = template(data); return new ContentResult { ContentType = "text/html", Content = result }; } [HttpGet("UpdateBatteryFirmware/{installationIp}")] public async Task UpdateBatteryFirmware(String installationIp) { //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 SendNewBatteryFirmware(installationIp); var batteryTtyName = split[1].Split(".").Last(); var localCommand = "echo start"; var installation = Db.Installations.First(installation => installation.Ip == installationIp); installation.BatteryUpdateStatus = "Running"; Db.Update(installation: installation); var batteryIdsResult = await Db.ExecuteBufferedAsyncCommandOnIp(installationIp, $"dbus-send --system --dest=com.victronenergy.battery.{batteryTtyName} --type=method_call --print-reply / com.victronenergy.BusItem.GetText | grep -E -o '_Battery/[0-9]+/' | grep -E -o '[0-9]+'| sort -u"); var batteryIds = batteryIdsResult.Split("\n").ToList(); batteryIds.Pop(); foreach (var batteryId in batteryIds) { localCommand = localCommand.Append( $" && /opt/innovenergy/scripts/upload-bms-firmware {batteryTtyName} {batteryId} /opt/innovenergy/bms-firmware/{FirmwareVersion}.bin"); } #pragma warning disable CS4014 // Console.WriteLine(localCommand); Db.ExecuteBufferedAsyncCommandOnIp(installationIp, localCommand) .ContinueWith(async t => { Console.WriteLine(t.Result); installation.BatteryUpdateStatus = "Complete"; // installation.BatteryFirmwareVersion = FirmwareVersion; Db.Update(installation: installation); var vrmInst = await FindVrmInstallationByIp(installation.Ip!); await UpdateVrmTagsToNewFirmware(installationIp); await Db.UpdateAlarms(vrmInst); }); #pragma warning restore CS4014 return "Battery update is successfully initiated, it will take around 15 minutes to complete! You can close this page now."; } private static async Task UpdateVrmTagsToNewFirmware(String installationIp) { 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 async Task SendNewBatteryFirmware(String installationIp) { await Cli.Wrap("rsync") .WithArguments($@"-r --relative bms-firmware/{FirmwareVersion}.bin") .AppendArgument($@"root@{installationIp}:/opt/innovenergy") .ExecuteAsync(); } // [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) // { // var instList = Db.InstallationsAndDetails.Values.ToList(); // foreach (var detailList in instList.Select((value, index) => new { Value = value, Index = index})) // { // if (detailList.Value.All(detail => detail.Json["idSite"]?.GetValue() != serialNumber)) continue; // var retour = Db.InstallationsAndDetails.Keys.ToList()[detailList.Index].Json; // retour["details"] = JsonSerializer.Deserialize(JsonSerializer.Serialize(detailList.Value.Select(d => d.Json).ToArray())); // return retour; // } // // return new NotFoundResult(); // } // remove the original ones???????? [HttpPost("UploadNodeRedFiles/{installationIp}")] public async Task UploadNodeRedFiles(String installationIp) { // Define the mapping of files to remote locations var fileLocationMappings = new Dictionary { { "flows.json", "/opt/data/nodered/.node-red/" }, { "settings-user.js", "/opt/data/nodered/.node-red/" }, { "rc.local", "/data/" }, { "dbus-fzsonick-48tl", "/data/"} }; var nodeRedFilesFolder = Path.Combine(Directory.GetCurrentDirectory(), "NodeRedFiles"); if (!Directory.Exists(nodeRedFilesFolder)) { return BadRequest("NodeRedFiles folder does not exist."); } var tasks = fileLocationMappings.Select(async mapping => { var fileName = mapping.Key; var remoteLocation = mapping.Value; var filePath = Path.Combine(nodeRedFilesFolder, fileName); if (!System.IO.File.Exists(filePath)) { throw new FileNotFoundException($"File {fileName} not found in {nodeRedFilesFolder}."); } // Execute the SCP command to upload the file await Cli.Wrap("rsync") .WithArguments($@"-r {filePath}") .AppendArgument($@"root@{installationIp}:{remoteLocation}") .ExecuteAsync(); }); try { await Task.WhenAll(tasks); return Ok("All files uploaded successfully."); } catch (Exception ex) { return StatusCode(500, $"An error occurred while uploading files: {ex.Message}"); } } }