Merge branch 'main' of https://git.innov.energy/Innovenergy/git_trunk
This commit is contained in:
commit
b649abb7f8
Binary file not shown.
|
@ -7,7 +7,7 @@ public static class Ssh
|
|||
{
|
||||
const String ConfigFile = "/data/innovenergy/openvpn/installation-name";
|
||||
|
||||
public static Task<Int32> Interactive(String host, String installationName, String user, String port)
|
||||
public static Task<Int32> Interactive(String host, String? installationName, String user, String port)
|
||||
{
|
||||
var fixInstallationName = $"echo '{installationName}' > {ConfigFile}; exec bash -l";
|
||||
|
||||
|
|
|
@ -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 = @"<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</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>
|
||||
</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}}/{{NumBatteries}}>⬆️{{FirmwareVersion}}</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>"
|
||||
};
|
||||
|
||||
String source = @"<head>
|
||||
<style>
|
||||
tbody {
|
||||
background-color: #e4f0f5;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
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;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border: 1px solid rgb(190, 190, 190);
|
||||
padding: 5px 10px;
|
||||
}
|
||||
|
||||
td {
|
||||
text-align: left;
|
||||
}</style></head>
|
||||
|
||||
<table>
|
||||
<tbody>
|
||||
{{#inst}}
|
||||
{{> installations}}
|
||||
{{/inst}}
|
||||
</tbody>
|
||||
</table>";
|
||||
|
||||
String partialSource =
|
||||
@"<tr><td>{{Name}}</td>
|
||||
<td><a target='_blank' href=http://{{Ip}}>{{Ip}}</a></td>
|
||||
<td><a target='_blank' href=https://vrm.victronenergy.com/installation/{{Vrm}}/dashboard>VRM</a></td>
|
||||
<td>{{Identifier}}</td>
|
||||
<td>{{Serial}}</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></tr>";
|
||||
|
||||
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<String> 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<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 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 = "<Pending>")]
|
||||
// public Object GetInstallation(UInt64 serialNumber)
|
||||
|
@ -123,9 +241,3 @@ td {
|
|||
// }
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// installation Name, ip (link uf gui), idSite (vrm link), identifier , machineserial (HQ...)
|
||||
|
||||
|
||||
|
|
|
@ -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<Detail> 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
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
using InnovEnergy.App.VrmGrabber.DataTypes;
|
||||
|
||||
|
||||
namespace InnovEnergy.App.VrmGrabber.Database;
|
||||
|
||||
|
||||
|
|
|
@ -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 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<Lib.Victron.VictronVRM.Installation, IReadOnlyList<Detail>> InstallationsAndDetails;
|
||||
internal const String DbPath = "./db.sqlite";
|
||||
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 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>();
|
||||
});
|
||||
Connection.RunInTransaction(() => { Connection.CreateTable<Installation>(); });
|
||||
}
|
||||
|
||||
InstallationsAndDetails = new Dictionary<Lib.Victron.VictronVRM.Installation, IReadOnlyList<Detail>>();
|
||||
|
||||
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());
|
||||
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<Dictionary<Lib.Victron.VictronVRM.Installation, IReadOnlyList<Detail>>> 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<VrmAccount> GetVrmAccount()
|
||||
{
|
||||
var fileContent = await File.ReadAllTextAsync("./token.json");
|
||||
|
||||
var acc = Deserialize<AccToken>(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<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 > 5 ? "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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,3 +267,9 @@ 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
|
||||
*/
|
||||
|
|
|
@ -1,13 +1,18 @@
|
|||
using InnovEnergy.App.VrmGrabber.DataTypes;
|
||||
|
||||
|
||||
namespace InnovEnergy.App.VrmGrabber.Database;
|
||||
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
namespace InnovEnergy.App.VrmGrabber;
|
||||
|
||||
public static class ShuffleClass
|
||||
{
|
||||
private static readonly Random Rng = new Random();
|
||||
|
||||
public static void Shuffle<T>(this IList<T> list)
|
||||
{
|
||||
var n = list.Count;
|
||||
while (n > 1) {
|
||||
n--;
|
||||
var k = Rng.Next(n + 1);
|
||||
(list[k], list[n]) = (list[n], list[k]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -38,4 +38,10 @@
|
|||
<_ContentIncludedByDefault Remove="wwwroot\index.html" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="db.sqlite">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
Binary file not shown.
|
@ -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}
|
||||
|
|
|
@ -11,7 +11,7 @@ public static class JsonNodeAccessors
|
|||
public static T Get<T>(this JsonNode n, String propName) => n[propName]!.GetValue<T>();
|
||||
|
||||
|
||||
public static String GetString(this JsonNode n, String propName) => n.Get<String>(propName);
|
||||
public static String? GetString(this JsonNode n, String propName) => n.TryGetString(propName);
|
||||
public static Boolean GetBoolean(this JsonNode n, String propName) => n.Get<Boolean>(propName);
|
||||
|
||||
public static UInt32 GetUInt32(this JsonNode n, String propName) => n.Get<UInt32>(propName);
|
||||
|
@ -43,7 +43,7 @@ public static class JsonNodeAccessors
|
|||
{
|
||||
try
|
||||
{
|
||||
return n.TryGet<String>(propName);
|
||||
return n.TryGet<String>(propName) ?? "";
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
|
|
@ -118,7 +118,7 @@ public static class StringUtils
|
|||
return String.Join("", strings);
|
||||
}
|
||||
|
||||
public static String JoinWith(this IEnumerable<String> strings, String separator)
|
||||
public static String JoinWith(this IEnumerable<String?> strings, String separator)
|
||||
{
|
||||
return String.Join(separator, strings);
|
||||
}
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -50,6 +50,20 @@ public class VrmAccount : IDisposable
|
|||
.ToArray(vrmReply.Records.Count);
|
||||
}
|
||||
|
||||
public async Task<Installation> 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()
|
||||
{
|
||||
if (IsLoggedIn)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue