using System.Diagnostics.CodeAnalysis; 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 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 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 void Init() { // used to force static constructor } static Db() { // on startup create/migrate tables 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(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); 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"; //Todo this seems to be broken? Need to test on Server... 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; } } public class AccToken { public UInt64 idUser { 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 */