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 = @"<head> <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 This site is updated once per day!</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> <th>Last Update Status</th> <th>Upload Node Red Files</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}}>⬆️{{FirmwareVersion}}</a></td> <td>{{BatteryUpdateStatus}}</td> <td><a target='_blank' href=http://{{ServerIp}}/UploadNodeRedFiles/{{Ip}}>⬆️{{NodeRedFiles}}</a></td> </tr>"; var installationsInDb = Db.Installations.OrderBy(i => i.Name, StringComparer.OrdinalIgnoreCase).ToList(); if (installationsInDb.Count == 0) return new ContentResult { ContentType = "text/html", Content = "<p>Please wait page is still loading</p>" }; 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<String> 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<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 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 = "<Pending>")] // 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<UInt64>() != serialNumber)) continue; // var retour = Db.InstallationsAndDetails.Keys.ToList()[detailList.Index].Json; // retour["details"] = JsonSerializer.Deserialize<JsonArray>(JsonSerializer.Serialize(detailList.Value.Select(d => d.Json).ToArray())); // return retour; // } // // return new NotFoundResult(); // } // remove the original ones???????? [HttpPost("UploadNodeRedFiles/{installationIp}")] public async Task<IActionResult> UploadNodeRedFiles(String installationIp) { // Define the mapping of files to remote locations var fileLocationMappings = new Dictionary<string, string> { { "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}"); } } }