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";
|
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";
|
var fixInstallationName = $"echo '{installationName}' > {ConfigFile}; exec bash -l";
|
||||||
|
|
||||||
|
|
|
@ -1,45 +1,72 @@
|
||||||
using System.Web;
|
using CliWrap;
|
||||||
|
using CliWrap.Buffered;
|
||||||
using HandlebarsDotNet;
|
using HandlebarsDotNet;
|
||||||
using InnovEnergy.App.RemoteSupportConsole;
|
|
||||||
using InnovEnergy.App.VrmGrabber.Database;
|
using InnovEnergy.App.VrmGrabber.Database;
|
||||||
using InnovEnergy.Lib.Victron.VictronVRM;
|
using InnovEnergy.App.VrmGrabber.DataTypes;
|
||||||
|
using InnovEnergy.Lib.Utils;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using FILE=System.IO.File;
|
using FILE=System.IO.File;
|
||||||
|
using VrmInstallation = InnovEnergy.Lib.Victron.VictronVRM.Installation;
|
||||||
|
|
||||||
namespace InnovEnergy.App.VrmGrabber;
|
namespace InnovEnergy.App.VrmGrabber;
|
||||||
|
|
||||||
public record Install(
|
public record InstallationToHtmlInterface(
|
||||||
String Name,
|
String Name,
|
||||||
String Ip,
|
String Ip,
|
||||||
UInt64 Vrm,
|
Int64 Vrm,
|
||||||
String Identifier,
|
String Identifier,
|
||||||
String Serial,
|
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]
|
[Controller]
|
||||||
public class Controller : ControllerBase
|
public class Controller : ControllerBase
|
||||||
{
|
{
|
||||||
|
|
||||||
|
//Todo automatically grab newest version?
|
||||||
|
private const String FirmwareVersion = "AF09";
|
||||||
|
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("/")]
|
[Route("/")]
|
||||||
[Produces("text/html")]
|
[Produces("text/html")]
|
||||||
public ActionResult Index()
|
public ActionResult Index()
|
||||||
{
|
{
|
||||||
var instList = Db.InstallationsAndDetails.Keys.ToList();
|
const String source = @"<head>
|
||||||
if (instList.Count == 0) return new ContentResult
|
|
||||||
{
|
|
||||||
ContentType = "text/html",
|
|
||||||
Content = "<p>Please wait page is still loading</p>"
|
|
||||||
};
|
|
||||||
|
|
||||||
String source = @"<head>
|
|
||||||
<style>
|
<style>
|
||||||
tbody {
|
tbody {
|
||||||
background-color: #e4f0f5;
|
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 {
|
table {
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
border: 2px solid rgb(200, 200, 200);
|
border: 2px solid rgb(200, 200, 200);
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
|
@ -47,43 +74,83 @@ table {
|
||||||
position: absolute; top: 0; bottom: 0; left: 0; right: 0;
|
position: absolute; top: 0; bottom: 0; left: 0; right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
td,
|
thead th {
|
||||||
th {
|
|
||||||
border: 1px solid rgb(190, 190, 190);
|
border: 1px solid rgb(190, 190, 190);
|
||||||
padding: 5px 10px;
|
padding: 5px 10px;
|
||||||
|
position: sticky;
|
||||||
|
position: -webkit-sticky;
|
||||||
|
top: 0px;
|
||||||
|
background: white;
|
||||||
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
}
|
||||||
|
#managerTable {
|
||||||
|
overflow: hidden;
|
||||||
}</style></head>
|
}</style></head>
|
||||||
|
|
||||||
|
<div id='managerTable'>
|
||||||
<table>
|
<table>
|
||||||
<tbody>
|
<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}}
|
{{#inst}}
|
||||||
{{> installations}}
|
{{> installations}}
|
||||||
{{/inst}}
|
{{/inst}}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>";
|
</table>
|
||||||
|
<div id='managerTable'>";
|
||||||
|
|
||||||
String partialSource =
|
|
||||||
@"<tr><td>{{Name}}</td>
|
|
||||||
<td><a target='_blank' href=http://{{Ip}}>{{Ip}}</a></td>
|
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://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>{{Identifier}}</td>
|
||||||
|
<td>{{LastSeen}}</td>
|
||||||
<td>{{Serial}}</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>";
|
<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>"
|
||||||
|
};
|
||||||
|
|
||||||
Handlebars.RegisterTemplate("installations", partialSource);
|
Handlebars.RegisterTemplate("installations", partialSource);
|
||||||
var template = Handlebars.Compile(source);
|
var template = Handlebars.Compile(source);
|
||||||
var insts = instList.Select(i =>
|
var installsForHtml = installationsInDb.Select(i => new InstallationToHtmlInterface(
|
||||||
{
|
i.Name,
|
||||||
return new Install(i.Name, Ip(i), i.IdSite, i.Identifier, Serial(i), HttpUtility.UrlEncode(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
|
var data = new
|
||||||
{
|
{
|
||||||
inst = insts
|
inst = installsForHtml,
|
||||||
};
|
};
|
||||||
|
|
||||||
var result = template(data);
|
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))]
|
// [HttpGet(nameof(GetInstallation))]
|
||||||
// [UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
|
// [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)
|
// 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 InnovEnergy.Lib.Victron.VictronVRM;
|
||||||
|
using SQLite;
|
||||||
|
|
||||||
namespace InnovEnergy.App.VrmGrabber.DataTypes;
|
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 Installation()
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
public String Name { get; set; }
|
public String? Name { get; set;}
|
||||||
// Settings
|
public String? Ip { get; set;}
|
||||||
public IReadOnlyList<Detail> Notes { 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;
|
using InnovEnergy.App.VrmGrabber.DataTypes;
|
||||||
|
|
||||||
|
|
||||||
namespace InnovEnergy.App.VrmGrabber.Database;
|
namespace InnovEnergy.App.VrmGrabber.Database;
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -1,62 +1,264 @@
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
using System.Reactive.Concurrency;
|
using System.Runtime.InteropServices;
|
||||||
using System.Reactive.Linq;
|
using System.Web;
|
||||||
using System.Reactive.Threading.Tasks;
|
using CliWrap;
|
||||||
|
using CliWrap.Buffered;
|
||||||
|
using InnovEnergy.App.RemoteSupportConsole;
|
||||||
using InnovEnergy.Lib.Utils;
|
using InnovEnergy.Lib.Utils;
|
||||||
using InnovEnergy.Lib.Victron.VictronVRM;
|
using InnovEnergy.Lib.Victron.VictronVRM;
|
||||||
using SQLite;
|
using SQLite;
|
||||||
using static System.Text.Json.JsonSerializer;
|
using static System.Text.Json.JsonSerializer;
|
||||||
|
using static InnovEnergy.App.VrmGrabber.Database.Systemd;
|
||||||
using Installation = InnovEnergy.App.VrmGrabber.DataTypes.Installation;
|
using Installation = InnovEnergy.App.VrmGrabber.DataTypes.Installation;
|
||||||
|
using VrmInstallation = InnovEnergy.Lib.Victron.VictronVRM.Installation;
|
||||||
|
using FILE=System.IO.File;
|
||||||
|
|
||||||
|
|
||||||
namespace InnovEnergy.App.VrmGrabber.Database;
|
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 partial class Db
|
||||||
{
|
{
|
||||||
public static Dictionary<Lib.Victron.VictronVRM.Installation, IReadOnlyList<Detail>> InstallationsAndDetails;
|
public static Dictionary<Installation, InstallationDetails> InstallationsAndDetails;
|
||||||
internal const String DbPath = "./db.sqlite";
|
|
||||||
|
|
||||||
|
|
||||||
|
private const String DbPath = "./db.sqlite";
|
||||||
|
|
||||||
private static SQLiteConnection Connection { get; } = new SQLiteConnection(DbPath);
|
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()
|
public static void Init()
|
||||||
{
|
{
|
||||||
// used to force static constructor
|
// used to force static constructor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
static Db()
|
static Db()
|
||||||
{
|
{
|
||||||
// on startup create/migrate tables
|
// on startup create/migrate tables
|
||||||
|
|
||||||
Connection.RunInTransaction(() =>
|
Connection.RunInTransaction(() => { Connection.CreateTable<Installation>(); });
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async Task UpdateDetailsAndInstallations()
|
||||||
{
|
{
|
||||||
// Connection.CreateTable<Installation>();
|
sd_notify(0, "READY=1");
|
||||||
});
|
do {
|
||||||
|
await UpdateInstallationsAndDetailsFromVrm(0);
|
||||||
InstallationsAndDetails = new Dictionary<Lib.Victron.VictronVRM.Installation, IReadOnlyList<Detail>>();
|
}
|
||||||
|
while (true) ;
|
||||||
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());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
|
[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 fileContent = await File.ReadAllTextAsync("./token.json");
|
||||||
|
|
||||||
var acc = Deserialize<AccToken>(fileContent);
|
var acc = Deserialize<AccToken>(fileContent);
|
||||||
var user = VrmAccount.Token(acc.idUser, acc.token);
|
var user = VrmAccount.Token(acc!.idUser, acc.token);
|
||||||
var installations = await user.GetInstallations();
|
return user;
|
||||||
return installations
|
}
|
||||||
.Do(i=>i.Name.WriteLine())
|
|
||||||
.ToDictionary(i => i, i => i.GetDetails().Result);
|
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 UInt64 idUser { get; init;}
|
||||||
public String token { 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;
|
using InnovEnergy.App.VrmGrabber.DataTypes;
|
||||||
|
|
||||||
|
|
||||||
namespace InnovEnergy.App.VrmGrabber.Database;
|
namespace InnovEnergy.App.VrmGrabber.Database;
|
||||||
|
|
||||||
|
|
||||||
public static partial class Db
|
public static partial class Db
|
||||||
{
|
{
|
||||||
|
|
||||||
public static Boolean Delete(Installation installation)
|
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 partial class Db
|
||||||
{
|
{
|
||||||
|
|
||||||
public static Installation? GetInstallationById(Int64? id)
|
public static Installation? GetInstallationByIdentifier(String? identifier)
|
||||||
{
|
{
|
||||||
return Installations
|
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 class Program
|
||||||
{
|
{
|
||||||
public static void Main(String[] args)
|
public static async Task Main(String[] args)
|
||||||
{
|
{
|
||||||
|
var updateTask = Db.UpdateDetailsAndInstallations();
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
builder.Services.AddControllers();
|
builder.Services.AddControllers();
|
||||||
builder.Services.AddSwaggerGen(c =>
|
// builder.Services.AddSwaggerGen(c =>
|
||||||
{
|
// {
|
||||||
c.SwaggerDoc("v1", OpenApiInfo);
|
// c.UseAllOfToExtendReferenceSchemas();
|
||||||
c.UseAllOfToExtendReferenceSchemas();
|
// c.SupportNonNullableReferenceTypes();
|
||||||
c.SupportNonNullableReferenceTypes();
|
// });
|
||||||
});
|
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
app.UseSwagger();
|
// app.UseSwagger();
|
||||||
app.UseSwaggerUI();
|
// app.UseSwaggerUI();
|
||||||
app.UseHttpsRedirection();
|
// app.UseHttpsRedirection();
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
// app.MapGet("/", () => Controller.Index());
|
// app.MapGet("/", () => Controller.Index());
|
||||||
app.Run();
|
var webTask = app.RunAsync();
|
||||||
|
await Task.WhenAll(webTask, updateTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static OpenApiInfo OpenApiInfo { get; } = new OpenApiInfo
|
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" />
|
<_ContentIncludedByDefault Remove="wwwroot\index.html" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<None Update="db.sqlite">
|
||||||
|
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||||
|
</None>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
Binary file not shown.
|
@ -188,8 +188,8 @@ Global
|
||||||
{B816BB44-E97E-4E02-B80A-BEDB5B923A96}.Release|Any CPU.Build.0 = Release|Any CPU
|
{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.ActiveCfg = Release|Any CPU
|
||||||
{4F9BB20B-8030-48AB-A37B-23796459D516}.Release|Any CPU.Build.0 = 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.ActiveCfg = Debug|Any CPU
|
||||||
{4F9BB20B-8030-48AB-A37B-23796459D516}.Debug|Any CPU.Build.0 = Release-Server|linux-arm
|
{4F9BB20B-8030-48AB-A37B-23796459D516}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(NestedProjects) = preSolution
|
GlobalSection(NestedProjects) = preSolution
|
||||||
{CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A}
|
{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 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 Boolean GetBoolean(this JsonNode n, String propName) => n.Get<Boolean>(propName);
|
||||||
|
|
||||||
public static UInt32 GetUInt32(this JsonNode n, String propName) => n.Get<UInt32>(propName);
|
public static UInt32 GetUInt32(this JsonNode n, String propName) => n.Get<UInt32>(propName);
|
||||||
|
@ -43,7 +43,7 @@ public static class JsonNodeAccessors
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return n.TryGet<String>(propName);
|
return n.TryGet<String>(propName) ?? "";
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
|
|
|
@ -118,7 +118,7 @@ public static class StringUtils
|
||||||
return String.Join("", strings);
|
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);
|
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);
|
public UnixTime Created => Json.GetUInt32("syscreated").Apply(UnixTime.FromTicks);
|
||||||
|
|
||||||
// Settings
|
// Settings
|
||||||
|
|
||||||
public String Name => Json.GetString("name");
|
public String Name => Json.GetString("name");
|
||||||
public String Notes => Json.GetString("notes");
|
public String Notes => Json.GetString("notes");
|
||||||
public String PhoneNumber => Json.GetString("phonenumber");
|
public String PhoneNumber => Json.GetString("phonenumber");
|
||||||
|
|
|
@ -31,6 +31,16 @@ public static class Requests
|
||||||
.WithHeader("X-Authorization", account.Auth);
|
.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)
|
public static IFlurlRequest AllInstallationsRequest(this VrmAccount account)
|
||||||
{
|
{
|
||||||
return ApiRoot
|
return ApiRoot
|
||||||
|
|
|
@ -50,6 +50,20 @@ public class VrmAccount : IDisposable
|
||||||
.ToArray(vrmReply.Records.Count);
|
.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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (IsLoggedIn)
|
if (IsLoggedIn)
|
||||||
|
|
|
@ -84,6 +84,7 @@ def init_modbus(tty):
|
||||||
def failed(response):
|
def failed(response):
|
||||||
# type: (ModbusResponse) -> bool
|
# type: (ModbusResponse) -> bool
|
||||||
|
|
||||||
|
# Todo 'ModbusIOException' object has no attribute 'function_code'
|
||||||
return response.function_code > 0x80
|
return response.function_code > 0x80
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue