300 lines
10 KiB
C#
300 lines
10 KiB
C#
using System.Diagnostics.CodeAnalysis;
|
|
using System.IO.Enumeration;
|
|
using System.Runtime.InteropServices;
|
|
using System.Text.Json.Nodes;
|
|
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<Detail> details)
|
|
{
|
|
Details = details;
|
|
Ip = ip;
|
|
}
|
|
|
|
public IReadOnlyList<Detail>? Details { get; set; }
|
|
public String? Ip { get; set; }
|
|
}
|
|
|
|
public static partial class Db
|
|
{
|
|
public static Dictionary<Installation, InstallationDetails> InstallationsAndDetails;
|
|
|
|
|
|
private const String DbPath = "./db.sqlite";
|
|
|
|
private static SQLiteConnection Connection { get; } = new SQLiteConnection(DbPath);
|
|
|
|
public static TableQuery<Installation> Installations => Connection.Table<Installation>();
|
|
|
|
public static void Init()
|
|
{
|
|
// used to force static constructor
|
|
}
|
|
|
|
|
|
static Db()
|
|
{
|
|
// on startup create/migrate tables
|
|
|
|
Connection.RunInTransaction(() => { Connection.CreateTable<Installation>(); });
|
|
}
|
|
|
|
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 = "<Pending>")]
|
|
private static async Task UpdateInstallationsAndDetailsFromVrm(Int32 _)
|
|
{
|
|
var user = await GetVrmAccount();
|
|
var readOnlyInstallations = await user.GetInstallations();
|
|
|
|
var installations = readOnlyInstallations.ToList();
|
|
installations.Shuffle(); // This
|
|
|
|
foreach (var installation in installations)
|
|
{
|
|
Console.WriteLine(installation.Name);
|
|
sd_notify(0, "WATCHDOG=1");
|
|
var details = await GetInstallationDetails(installation);
|
|
// Thread.Sleep(1000);
|
|
await UpdateAlarms(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]),
|
|
"No updates"
|
|
);
|
|
|
|
if (ip[0] != "Unknown")
|
|
await UpdateInstallationName(installation, ip[0]);
|
|
|
|
if (GetInstallationByIdentifier(installation.Identifier) == null)
|
|
{
|
|
Create(updatedInstallation);
|
|
}
|
|
else
|
|
{
|
|
Update(updatedInstallation);
|
|
}
|
|
}
|
|
}
|
|
|
|
[RequiresUnreferencedCode(
|
|
"Calls System.Text.Json.JsonSerializer.SerializeToElement<TValue>(TValue, JsonSerializerOptions)")]
|
|
public static async Task<Boolean> UpdateAlarms(VrmInstallation installation)
|
|
{
|
|
// installation.GetDevices().batteryMonitor.setTemperatureAlarms(245,250,315,313);
|
|
var alarmJson = JsonNode.Parse(File.ReadAllText("./alarm.json"))!.AsObject();
|
|
var tags = await installation.GetTags();
|
|
if (tags.Contains("FM-AF09"))
|
|
{
|
|
// Console.WriteLine(installation.IdSite.ToString());
|
|
// await installation.GetAlarms();
|
|
return await installation.SetAlarms(alarmJson);
|
|
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public static async Task<VrmAccount> GetVrmAccount()
|
|
{
|
|
var fileContent = await File.ReadAllTextAsync("./token.json");
|
|
var acc = Deserialize<AccToken>(fileContent);
|
|
var user = VrmAccount.Token(acc!.idUser, acc.token);
|
|
return user;
|
|
}
|
|
|
|
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 > 10 ? "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;
|
|
}
|
|
}
|
|
|
|
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
|
|
*/
|