Update SodistoreMax code

This commit is contained in:
Noe 2025-03-17 16:37:22 +01:00
parent 90a4257bbf
commit 55f6b4baff
319 changed files with 33208 additions and 2118 deletions
csharp
App
App_backup

View File

@ -55,22 +55,22 @@ public static class WebsocketManager
//Console.WriteLine("Installation ID is "+installationConnection.Key);
if (installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) < TimeSpan.FromMinutes(60)){
Console.WriteLine("Installation ID is "+installationConnection.Key + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
}
if (installationConnection.Value.Product==(int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60))
{
//Console.WriteLine("Installation ID is "+installationConnection.Key + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
//Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp);
//Console.WriteLine("timestamp now is is "+(DateTime.Now));
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.Salidomo && f.Id == installationConnection.Key);
Console.WriteLine("Installation ID is "+installation.Name + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
installation.Status = (int)StatusType.Offline;
installation.Apply(Db.Update);
installationConnection.Value.Status = (int)StatusType.Offline;
if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);}
else{Console.WriteLine("NONE IS CONNECTED TO THAT INSTALLATION-------------------------------------------------------------");}
}
}
Console.WriteLine("FINISHED WITH UPDATING\n");
@ -84,7 +84,7 @@ public static class WebsocketManager
{
var installation = Db.GetInstallationById(installationId);
var installationConnection = InstallationConnections[installationId];
Console.WriteLine("Update all the connected websockets for installation " + installationId);
Console.WriteLine("Update all the connected websockets for installation " + installation.Name);
var jsonObject = new
{

View File

@ -29,4 +29,16 @@
<Folder Include="resources\" />
</ItemGroup>
<ItemGroup>
<Compile Remove="src\temp\**" />
</ItemGroup>
<ItemGroup>
<EmbeddedResource Remove="src\temp\**" />
</ItemGroup>
<ItemGroup>
<None Remove="src\temp\**" />
</ItemGroup>
</Project>

View File

@ -1,5 +1,7 @@
using InnovEnergy.App.SaliMax.Ess;
using InnovEnergy.Lib.Utils;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using static System.Double;
namespace InnovEnergy.App.SaliMax.AggregationService;
@ -14,7 +16,7 @@ public static class Aggregator
// Calculate the time until the next rounded hour
var timeUntilNextHour = nextRoundedHour - currentDateTime;
// Output the current and next rounded hour times
Console.WriteLine("------------------------------------------HourlyDataAggregationManager-------------------------------------------");
Console.WriteLine("Current Date and Time: " + currentDateTime);
@ -22,7 +24,7 @@ public static class Aggregator
// Output the time until the next rounded hour
Console.WriteLine("Waiting for " + timeUntilNextHour.TotalMinutes + " minutes...");
Console.WriteLine("-----------------------------------------------------------------------------------------------------------------");
// Wait until the next rounded hour
await Task.Delay(timeUntilNextHour);
@ -30,7 +32,7 @@ public static class Aggregator
{
try
{
AggregatedData hourlyAggregatedData = CreateHourlyData("LogDirectory",DateTime.Now.AddHours(-1).ToUnixTime(),DateTime.Now.ToUnixTime());
AggregatedData hourlyAggregatedData = CreateHourlyData("JsonLogDirectory",DateTime.Now.AddHours(-1).ToUnixTime(),DateTime.Now.ToUnixTime());
hourlyAggregatedData.Save("HourlyData");
}
catch (Exception e)
@ -83,14 +85,14 @@ public static class Aggregator
private static void DeleteHourlyData(String myDirectory, Int64 beforeTimestamp)
{
var csvFiles = Directory.GetFiles(myDirectory, "*.csv");
var jsonFiles = Directory.GetFiles(myDirectory, "*.json");
Console.WriteLine("Delete data before"+beforeTimestamp);
foreach (var csvFile in csvFiles)
foreach (var jsonFile in jsonFiles)
{
if (IsFileWithinTimeRange(csvFile, 0, beforeTimestamp))
if (IsFileWithinTimeRange(jsonFile, 0, beforeTimestamp))
{
File.Delete(csvFile);
Console.WriteLine($"Deleted hourly data file: {csvFile}");
File.Delete(jsonFile);
Console.WriteLine($"Deleted hourly data file: {jsonFile}");
}
}
}
@ -99,7 +101,7 @@ public static class Aggregator
private static AggregatedData CreateHourlyData(String myDirectory, Int64 afterTimestamp, Int64 beforeTimestamp)
{
// Get all CSV files in the specified directory
var csvFiles = Directory.GetFiles(myDirectory, "*.csv");
var jsonFiles = Directory.GetFiles(myDirectory, "*.json");
var batterySoc = new List<Double>();
var pvPowerSum = new List<Double>();
var heatingPower = new List<Double>();
@ -111,83 +113,63 @@ public static class Aggregator
Console.WriteLine("File timestamp should start after "+ afterTimestamp);
foreach (var csvFile in csvFiles)
foreach (var jsonFile in jsonFiles)
{
if (csvFile == "LogDirectory/log.csv")
if (jsonFile == "LogDirectory/log.json")
{
continue;
}
if (IsFileWithinTimeRange(csvFile, afterTimestamp, beforeTimestamp))
if (IsFileWithinTimeRange(jsonFile, afterTimestamp, beforeTimestamp))
{
using var reader = new StreamReader(csvFile);
while (!reader.EndOfStream)
try
{
// Read and parse JSON
var jsonData = File.ReadAllText(jsonFile);
var line = reader.ReadLine();
var lines = line?.Split(';');
// Assuming there are always three columns (variable name and its value)
if (lines is { Length: 3 })
// Step 2: Find the first '{' character and trim everything before it
int startIndex = jsonData.IndexOf('{');
if (startIndex != -1)
{
var variableName = lines[0].Trim();
jsonData = jsonData.Substring(startIndex); // Trim everything before '{'
}
if (TryParse(lines[1].Trim(), out var value))
{
switch (variableName)
{
case "/Battery/Soc":
batterySoc.Add(value);
break;
case "/PvOnDc/DcWh" :
pvPowerSum.Add(value);
break;
var jsonObject = JObject.Parse(jsonData);
case "/Battery/Dc/Power":
if (value < 0)
{
batteryDischargePower.Add(value);
}
else
{
batteryChargePower.Add(value);
}
break;
case "/GridMeter/ActivePowerExportT3":
// we are using different register to check which value from the grid meter we need to use
// At the moment register 8002 amd 8012. in KWh
gridPowerExport.Add(value);
break;
case "/GridMeter/ActivePowerImportT3":
gridPowerImport.Add(value);
break;
case "/Battery/HeatingPower":
heatingPower.Add(value);
break;
// Add more cases as needed
default:
// Code to execute when variableName doesn't match any condition
break;
}
}
if (jsonObject["Battery"] != null && jsonObject["Battery"]["Soc"] != null)
{
batterySoc.Add((double)jsonObject["Battery"]["Soc"]);
}
if (jsonObject["PvOnDc"] != null && jsonObject["PvOnDc"]["DcWh"] != null)
{
pvPowerSum.Add((double)jsonObject["PvOnDc"]["DcWh"]);
}
if (jsonObject["Battery"] != null && jsonObject["Battery"]["Dc"]["Power"] != null)
{
double batteryPower = (double)jsonObject["Battery"]["Dc"]["Power"];
if (batteryPower < 0)
batteryDischargePower.Add(batteryPower);
else
{
//Handle cases where variableValue is not a valid number
// Console.WriteLine(
// $"Invalid numeric value for variable {variableName}:{lines[1].Trim()}");
}
batteryChargePower.Add(batteryPower);
}
else
if (jsonObject["GridMeter"] != null && jsonObject["GridMeter"]["ActivePowerExportT3"] != null)
{
// Handle invalid column format
//Console.WriteLine("Invalid format in column");
gridPowerExport.Add((double)jsonObject["GridMeter"]["ActivePowerExportT3"]);
}
if (jsonObject["GridMeter"] != null && jsonObject["GridMeter"]["ActivePowerImportT3"] != null)
{
gridPowerImport.Add((double)jsonObject["GridMeter"]["ActivePowerImportT3"]);
}
if (jsonObject["Battery"] != null && jsonObject["Battery"]["HeatingPower"] != null)
{
heatingPower.Add((double)jsonObject["Battery"]["HeatingPower"]);
}
}
catch (Exception e)
{
Console.WriteLine($"Failed to parse JSON file {jsonFile}: {e.Message}");
}
}
}
@ -213,14 +195,14 @@ public static class Aggregator
AggregatedData aggregatedData = new AggregatedData
{
MaxSoc = dMaxSoc,
MinSoc = dMinSoc,
DischargingBatteryPower = dischargingEnergy,
ChargingBatteryPower = chargingEnergy,
GridExportPower = dSumGridExportPower,
GridImportPower = dSumGridImportPower,
PvPower = dSumPvPower,
HeatingPower = heatingPowerAvg
MaxSoc = Math.Round(dMaxSoc, 2),
MinSoc = Math.Round(dMinSoc, 2) ,
DischargingBatteryPower = Math.Round(dischargingEnergy, 2) ,
ChargingBatteryPower = Math.Round(chargingEnergy, 2) ,
GridExportPower = Math.Round(dSumGridExportPower, 2) ,
GridImportPower = Math.Round(dSumGridImportPower, 2) ,
PvPower = Math.Round(dSumPvPower, 2) ,
HeatingPower = Math.Round(heatingPowerAvg, 2)
};
// Print the stored CSV data for verification
@ -245,7 +227,7 @@ public static class Aggregator
private static AggregatedData CreateDailyData(String myDirectory, Int64 afterTimestamp, Int64 beforeTimestamp)
{
// Get all CSV files in the specified directory
var csvFiles = Directory.GetFiles(myDirectory, "*.csv");
var jsonFiles = Directory.GetFiles(myDirectory, "*.json");
var batterySoc = new List<Double>();
var pvPower = new List<Double>();
var gridPowerImport = new List<Double>();
@ -258,79 +240,71 @@ public static class Aggregator
Console.WriteLine("File timestamp should start after "+ afterTimestamp);
foreach (var csvFile in csvFiles)
foreach (var jsonFile in jsonFiles)
{
if (csvFile == "LogDirectory/log.csv")
if (jsonFile == "JsonLogDirectory/log.json")
{
continue;
}
if (IsFileWithinTimeRange(csvFile, afterTimestamp, beforeTimestamp))
if (IsFileWithinTimeRange(jsonFile, afterTimestamp, beforeTimestamp))
{
using var reader = new StreamReader(csvFile);
while (!reader.EndOfStream)
try
{
var jsonData = File.ReadAllText(jsonFile);
//Console.WriteLine("Parse file "+jsonFile);
var line = reader.ReadLine();
var lines = line?.Split(';');
// Parse JSON into a Dictionary
var jsonDict = JsonConvert.DeserializeObject<Dictionary<string, Double>>(jsonData);
// Assuming there are always three columns (variable name and its value)
if (lines is { Length: 3 })
// Process values
foreach (var (variableName, value) in jsonDict)
{
var variableName = lines[0].Trim();
if (TryParse(lines[1].Trim(), out var value))
switch (variableName)
{
switch (variableName)
{
case "/MinSoc" or "/MaxSoc":
batterySoc.Add(value);
break;
case "/PvPower":
pvPower.Add(value);
break;
case "MinSoc":
case "MaxSoc":
batterySoc.Add(value);
break;
case "/DischargingBatteryPower" :
batteryDischargePower.Add(value);
break;
case "/ChargingBatteryPower" :
batteryChargePower.Add(value);
break;
case "/GridExportPower":
gridPowerExport.Add(value);
break;
case "/GridImportPower":
gridPowerImport.Add(value);
break;
case "/HeatingPower":
heatingPowerAvg.Add(value);
break;
// Add more cases as needed
default:
// Code to execute when variableName doesn't match any condition
break;
}
case "PvPower":
pvPower.Add(value);
break;
}
else
{
//Handle cases where variableValue is not a valid number
// Console.WriteLine(
// $"Invalid numeric value for variable {variableName}:{lines[1].Trim()}");
case "DischargingBatteryPower":
batteryDischargePower.Add(value);
break;
case "ChargingBatteryPower":
batteryChargePower.Add(value);
break;
case "GridExportPower":
gridPowerExport.Add(value);
break;
case "GridImportPower":
gridPowerImport.Add(value);
break;
case "HeatingPower":
heatingPowerAvg.Add(value);
break;
default:
// Ignore unknown variables
break;
}
}
else
{
// Handle invalid column format
//Console.WriteLine("Invalid format in column");
}
}
catch (Exception e)
{
Console.WriteLine($"Failed to parse JSON file {jsonFile}: {e.Message}");
}
}
}

View File

@ -6,6 +6,7 @@ using InnovEnergy.App.SaliMax.Devices;
using InnovEnergy.App.SaliMax.SystemConfig;
using InnovEnergy.Lib.Units;
using InnovEnergy.Lib.Utils;
using Newtonsoft.Json;
using static System.Text.Json.JsonSerializer;
namespace InnovEnergy.App.SaliMax.AggregationService;
@ -30,7 +31,7 @@ public class AggregatedData
{
var date = DateTime.Now.ToUnixTime();
var defaultHDataPath = Environment.CurrentDirectory + "/" + directory + "/";
var dataFilePath = defaultHDataPath + date + ".csv";
var dataFilePath = defaultHDataPath + date + ".json";
if (!Directory.Exists(defaultHDataPath))
{
@ -41,8 +42,11 @@ public class AggregatedData
try
{
var csvString = this.ToCsv();
File.WriteAllText(dataFilePath, csvString);
// Convert the object to a JSON string
var jsonString = JsonConvert.SerializeObject(this, Formatting.None);
// Write JSON to file
File.WriteAllText(dataFilePath, jsonString);
}
catch (Exception e)
{
@ -54,21 +58,21 @@ public class AggregatedData
public static void DeleteDailyData(String directory)
{
var csvFiles = Directory.GetFiles(directory, "*.csv");
foreach (var csvFile in csvFiles)
var jsonFiles = Directory.GetFiles(directory, "*.json");
foreach (var jsonFile in jsonFiles)
{
File.Delete(csvFile);
Console.WriteLine($"Deleted daily data file: {csvFile}");
File.Delete(jsonFile);
Console.WriteLine($"Deleted daily data file: {jsonFile}");
}
}
public async Task<Boolean> PushToS3()
{
var csv = this.ToCsv();
var jsonString = JsonConvert.SerializeObject(this, Formatting.None);
if (_S3Config is null)
return false;
var s3Path = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd") + ".csv";
var s3Path = DateTime.Now.AddDays(-1).ToString("yyyy-MM-dd") + ".json";
var request = _S3Config.CreatePutRequest(s3Path);
// Compress CSV data to a byte array
@ -78,11 +82,11 @@ public class AggregatedData
//Create a zip directory and put the compressed file inside
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
{
var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive
var entry = archive.CreateEntry("data.json", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive
using (var entryStream = entry.Open())
using (var writer = new StreamWriter(entryStream))
{
writer.Write(csv);
writer.Write(jsonString);
}
}
@ -98,9 +102,6 @@ public class AggregatedData
// Upload the compressed data (ZIP archive) to S3
var response = await request.PutAsync(stringContent);
//
// var request = _S3Config.CreatePutRequest(s3Path);
// var response = await request.PutAsync(new StringContent(csv));
if (response.StatusCode != 200)
{
@ -113,18 +114,5 @@ public class AggregatedData
return true;
}
// public static HourlyData? Load(String dataFilePath)
// {
// try
// {
// var csvString = File.ReadAllText(dataFilePath);
// return Deserialize<HourlyData>(jsonString)!;
// }
// catch (Exception e)
// {
// $"Failed to read config file {dataFilePath}, using default config\n{e}".WriteLine();
// return null;
// }
// }
}

View File

@ -6,7 +6,7 @@ public class LogFileConcatenator
{
private readonly string _logDirectory;
public LogFileConcatenator(String logDirectory = "LogDirectory/")
public LogFileConcatenator(String logDirectory = "JsonLogDirectory/")
{
_logDirectory = logDirectory;
}
@ -14,7 +14,7 @@ public class LogFileConcatenator
public String ConcatenateFiles(int numberOfFiles)
{
var logFiles = Directory
.GetFiles(_logDirectory, "log_*.csv")
.GetFiles(_logDirectory, "log_*.json")
.OrderByDescending(file => file)
.Take(numberOfFiles)
.OrderBy(file => file)

View File

@ -8,7 +8,7 @@ public static class Logger
//private const Int32 MaxFileSizeBytes = 2024 * 30; // TODO: move to settings
private const Int32 MaxLogFileCount = 5000; // TODO: move to settings
private const String LogFilePath = "LogDirectory/log.csv"; // TODO: move to settings
private const String LogFilePath = "JsonLogDirectory/log.json"; // TODO: move to settings
// ReSharper disable once InconsistentNaming
private static readonly ILogger _logger = new CustomLogger(LogFilePath, MaxLogFileCount);
@ -27,7 +27,7 @@ public static class Logger
public static T LogError<T>(this T t) where T : notnull
{
_logger.LogError(t.ToString()); // TODO: check warning
//_logger.LogError(t.ToString()); // TODO: check warning
return t;
}

View File

@ -25,6 +25,7 @@ using InnovEnergy.Lib.Protocols.Modbus.Channels;
using InnovEnergy.Lib.Units;
using InnovEnergy.Lib.Utils;
using InnovEnergy.App.SaliMax.DataTypes;
using Newtonsoft.Json;
using static System.Int32;
using static InnovEnergy.App.SaliMax.AggregationService.Aggregator;
using static InnovEnergy.App.SaliMax.MiddlewareClasses.MiddlewareAgent;
@ -809,12 +810,74 @@ internal static class Program
sc.ResetAlarmsAndWarnings = true;
}
private static void InsertIntoJson(Dictionary<string, object> jsonDict, String[] keys, string value)
{
Dictionary<string, object> currentDict = jsonDict;
for (int i = 1; i < keys.Length; i++) // Start at 1 to skip empty root
{
string key = keys[i];
if (!currentDict.ContainsKey(key))
{
currentDict[key] = new Dictionary<string, object>();
}
if (i == keys.Length - 1) // Last key, store the value
{
if (!value.Contains(",") && double.TryParse(value, out double doubleValue)) // Try to parse value as a number
{
currentDict[key] = Math.Round(doubleValue, 2); // Round to 2 decimal places
}
else
{
currentDict[key] = value; // Store as string if not a number
}
}
else
{
currentDict = (Dictionary<string, object>)currentDict[key];
}
}
}
private static async Task<Boolean> UploadCsv(StatusRecord status, DateTime timeStamp)
{
status.ToJson();
var csv = status.ToCsv().LogInfo();
//status.ToJson();
var csv = status.ToCsv();
Dictionary<string, object> jsonData = new Dictionary<string, object>();
//Console.WriteLine(csv);
foreach (var line in csv.Split('\n'))
{
if (string.IsNullOrWhiteSpace(line)) continue;
string[] parts = line.Split(';');
//if (parts.Length < 2) continue;
string keyPath = parts[0];
string value = parts[1];
string unit = parts.Length > 2 ? parts[2].Trim() : "";
//Console.WriteLine(line);
// Console.WriteLine($"Key: {keyPath}, Value: {value}, Unit: {unit}");
InsertIntoJson(jsonData, keyPath.Split('/'), value);
}
string jsonOutput = JsonConvert.SerializeObject(jsonData, Formatting.None);
jsonOutput.LogInfo();
await RestApiSavingFile(csv);
@ -835,7 +898,7 @@ internal static class Program
var logFileConcatenator = new LogFileConcatenator();
var s3Path = timeStamp.ToUnixTime() + ".csv";
var s3Path = timeStamp.ToUnixTime() + ".json";
s3Path.WriteLine("");
var csvToSend = logFileConcatenator.ConcatenateFiles(NbrOfFileToConcatenate);
@ -902,7 +965,7 @@ internal static class Program
//Create a zip directory and put the compressed file inside
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
{
var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive
var entry = archive.CreateEntry("data.json", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive
using (var entryStream = entry.Open())
using (var writer = new StreamWriter(entryStream))
{

View File

@ -208,4 +208,215 @@ public class CombinedAdamRelaysRecord : IRelaysRecord
public IEnumerable<Boolean> K3InverterIsConnectedToIslandBus => _recordAdam6360D.K3InverterIsConnectedToIslandBus;
}
using System.Reflection.Metadata;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
#pragma warning disable CS8602 // Dereference of a possibly null reference.
public class CombinedAdamRelaysRecord : IRelaysRecord
{
private const UInt16 SlowFreq = 3000;
private const UInt16 HighFreq = 1000;
public CombinedAdamRelaysRecord(RelaysRecordAdam6060? relaysRecordAdam6060, RelaysRecordAdam6360D? relaysRecordAdam6360D)
{
_recordAdam6060 = relaysRecordAdam6060;
_recordAdam6360D = relaysRecordAdam6360D;
}
private static RelaysRecordAdam6060? _recordAdam6060;
private static RelaysRecordAdam6360D? _recordAdam6360D;
public static IRelaysRecord Instance { get; } = new CombinedAdamRelaysRecord(_recordAdam6060, _recordAdam6360D);
public Boolean K1GridBusIsConnectedToGrid => _recordAdam6360D.K1GridBusIsConnectedToGrid;
public Boolean K2IslandBusIsConnectedToGridBus => _recordAdam6360D.K2IslandBusIsConnectedToGridBus;
public Boolean FiWarning => _recordAdam6360D.FiWarning;
public Boolean FiError => _recordAdam6360D.FiError;
public Boolean K2ConnectIslandBusToGridBus
{
get => _recordAdam6360D.K2ConnectIslandBusToGridBus;
set => _recordAdam6360D.K2ConnectIslandBusToGridBus = value;
}
public Boolean Inverter1WagoStatus => _recordAdam6360D.Inverter1WagoStatus;
public Boolean Inverter2WagoStatus => _recordAdam6360D.Inverter2WagoStatus;
public Boolean Inverter3WagoStatus => _recordAdam6360D.Inverter3WagoStatus;
public Boolean Inverter4WagoStatus => _recordAdam6360D.Inverter4WagoStatus;
public Boolean Dc1WagoStatus => _recordAdam6060.Dc1WagoStatus;
public Boolean Dc2WagoStatus => _recordAdam6060.Dc2WagoStatus;
public Boolean Dc3WagoStatus => _recordAdam6060.Dc3WagoStatus;
public Boolean Dc4WagoStatus => _recordAdam6060.Dc4WagoStatus;
public Boolean DcSystemControlWagoStatus => _recordAdam6060.DcSystemControlWagoStatus;
public Boolean LedGreen { get => _recordAdam6360D.LedGreen; set => _recordAdam6360D.LedGreen = value;}
public Boolean LedRed { get => _recordAdam6360D.LedRed; set => _recordAdam6360D.LedRed = value;}
public Boolean Harvester1Step => _recordAdam6360D.Harvester1Step;
public Boolean Harvester2Step => _recordAdam6360D.Harvester2Step;
public Boolean Harvester3Step => _recordAdam6360D.Harvester3Step;
public Boolean Harvester4Step => _recordAdam6360D.Harvester4Step;
public UInt16 DigitalOutput0Mode { get => _recordAdam6360D.DigitalOutput0Mode; set => _recordAdam6360D.DigitalOutput0Mode = value; }
public UInt16 DigitalOutput1Mode
{
get => _recordAdam6360D.DigitalOutput1Mode;
set => _recordAdam6360D.DigitalOutput1Mode = value;
}
public UInt16 DigitalOutput2Mode
{
get => _recordAdam6360D.DigitalOutput2Mode;
set => _recordAdam6360D.DigitalOutput2Mode = value;
}
public UInt16 DigitalOutput3Mode
{
get => _recordAdam6360D.DigitalOutput3Mode;
set => _recordAdam6360D.DigitalOutput3Mode = value;
}
public UInt16 DigitalOutput4Mode
{
get => _recordAdam6360D.DigitalOutput4Mode;
set => _recordAdam6360D.DigitalOutput4Mode = value;
}
public UInt16 DigitalOutput5Mode
{
get => _recordAdam6360D.DigitalOutput5Mode;
set => _recordAdam6360D.DigitalOutput5Mode = value;
}
public Boolean Do0StartPulse { get => _recordAdam6360D.Do0Pulse; set => _recordAdam6360D.Do0Pulse = value; }
public Boolean Do1StartPulse { get => _recordAdam6360D.Do1Pulse; set => _recordAdam6360D.Do1Pulse = value; }
public Boolean Do2StartPulse { get => _recordAdam6360D.Do2Pulse; set => _recordAdam6360D.Do2Pulse = value; }
public Boolean Do3StartPulse { get => _recordAdam6360D.Do3Pulse; set => _recordAdam6360D.Do3Pulse = value; }
public Boolean Do4StartPulse { get => _recordAdam6360D.Do4Pulse; set => _recordAdam6360D.Do4Pulse = value; }
public Boolean Do5StartPulse { get => _recordAdam6360D.Do5Pulse; set => _recordAdam6360D.Do5Pulse = value; }
public UInt16 PulseOut0LowTime { get => _recordAdam6360D.PulseOut0LowTime; set => _recordAdam6360D.PulseOut0LowTime = value; }
public UInt16 PulseOut1LowTime { get => _recordAdam6360D.PulseOut1LowTime; set => _recordAdam6360D.PulseOut1LowTime = value; }
public UInt16 PulseOut2LowTime { get => _recordAdam6360D.PulseOut2LowTime; set => _recordAdam6360D.PulseOut2LowTime = value; }
public UInt16 PulseOut3LowTime { get => _recordAdam6360D.PulseOut3LowTime; set => _recordAdam6360D.PulseOut3LowTime = value; }
public UInt16 PulseOut4LowTime { get => _recordAdam6360D.PulseOut4LowTime; set => _recordAdam6360D.PulseOut4LowTime = value; }
public UInt16 PulseOut5LowTime { get => _recordAdam6360D.PulseOut5LowTime; set => _recordAdam6360D.PulseOut5LowTime = value; }
public UInt16 PulseOut0HighTime { get => _recordAdam6360D.PulseOut0HighTime; set => _recordAdam6360D.PulseOut0HighTime = value; }
public UInt16 PulseOut1HighTime { get => _recordAdam6360D.PulseOut1HighTime; set => _recordAdam6360D.PulseOut1HighTime = value; }
public UInt16 PulseOut2HighTime { get => _recordAdam6360D.PulseOut2HighTime; set => _recordAdam6360D.PulseOut2HighTime = value; }
public UInt16 PulseOut3HighTime { get => _recordAdam6360D.PulseOut3HighTime; set => _recordAdam6360D.PulseOut3HighTime = value; }
public UInt16 PulseOut4HighTime { get => _recordAdam6360D.PulseOut4HighTime; set => _recordAdam6360D.PulseOut4HighTime = value; }
public UInt16 PulseOut5HighTime { get => _recordAdam6360D.PulseOut5HighTime; set => _recordAdam6360D.PulseOut5HighTime = value; }
/**************************** Green LED *********************************/
public void PerformSolidGreenLed()
{
DigitalOutput0Mode = 0;
DigitalOutput1Mode = 0;
LedGreen = true;
LedRed = false;
}
public void PerformSlowFlashingGreenLed()
{
PulseOut0HighTime = SlowFreq;
PulseOut0LowTime = SlowFreq;
DigitalOutput0Mode = 2;
Do0StartPulse = true;
Do1StartPulse = false; // make sure the red LED is off
Console.WriteLine("Green Slow Flashing Starting");
}
public void PerformFastFlashingGreenLed()
{
PulseOut0HighTime = HighFreq;
PulseOut0LowTime = HighFreq;
DigitalOutput0Mode = 2;
Do0StartPulse = true;
Do1StartPulse = false;// make sure the red LED is off
Console.WriteLine("Green Slow Flashing Starting");
}
/**************************** Orange LED *********************************/
public void PerformSolidOrangeLed()
{
DigitalOutput0Mode = 0;
DigitalOutput1Mode = 0;
LedGreen = true;
LedRed = true;
}
public void PerformSlowFlashingOrangeLed()
{
PerformSlowFlashingGreenLed();
PerformSlowFlashingRedLed();
Do0StartPulse = true;
Do1StartPulse = true;
Console.WriteLine("Orange Slow Flashing Starting");
}
public void PerformFastFlashingOrangeLed()
{
PerformFastFlashingGreenLed();
PerformFastFlashingRedLed();
Do0StartPulse = true;
Do1StartPulse = true;
Console.WriteLine("Orange Fast Flashing Starting");
}
/**************************** RED LED *********************************/
public void PerformSolidRedLed()
{
DigitalOutput0Mode = 0;
DigitalOutput1Mode = 0;
LedGreen = false;
LedRed = true;
}
public void PerformSlowFlashingRedLed()
{
PulseOut1HighTime = SlowFreq;
PulseOut1LowTime = SlowFreq;
DigitalOutput1Mode = 2;
Do0StartPulse = false; // make sure the green LED is off
Do1StartPulse = true;
Console.WriteLine("Red Slow Flashing Starting");
}
public void PerformFastFlashingRedLed()
{
PulseOut1HighTime = HighFreq;
PulseOut1LowTime = HighFreq;
DigitalOutput1Mode = 2;
Do0StartPulse = false; // make sure the green LED is off
Do1StartPulse = true;
Console.WriteLine("Red Fast Flashing Starting");
}
public RelaysRecordAdam6360D? GetAdam6360DRecord()
{
return _recordAdam6360D;
}
public RelaysRecordAdam6060? GetAdam6060Record()
{
return _recordAdam6060;
}
public IEnumerable<Boolean> K3InverterIsConnectedToIslandBus => _recordAdam6360D.K3InverterIsConnectedToIslandBus;
}

View File

@ -1,6 +1,84 @@
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public interface IRelaysRecord
{
Boolean K1GridBusIsConnectedToGrid { get; }
Boolean K2IslandBusIsConnectedToGridBus { get; }
IEnumerable<Boolean> K3InverterIsConnectedToIslandBus { get; }
Boolean FiWarning { get; }
Boolean FiError { get; }
Boolean K2ConnectIslandBusToGridBus { get; set; }
// Boolean Inverter1WagoRelay { get; set; } // to add in the future
// Boolean Inverter2WagoRelay { get; set; } // to add in the future
// Boolean Inverter3WagoRelay { get; set; } // to add in the future
// Boolean Inverter4WagoRelay { get; set; } // to add in the future
Boolean Inverter1WagoStatus { get; }
Boolean Inverter2WagoStatus { get; }
Boolean Inverter3WagoStatus { get; }
Boolean Inverter4WagoStatus { get; }
Boolean Dc1WagoStatus { get; } // to test
Boolean Dc2WagoStatus { get; } // to test
Boolean Dc3WagoStatus { get; } // to test
Boolean Dc4WagoStatus { get; } // to test
Boolean DcSystemControlWagoStatus { get; } // to test
Boolean LedGreen { get; set; }
Boolean LedRed { get; }
Boolean Harvester1Step { get; }
Boolean Harvester2Step { get; }
Boolean Harvester3Step { get; }
Boolean Harvester4Step { get; }
Boolean Do0StartPulse { get; set; }
Boolean Do1StartPulse { get; set; }
Boolean Do2StartPulse { get; set; }
Boolean Do3StartPulse { get; set; }
Boolean Do4StartPulse { get; set; }
Boolean Do5StartPulse { get; set; }
UInt16 DigitalOutput0Mode { get; set; }
UInt16 DigitalOutput1Mode { get; set; }
UInt16 DigitalOutput2Mode { get; set; }
UInt16 DigitalOutput3Mode { get; set; }
UInt16 DigitalOutput4Mode { get; set; }
UInt16 DigitalOutput5Mode { get; set; }
UInt16 PulseOut0LowTime { get; set; }
UInt16 PulseOut1LowTime { get; set; }
UInt16 PulseOut2LowTime { get; set; }
UInt16 PulseOut3LowTime { get; set; }
UInt16 PulseOut4LowTime { get; set; }
UInt16 PulseOut5LowTime { get; set; }
UInt16 PulseOut0HighTime { get; set; }
UInt16 PulseOut1HighTime { get; set; }
UInt16 PulseOut2HighTime { get; set; }
UInt16 PulseOut3HighTime { get; set; }
UInt16 PulseOut4HighTime { get; set; }
UInt16 PulseOut5HighTime { get; set; }
void PerformSolidGreenLed();
void PerformSlowFlashingGreenLed();
void PerformFastFlashingGreenLed();
void PerformSolidOrangeLed();
void PerformSlowFlashingOrangeLed();
void PerformFastFlashingOrangeLed();
void PerformSolidRedLed();
void PerformSlowFlashingRedLed();
void PerformFastFlashingRedLed();
}
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public interface IRelaysRecord
{
Boolean K1GridBusIsConnectedToGrid { get; }

View File

@ -11,6 +11,46 @@ public class RelaysDeviceAdam6360
public RelaysDeviceAdam6360(Channel channel) => AdamDevice6360D = new Adam6360DDevice(channel, 2);
public RelaysRecordAdam6360D? Read()
{
try
{
return AdamDevice6360D.Read();
}
catch (Exception e)
{
$"Failed to read from {nameof(RelaysDeviceAdam6360)}\n{e}".LogError();
return null;
}
}
public void Write(RelaysRecordAdam6360D r)
{
try
{
AdamDevice6360D.Write(r);
}
catch (Exception e)
{
$"Failed to write to {nameof(RelaysDeviceAdam6360)}\n{e}".LogError();
}
}
}
using InnovEnergy.Lib.Devices.Adam6360D;
using InnovEnergy.Lib.Protocols.Modbus.Channels;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysDeviceAdam6360
{
private Adam6360DDevice AdamDevice6360D { get; }
public RelaysDeviceAdam6360(String hostname) => AdamDevice6360D = new Adam6360DDevice(hostname, 2);
public RelaysDeviceAdam6360(Channel channel) => AdamDevice6360D = new Adam6360DDevice(channel, 2);
public RelaysRecordAdam6360D? Read()
{
try

View File

@ -11,6 +11,44 @@ public class RelaysDeviceAdam6060
public RelaysDeviceAdam6060(Channel channel) => AdamDevice6060 = new Adam6060Device(channel, 2);
public RelaysRecordAdam6060? Read()
{
try
{
return AdamDevice6060.Read();
}
catch (Exception e)
{
$"Failed to read from {nameof(RelaysDeviceAdam6060)}\n{e}".LogError();
return null;
}
}
public void Write(RelaysRecordAdam6060 r)
{
try
{
AdamDevice6060.Write(r);
}
catch (Exception e)
{
$"Failed to write to {nameof(RelaysDeviceAdam6060)}\n{e}".LogError();
}
}
}
using InnovEnergy.Lib.Devices.Adam6060;
using InnovEnergy.Lib.Protocols.Modbus.Channels;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysDeviceAdam6060
{
private Adam6060Device AdamDevice6060 { get; }
public RelaysDeviceAdam6060(String hostname) => AdamDevice6060 = new Adam6060Device(hostname, 2);
public RelaysDeviceAdam6060(Channel channel) => AdamDevice6060 = new Adam6060Device(channel, 2);
public RelaysRecordAdam6060? Read()
{
try

View File

@ -2,6 +2,43 @@ using InnovEnergy.Lib.Devices.Amax5070;
using InnovEnergy.Lib.Protocols.Modbus.Channels;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysDeviceAmax
{
private Amax5070Device AmaxDevice { get; }
public RelaysDeviceAmax(Channel channel) => AmaxDevice = new Amax5070Device(channel);
public RelaysRecordAmax? Read()
{
try
{
return AmaxDevice.Read();
}
catch (Exception e)
{
$"Failed to read from {nameof(RelaysDeviceAmax)}\n{e}".LogError();
return null;
}
}
public void Write(RelaysRecordAmax r)
{
try
{
AmaxDevice.Write(r);
}
catch (Exception e)
{
$"Failed to write to {nameof(RelaysDeviceAmax)}\n{e}".LogError();
}
}
}
using InnovEnergy.Lib.Devices.Amax5070;
using InnovEnergy.Lib.Protocols.Modbus.Channels;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysDeviceAmax

View File

@ -1,6 +1,30 @@
using InnovEnergy.Lib.Devices.Adam6060;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysRecordAdam6060
{
private readonly Adam6060Registers _Regs;
private RelaysRecordAdam6060(Adam6060Registers regs) => _Regs = regs;
public Boolean Dc1WagoStatus => _Regs.DigitalInput0; // to test
public Boolean Dc2WagoStatus => _Regs.DigitalInput1; // to test
public Boolean Dc3WagoStatus => _Regs.DigitalInput4; // to test
public Boolean Dc4WagoStatus => _Regs.DigitalInput5; // to test
public Boolean DcSystemControlWagoStatus => _Regs.DigitalInput3; // to test
public static implicit operator Adam6060Registers(RelaysRecordAdam6060 d) => d._Regs;
public static implicit operator RelaysRecordAdam6060(Adam6060Registers d) => new RelaysRecordAdam6060(d);
}
using InnovEnergy.Lib.Devices.Adam6060;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysRecordAdam6060

View File

@ -2,6 +2,87 @@ using InnovEnergy.Lib.Devices.Adam6360D;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysRecordAdam6360D
{
private readonly Adam6360DRegisters _Regs;
private RelaysRecordAdam6360D(Adam6360DRegisters regs) => _Regs = regs;
public Boolean K1GridBusIsConnectedToGrid => _Regs.DigitalInput6;
public Boolean K2IslandBusIsConnectedToGridBus => !_Regs.DigitalInput4;
public Boolean Inverter1WagoStatus => _Regs.DigitalInput8;
public Boolean Inverter2WagoStatus => _Regs.DigitalInput9;
public Boolean Inverter3WagoStatus => _Regs.DigitalInput10;
public Boolean Inverter4WagoStatus => _Regs.DigitalInput11;
public IEnumerable<Boolean> K3InverterIsConnectedToIslandBus
{
get
{
yield return K3Inverter1IsConnectedToIslandBus;
yield return K3Inverter2IsConnectedToIslandBus;
yield return K3Inverter3IsConnectedToIslandBus;
yield return K3Inverter4IsConnectedToIslandBus;
}
}
private Boolean K3Inverter1IsConnectedToIslandBus => !_Regs.DigitalInput0; // change it to private should be ok
private Boolean K3Inverter2IsConnectedToIslandBus => !_Regs.DigitalInput1;
private Boolean K3Inverter3IsConnectedToIslandBus => !_Regs.DigitalInput2;
private Boolean K3Inverter4IsConnectedToIslandBus => !_Regs.DigitalInput3;
public Boolean FiWarning => !_Regs.DigitalInput5;
public Boolean FiError => !_Regs.DigitalInput7;
public Boolean Harvester1Step =>_Regs.DigitalOutput2;
public Boolean Harvester2Step =>_Regs.DigitalOutput3;
public Boolean Harvester3Step =>_Regs.DigitalOutput4;
public Boolean Harvester4Step =>_Regs.DigitalOutput5;
public Boolean LedGreen { get =>_Regs.DigitalOutput0; set => _Regs.DigitalOutput0 = value;}
public Boolean LedRed { get =>_Regs.DigitalOutput1; set => _Regs.DigitalOutput1 = value;}
public Boolean Do0Pulse { get => _Regs.Do0Pulse; set => _Regs.Do0Pulse = value;}
public Boolean Do1Pulse { get => _Regs.Do1Pulse; set => _Regs.Do1Pulse = value;}
public Boolean Do2Pulse { get => _Regs.Do2Pulse; set => _Regs.Do2Pulse = value;}
public Boolean Do3Pulse { get => _Regs.Do3Pulse; set => _Regs.Do3Pulse = value;}
public Boolean Do4Pulse { get => _Regs.Do4Pulse; set => _Regs.Do4Pulse = value;}
public Boolean Do5Pulse { get => _Regs.Do5Pulse; set => _Regs.Do5Pulse = value;}
public UInt16 PulseOut0LowTime { get => _Regs.PulseOut0LowTime; set => _Regs.PulseOut0LowTime = value;} //in milleseconds
public UInt16 PulseOut1LowTime { get => _Regs.PulseOut1LowTime; set => _Regs.PulseOut1LowTime = value;}
public UInt16 PulseOut2LowTime { get => _Regs.PulseOut2LowTime; set => _Regs.PulseOut2LowTime = value;}
public UInt16 PulseOut3LowTime { get => _Regs.PulseOut3LowTime; set => _Regs.PulseOut3LowTime = value;}
public UInt16 PulseOut4LowTime { get => _Regs.PulseOut4LowTime; set => _Regs.PulseOut4LowTime = value;}
public UInt16 PulseOut5LowTime { get => _Regs.PulseOut5LowTime; set => _Regs.PulseOut5LowTime = value;}
public UInt16 PulseOut0HighTime { get => _Regs.PulseOut0HighTime; set => _Regs.PulseOut0HighTime = value;} // in milleseconds
public UInt16 PulseOut1HighTime { get => _Regs.PulseOut1HighTime; set => _Regs.PulseOut1HighTime = value;}
public UInt16 PulseOut2HighTime { get => _Regs.PulseOut2HighTime; set => _Regs.PulseOut2HighTime = value;}
public UInt16 PulseOut3HighTime { get => _Regs.PulseOut3HighTime; set => _Regs.PulseOut3HighTime = value;}
public UInt16 PulseOut4HighTime { get => _Regs.PulseOut4HighTime; set => _Regs.PulseOut4HighTime = value;}
public UInt16 PulseOut5HighTime { get => _Regs.PulseOut5HighTime; set => _Regs.PulseOut5HighTime = value;}
public UInt16 DigitalOutput0Mode { get => _Regs.DigitalOutput0Mode; set => _Regs.DigitalOutput0Mode = value;} // To test: 0, 1 or 2
public UInt16 DigitalOutput1Mode { get => _Regs.DigitalOutput1Mode; set => _Regs.DigitalOutput1Mode = value;}
public UInt16 DigitalOutput2Mode { get => _Regs.DigitalOutput2Mode; set => _Regs.DigitalOutput2Mode = value;}
public UInt16 DigitalOutput3Mode { get => _Regs.DigitalOutput3Mode; set => _Regs.DigitalOutput3Mode = value;}
public UInt16 DigitalOutput4Mode { get => _Regs.DigitalOutput4Mode; set => _Regs.DigitalOutput4Mode = value;}
public UInt16 DigitalOutput5Mode { get => _Regs.DigitalOutput5Mode; set => _Regs.DigitalOutput5Mode = value;}
public Boolean K2ConnectIslandBusToGridBus { get => _Regs.Relay0; set => _Regs.Relay0 = value;}
public static implicit operator Adam6360DRegisters(RelaysRecordAdam6360D d) => d._Regs;
public static implicit operator RelaysRecordAdam6360D(Adam6360DRegisters d) => new RelaysRecordAdam6360D(d);
}
using InnovEnergy.Lib.Devices.Adam6360D;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysRecordAdam6360D
{
private readonly Adam6360DRegisters _Regs;

View File

@ -2,6 +2,140 @@ using InnovEnergy.Lib.Devices.Amax5070;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysRecordAmax : IRelaysRecord
{
private readonly Amax5070Registers _Regs;
private RelaysRecordAmax(Amax5070Registers regs) => _Regs = regs;
public Boolean K1GridBusIsConnectedToGrid => _Regs.DigitalInput22;
public Boolean K2IslandBusIsConnectedToGridBus => !_Regs.DigitalInput20;
public Boolean Inverter1WagoStatus => _Regs.DigitalInput0;
public Boolean Inverter2WagoStatus => _Regs.DigitalInput1;
public Boolean Inverter3WagoStatus => _Regs.DigitalInput2;
public Boolean Inverter4WagoStatus => _Regs.DigitalInput3;
public Boolean Dc1WagoStatus => _Regs.DigitalInput6;
public Boolean Dc2WagoStatus => _Regs.DigitalInput7;
public Boolean Dc3WagoStatus => _Regs.DigitalInput10;
public Boolean Dc4WagoStatus => _Regs.DigitalInput11;
public Boolean DcSystemControlWagoStatus => _Regs.DigitalInput9;
public Boolean LedGreen
{
get => _Regs.DigitalOutput0;
set => _Regs.DigitalOutput0 = value;
}
public Boolean LedRed => _Regs.DigitalOutput1;
public Boolean Harvester1Step => _Regs.DigitalOutput2;
public Boolean Harvester2Step => _Regs.DigitalOutput3;
public Boolean Harvester3Step => _Regs.DigitalOutput4;
public Boolean Harvester4Step => _Regs.DigitalOutput5;
public Boolean Do0StartPulse { get; set; }
public Boolean Do1StartPulse { get; set; }
public Boolean Do2StartPulse { get; set; }
public Boolean Do3StartPulse { get; set; }
public Boolean Do4StartPulse { get; set; }
public Boolean Do5StartPulse { get; set; }
public UInt16 DigitalOutput0Mode { get; set; }
public UInt16 DigitalOutput1Mode { get; set; }
public UInt16 DigitalOutput2Mode { get; set; }
public UInt16 DigitalOutput3Mode { get; set; }
public UInt16 DigitalOutput4Mode { get; set; }
public UInt16 DigitalOutput5Mode { get; set; }
public UInt16 PulseOut0LowTime { get; set; }
public UInt16 PulseOut1LowTime { get; set; }
public UInt16 PulseOut2LowTime { get; set; }
public UInt16 PulseOut3LowTime { get; set; }
public UInt16 PulseOut4LowTime { get; set; }
public UInt16 PulseOut5LowTime { get; set; }
public UInt16 PulseOut0HighTime { get; set; }
public UInt16 PulseOut1HighTime { get; set; }
public UInt16 PulseOut2HighTime { get; set; }
public UInt16 PulseOut3HighTime { get; set; }
public UInt16 PulseOut4HighTime { get; set; }
public UInt16 PulseOut5HighTime { get; set; }
public void PerformSolidGreenLed()
{
Console.WriteLine("Solid Green: This is not yet implemented ");
}
public void PerformSlowFlashingGreenLed()
{
Console.WriteLine("Slow Flashing Green: This is not yet implemented ");
}
public void PerformFastFlashingGreenLed()
{
Console.WriteLine("Fast Flashing Green: This is not yet implemented ");
}
public void PerformSolidOrangeLed()
{
Console.WriteLine("Solid Orange: This is not yet implemented ");
}
public void PerformSlowFlashingOrangeLed()
{
Console.WriteLine("Slow Flashing Orange: This is not yet implemented ");
}
public void PerformFastFlashingOrangeLed()
{
Console.WriteLine("Fast Flashing Orange: This is not yet implemented ");
}
public void PerformSolidRedLed()
{
Console.WriteLine("Solid Red: This is not yet implemented ");
}
public void PerformSlowFlashingRedLed()
{
Console.WriteLine("Slow Flashing Red: This is not yet implemented ");
}
public void PerformFastFlashingRedLed()
{
Console.WriteLine("Fast Flashing Red: This is not yet implemented ");
}
public IEnumerable<Boolean> K3InverterIsConnectedToIslandBus
{
get
{
yield return K3Inverter1IsConnectedToIslandBus;
yield return K3Inverter2IsConnectedToIslandBus;
yield return K3Inverter3IsConnectedToIslandBus;
yield return K3Inverter4IsConnectedToIslandBus;
}
}
private Boolean K3Inverter1IsConnectedToIslandBus => !_Regs.DigitalInput16;
private Boolean K3Inverter2IsConnectedToIslandBus => !_Regs.DigitalInput17;
private Boolean K3Inverter3IsConnectedToIslandBus => !_Regs.DigitalInput18;
private Boolean K3Inverter4IsConnectedToIslandBus => !_Regs.DigitalInput19;
public Boolean FiWarning => !_Regs.DigitalInput21;
public Boolean FiError => !_Regs.DigitalInput23;
public Boolean K2ConnectIslandBusToGridBus
{
get => _Regs.Relay23;
set => _Regs.Relay23 = value;
}
public static implicit operator Amax5070Registers(RelaysRecordAmax d) => d._Regs;
public static implicit operator RelaysRecordAmax(Amax5070Registers d) => new RelaysRecordAmax(d);
}
using InnovEnergy.Lib.Devices.Amax5070;
namespace InnovEnergy.App.SaliMax.SaliMaxRelays;
public class RelaysRecordAmax : IRelaysRecord
{
private readonly Amax5070Registers _Regs;

View File

@ -1,5 +1,7 @@
using InnovEnergy.App.SodiStoreMax.Ess;
using InnovEnergy.Lib.Utils;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using static System.Double;
namespace InnovEnergy.App.SodiStoreMax.AggregationService;
@ -30,7 +32,7 @@ public static class Aggregator
{
try
{
AggregatedData hourlyAggregatedData = CreateHourlyData("LogDirectory",DateTime.Now.AddHours(-1).ToUnixTime(),DateTime.Now.ToUnixTime());
AggregatedData hourlyAggregatedData = CreateHourlyData("JsonLogDirectory",DateTime.Now.AddHours(-1).ToUnixTime(),DateTime.Now.ToUnixTime());
hourlyAggregatedData.Save("HourlyData");
}
catch (Exception e)
@ -83,14 +85,14 @@ public static class Aggregator
private static void DeleteHourlyData(String myDirectory, Int64 beforeTimestamp)
{
var csvFiles = Directory.GetFiles(myDirectory, "*.csv");
var jsonFiles = Directory.GetFiles(myDirectory, "*.json");
Console.WriteLine("Delete data before"+beforeTimestamp);
foreach (var csvFile in csvFiles)
foreach (var jsonFile in jsonFiles)
{
if (IsFileWithinTimeRange(csvFile, 0, beforeTimestamp))
if (IsFileWithinTimeRange(jsonFile, 0, beforeTimestamp))
{
File.Delete(csvFile);
Console.WriteLine($"Deleted hourly data file: {csvFile}");
File.Delete(jsonFile);
Console.WriteLine($"Deleted hourly data file: {jsonFile}");
}
}
}
@ -99,7 +101,7 @@ public static class Aggregator
private static AggregatedData CreateHourlyData(String myDirectory, Int64 afterTimestamp, Int64 beforeTimestamp)
{
// Get all CSV files in the specified directory
var csvFiles = Directory.GetFiles(myDirectory, "*.csv");
var jsonFiles = Directory.GetFiles(myDirectory, "*.json");
var batterySoc = new List<Double>();
var pvPowerSum = new List<Double>();
var heatingPower = new List<Double>();
@ -111,84 +113,135 @@ public static class Aggregator
Console.WriteLine("File timestamp should start after "+ afterTimestamp);
foreach (var csvFile in csvFiles)
foreach (var jsonFile in jsonFiles)
{
if (csvFile == "LogDirectory/log.csv")
if (jsonFile == "JsonLogDirectory/log.json")
{
continue;
}
if (IsFileWithinTimeRange(csvFile, afterTimestamp, beforeTimestamp))
if (IsFileWithinTimeRange(jsonFile, afterTimestamp, beforeTimestamp))
{
using var reader = new StreamReader(csvFile);
while (!reader.EndOfStream)
try
{
// Read and parse JSON
var jsonData = File.ReadAllText(jsonFile);
var line = reader.ReadLine();
var lines = line?.Split(';');
// Assuming there are always three columns (variable name and its value)
if (lines is { Length: 3 })
// Step 2: Find the first '{' character and trim everything before it
int startIndex = jsonData.IndexOf('{');
if (startIndex != -1)
{
var variableName = lines[0].Trim();
if (TryParse(lines[1].Trim(), out var value))
{
switch (variableName)
{
case "/Battery/Soc":
batterySoc.Add(value);
break;
case "/PvOnDc/DcWh" :
pvPowerSum.Add(value);
break;
case "/Battery/Dc/Power":
if (value < 0)
{
batteryDischargePower.Add(value);
}
else
{
batteryChargePower.Add(value);
}
break;
case "/GridMeter/ActivePowerExportT3":
// we are using different register to check which value from the grid meter we need to use
// At the moment register 8002 amd 8012. in KWh
gridPowerExport.Add(value);
break;
case "/GridMeter/ActivePowerImportT3":
gridPowerImport.Add(value);
break;
case "/Battery/HeatingPower":
heatingPower.Add(value);
break;
// Add more cases as needed
default:
// Code to execute when variableName doesn't match any condition
break;
}
}
else
{
//Handle cases where variableValue is not a valid number
// Console.WriteLine(
// $"Invalid numeric value for variable {variableName}:{lines[1].Trim()}");
}
jsonData = jsonData.Substring(startIndex); // Trim everything before '{'
}
else
var jsonObject = JObject.Parse(jsonData);
if (jsonObject["Battery"] != null && jsonObject["Battery"]["Soc"] != null)
{
// Handle invalid column format
//Console.WriteLine("Invalid format in column");
batterySoc.Add((double)jsonObject["Battery"]["Soc"]);
}
if (jsonObject["PvOnDc"] != null && jsonObject["PvOnDc"]["DcWh"] != null)
{
pvPowerSum.Add((double)jsonObject["PvOnDc"]["DcWh"]);
}
if (jsonObject["Battery"] != null && jsonObject["Battery"]["Dc"]["Power"] != null)
{
double batteryPower = (double)jsonObject["Battery"]["Dc"]["Power"];
if (batteryPower < 0)
batteryDischargePower.Add(batteryPower);
else
batteryChargePower.Add(batteryPower);
}
if (jsonObject["GridMeter"] != null && jsonObject["GridMeter"]["ActivePowerExportT3"] != null)
{
gridPowerExport.Add((double)jsonObject["GridMeter"]["ActivePowerExportT3"]);
}
if (jsonObject["GridMeter"] != null && jsonObject["GridMeter"]["ActivePowerImportT3"] != null)
{
gridPowerImport.Add((double)jsonObject["GridMeter"]["ActivePowerImportT3"]);
}
if (jsonObject["Battery"] != null && jsonObject["Battery"]["HeatingPower"] != null)
{
heatingPower.Add((double)jsonObject["Battery"]["HeatingPower"]);
}
}
catch (Exception e)
{
Console.WriteLine($"Failed to parse JSON file {jsonFile}: {e.Message}");
}
// using var reader = new StreamReader(jsonFile);
//
// while (!reader.EndOfStream)
// {
//
// var line = reader.ReadLine();
// var lines = line?.Split(';');
//
// // Assuming there are always three columns (variable name and its value)
// if (lines is { Length: 3 })
// {
// var variableName = lines[0].Trim();
//
// if (TryParse(lines[1].Trim(), out var value))
// {
// switch (variableName)
// {
// case "/Battery/Soc":
// batterySoc.Add(value);
// break;
//
// case "/PvOnDc/DcWh" :
// pvPowerSum.Add(value);
// break;
//
// case "/Battery/Dc/Power":
//
// if (value < 0)
// {
// batteryDischargePower.Add(value);
// }
// else
// {
// batteryChargePower.Add(value);
//
// }
// break;
//
// case "/GridMeter/ActivePowerExportT3":
// // we are using different register to check which value from the grid meter we need to use
// // At the moment register 8002 amd 8012. in KWh
// gridPowerExport.Add(value);
// break;
// case "/GridMeter/ActivePowerImportT3":
// gridPowerImport.Add(value);
// break;
// case "/Battery/HeatingPower":
// heatingPower.Add(value);
// break;
// // Add more cases as needed
// default:
// // Code to execute when variableName doesn't match any condition
// break;
// }
//
// }
// else
// {
// //Handle cases where variableValue is not a valid number
// // Console.WriteLine(
// // $"Invalid numeric value for variable {variableName}:{lines[1].Trim()}");
// }
// }
// else
// {
// // Handle invalid column format
// //Console.WriteLine("Invalid format in column");
// }
//}
}
}
@ -244,8 +297,8 @@ public static class Aggregator
private static AggregatedData CreateDailyData(String myDirectory, Int64 afterTimestamp, Int64 beforeTimestamp)
{
// Get all CSV files in the specified directory
var csvFiles = Directory.GetFiles(myDirectory, "*.csv");
// Get all JSON files in the specified directory
var jsonFiles = Directory.GetFiles(myDirectory, "*.json");
var batterySoc = new List<Double>();
var pvPower = new List<Double>();
var gridPowerImport = new List<Double>();
@ -258,79 +311,133 @@ public static class Aggregator
Console.WriteLine("File timestamp should start after "+ afterTimestamp);
foreach (var csvFile in csvFiles)
foreach (var jsonFile in jsonFiles)
{
if (csvFile == "LogDirectory/log.csv")
if (jsonFile == "JsonLogDirectory/log.json")
{
continue;
}
if (IsFileWithinTimeRange(csvFile, afterTimestamp, beforeTimestamp))
if (IsFileWithinTimeRange(jsonFile, afterTimestamp, beforeTimestamp))
{
using var reader = new StreamReader(csvFile);
while (!reader.EndOfStream)
try
{
var jsonData = File.ReadAllText(jsonFile);
//Console.WriteLine("Parse file "+jsonFile);
var line = reader.ReadLine();
var lines = line?.Split(';');
// Parse JSON into a Dictionary
var jsonDict = JsonConvert.DeserializeObject<Dictionary<string, Double>>(jsonData);
// Assuming there are always three columns (variable name and its value)
if (lines is { Length: 3 })
// Process values
foreach (var (variableName, value) in jsonDict)
{
var variableName = lines[0].Trim();
if (TryParse(lines[1].Trim(), out var value))
switch (variableName)
{
switch (variableName)
{
case "/MinSoc" or "/MaxSoc":
batterySoc.Add(value);
break;
case "/PvPower":
pvPower.Add(value);
break;
case "MinSoc":
case "MaxSoc":
batterySoc.Add(value);
break;
case "/DischargingBatteryPower" :
batteryDischargePower.Add(value);
break;
case "/ChargingBatteryPower" :
batteryChargePower.Add(value);
break;
case "/GridExportPower":
gridPowerExport.Add(value);
break;
case "/GridImportPower":
gridPowerImport.Add(value);
break;
case "/HeatingPower":
heatingPowerAvg.Add(value);
break;
// Add more cases as needed
default:
// Code to execute when variableName doesn't match any condition
break;
}
case "PvPower":
pvPower.Add(value);
break;
}
else
{
//Handle cases where variableValue is not a valid number
// Console.WriteLine(
// $"Invalid numeric value for variable {variableName}:{lines[1].Trim()}");
case "DischargingBatteryPower":
batteryDischargePower.Add(value);
break;
case "ChargingBatteryPower":
batteryChargePower.Add(value);
break;
case "GridExportPower":
gridPowerExport.Add(value);
break;
case "GridImportPower":
gridPowerImport.Add(value);
break;
case "HeatingPower":
heatingPowerAvg.Add(value);
break;
default:
// Ignore unknown variables
break;
}
}
else
{
// Handle invalid column format
//Console.WriteLine("Invalid format in column");
}
}
catch (Exception e)
{
Console.WriteLine($"Failed to parse JSON file {jsonFile}: {e.Message}");
}
// using var reader = new StreamReader(csvFile);
//
// while (!reader.EndOfStream)
// {
//
// var line = reader.ReadLine();
// var lines = line?.Split(';');
//
// // Assuming there are always three columns (variable name and its value)
// if (lines is { Length: 3 })
// {
// var variableName = lines[0].Trim();
//
// if (TryParse(lines[1].Trim(), out var value))
// {
// switch (variableName)
// {
// case "/MinSoc" or "/MaxSoc":
// batterySoc.Add(value);
// break;
//
// case "/PvPower":
// pvPower.Add(value);
// break;
//
// case "/DischargingBatteryPower" :
// batteryDischargePower.Add(value);
// break;
//
// case "/ChargingBatteryPower" :
// batteryChargePower.Add(value);
// break;
//
// case "/GridExportPower":
// gridPowerExport.Add(value);
// break;
//
// case "/GridImportPower":
// gridPowerImport.Add(value);
// break;
//
// case "/HeatingPower":
// heatingPowerAvg.Add(value);
// break;
// // Add more cases as needed
// default:
// // Code to execute when variableName doesn't match any condition
// break;
// }
//
// }
// else
// {
// //Handle cases where variableValue is not a valid number
// // Console.WriteLine(
// // $"Invalid numeric value for variable {variableName}:{lines[1].Trim()}");
// }
// }
// else
// {
// // Handle invalid column format
// //Console.WriteLine("Invalid format in column");
// }
//}
}
}
@ -360,6 +467,7 @@ public static class Aggregator
Console.WriteLine("CSV data reading and storage completed.");
Console.WriteLine("CSV data reading and storage completed.");
Console.WriteLine("-----------------------------------------------------------------------------------------------------------------");

View File

@ -6,7 +6,7 @@ public class LogFileConcatenator
{
private readonly string _logDirectory;
public LogFileConcatenator(String logDirectory = "LogDirectory/")
public LogFileConcatenator(String logDirectory = "JsonLogDirectory/")
{
_logDirectory = logDirectory;
}
@ -14,7 +14,7 @@ public class LogFileConcatenator
public String ConcatenateFiles(int numberOfFiles)
{
var logFiles = Directory
.GetFiles(_logDirectory, "log_*.csv")
.GetFiles(_logDirectory, "log_*.json")
.OrderByDescending(file => file)
.Take(numberOfFiles)
.OrderBy(file => file)

View File

@ -9,7 +9,7 @@ public static class Logger
//private const Int32 MaxFileSizeBytes = 2024 * 30; // TODO: move to settings
private const Int32 MaxLogFileCount = 5000; // TODO: move to settings
private const String LogFilePath = "LogDirectory/log.csv"; // TODO: move to settings
private const String LogFilePath = "JsonLogDirectory/log.json"; // TODO: move to settings
// ReSharper disable once InconsistentNaming
private static readonly ILogger _logger = new CustomLogger(LogFilePath, MaxLogFileCount);
@ -22,19 +22,19 @@ public static class Logger
public static T LogDebug<T>(this T t) where T : notnull
{
_logger.LogDebug(t.ToString()); // TODO: check warning
// _logger.LogDebug(t.ToString()); // TODO: check warning
return t;
}
public static T LogError<T>(this T t) where T : notnull
{
_logger.LogError(t.ToString()); // TODO: check warning
// _logger.LogError(t.ToString()); // TODO: check warning
return t;
}
public static T LogWarning<T>(this T t) where T : notnull
{
_logger.LogWarning(t.ToString()); // TODO: check warning
// _logger.LogWarning(t.ToString()); // TODO: check warning
return t;
}
}

View File

@ -30,6 +30,7 @@ using InnovEnergy.Lib.Units;
using InnovEnergy.Lib.Utils;
using InnovEnergy.App.SodiStoreMax.DataTypes;
using InnovEnergy.Lib.Utils.Net;
using Newtonsoft.Json;
using static System.Int32;
using static InnovEnergy.App.SodiStoreMax.AggregationService.Aggregator;
using static InnovEnergy.App.SodiStoreMax.MiddlewareClasses.MiddlewareAgent;
@ -798,12 +799,68 @@ internal static class Program
sc.ResetAlarmsAndWarnings = true;
}
private static void InsertIntoJson(Dictionary<string, object> jsonDict, String[] keys, string value)
{
Dictionary<string, object> currentDict = jsonDict;
for (int i = 1; i < keys.Length; i++) // Start at 1 to skip empty root
{
string key = keys[i];
if (!currentDict.ContainsKey(key))
{
currentDict[key] = new Dictionary<string, object>();
}
if (i == keys.Length - 1) // Last key, store the value
{
if (!value.Contains(",") && double.TryParse(value, out double doubleValue)) // Try to parse value as a number
{
currentDict[key] = Math.Round(doubleValue, 2); // Round to 2 decimal places
}
else
{
currentDict[key] = value; // Store as string if not a number
}
}
else
{
currentDict = (Dictionary<string, object>)currentDict[key];
}
}
}
private static async Task<Boolean> UploadCsv(StatusRecord status, DateTime timeStamp)
{
var csv = status.ToCsv().LogInfo();
var csv = status.ToCsv();
Dictionary<string, object> jsonData = new Dictionary<string, object>();
//Console.WriteLine(csv);
foreach (var line in csv.Split('\n'))
{
if (string.IsNullOrWhiteSpace(line)) continue;
string[] parts = line.Split(';');
//if (parts.Length < 2) continue;
string keyPath = parts[0];
string value = parts[1];
string unit = parts.Length > 2 ? parts[2].Trim() : "";
//Console.WriteLine(line);
// Console.WriteLine($"Key: {keyPath}, Value: {value}, Unit: {unit}");
InsertIntoJson(jsonData, keyPath.Split('/'), value);
}
string jsonOutput = JsonConvert.SerializeObject(jsonData, Formatting.None);
jsonOutput.LogInfo();
await RestApiSavingFile(csv);
var s3Config = status.Config.S3;
@ -823,7 +880,7 @@ internal static class Program
var logFileConcatenator = new LogFileConcatenator();
var s3Path = timeStamp.ToUnixTime() + ".csv";
var s3Path = timeStamp.ToUnixTime() + ".json";
s3Path.WriteLine("");
var csvToSend = logFileConcatenator.ConcatenateFiles(NbrOfFileToConcatenate);
@ -890,7 +947,7 @@ internal static class Program
//Create a zip directory and put the compressed file inside
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true))
{
var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive
var entry = archive.CreateEntry("data.json", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive
using (var entryStream = entry.Open())
using (var writer = new StreamWriter(entryStream))
{

View File

@ -0,0 +1,239 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../InnovEnergy.App.props" />
<PropertyGroup>
<RootNamespace>InnovEnergy.App.Backend</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AWSSDK.S3" Version="3.7.205.17" />
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="Hellang.Middleware.ProblemDetails" Version="6.5.1" />
<PackageReference Include="Microsoft.AspNet.Identity.Core" Version="2.2.3" />
<PackageReference Include="Microsoft.AspNet.Identity.Owin" Version="2.2.3" />
<PackageReference Include="Microsoft.AspNet.WebApi.Core" Version="5.2.9" />
<PackageReference Include="Microsoft.AspNetCore.Authentication.Certificate" Version="6.0.21" />
<PackageReference Include="Microsoft.AspNetCore.Identity" Version="2.2.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.Core" Version="2.2.5" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="7.0.0" />
<PackageReference Include="Microsoft.Identity.Web" Version="1.26.0" />
<PackageReference Include="Microsoft.Owin.Cors" Version="4.2.2" />
<PackageReference Include="Microsoft.Owin.Host.SystemWeb" Version="4.2.2" />
<PackageReference Include="Microsoft.Owin.Security" Version="4.2.2" />
<PackageReference Include="Microsoft.Owin.Security.Cookies" Version="4.2.2" />
<PackageReference Include="Microsoft.Owin.Security.OAuth" Version="4.2.2" />
<PackageReference Include="RabbitMQ.Client" Version="6.6.0" />
<PackageReference Include="sqlite-net-pcl" Version="1.8.116" />
<PackageReference Include="sqlite-net-sqlcipher" Version="1.9.141-beta" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.2.3" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters" Version="7.0.6" />
<PackageReference Include="Swashbuckle.AspNetCore.Filters.Abstractions" Version="7.0.6" />
<PackageReference Include="System.Data.SQLite.Core" Version="1.0.118" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../Lib/Utils/Utils.csproj" />
<ProjectReference Include="../../Lib/Mailer/Mailer.csproj" />
<ProjectReference Include="../../Lib/S3Utils/S3Utils.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Resources/s3cmd.py">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
<None Remove="DbBackups\db-1698326303.sqlite" />
<None Remove="DbBackups\db-1698327045.sqlite" />
<None Remove="DbBackups\db-1699453468.sqlite" />
<None Remove="DbBackups\db-1699453414.sqlite" />
<None Remove="DbBackups\db-1699453413.sqlite" />
<None Remove="DbBackups\db-1699452457.sqlite" />
<None Remove="DbBackups\db-1699452190.sqlite" />
<None Remove="DbBackups\db-1699452189.sqlite" />
<None Remove="DbBackups\db-1699452183.sqlite" />
<None Remove="DbBackups\db-1699452182.sqlite" />
<None Remove="DbBackups\db-1699452103.sqlite" />
<None Remove="DbBackups\db-1699452102.sqlite" />
<None Remove="DbBackups\db-1699448121.sqlite" />
<None Remove="DbBackups\db-1699448016.sqlite" />
<None Remove="DbBackups\db-1699448015.sqlite" />
<None Remove="DbBackups\db-1699448014.sqlite" />
<None Remove="DbBackups\db-1699441586.sqlite" />
<None Remove="DbBackups\db-1699441583.sqlite" />
<None Remove="DbBackups\db-1699441582.sqlite" />
<None Remove="DbBackups\db-1699440129.sqlite" />
<None Remove="DbBackups\db-1699440124.sqlite" />
<None Remove="DbBackups\db-1699440123.sqlite" />
<None Remove="DbBackups\db-1699438975.sqlite" />
<None Remove="DbBackups\db-1699438972.sqlite" />
<None Remove="DbBackups\db-1699438971.sqlite" />
<None Remove="DbBackups\db-1699438952.sqlite" />
<None Remove="DbBackups\db-1699438945.sqlite" />
<None Remove="DbBackups\db-1699438944.sqlite" />
<None Remove="DbBackups\db-1699438895.sqlite" />
<None Remove="DbBackups\db-1699438888.sqlite" />
<None Remove="DbBackups\db-1699438887.sqlite" />
<None Remove="DbBackups\db-1699437588.sqlite" />
<None Remove="DbBackups\db-1699437586.sqlite" />
<None Remove="DbBackups\db-1699437585.sqlite" />
<None Remove="DbBackups\db-1699437584.sqlite" />
<None Remove="DbBackups\db-1699437551.sqlite" />
<None Remove="DbBackups\db-1699437550.sqlite" />
<None Remove="DbBackups\db-1699437549.sqlite" />
<None Remove="DbBackups\db-1699436793.sqlite" />
<None Remove="DbBackups\db-1699436791.sqlite" />
<None Remove="DbBackups\db-1699436790.sqlite" />
<None Remove="DbBackups\db-1699436653.sqlite" />
<None Remove="DbBackups\db-1699436652.sqlite" />
<None Remove="DbBackups\db-1699436088.sqlite" />
<None Remove="DbBackups\db-1699436067.sqlite" />
<None Remove="DbBackups\db-1699436066.sqlite" />
<None Remove="DbBackups\db-1699434989.sqlite" />
<None Remove="DbBackups\db-1699434979.sqlite" />
<None Remove="DbBackups\db-1699434978.sqlite" />
<None Remove="DbBackups\db-1699434917.sqlite" />
<None Remove="DbBackups\db-1699434916.sqlite" />
<None Remove="DbBackups\db-1699433682.sqlite" />
<None Remove="DbBackups\db-1699433681.sqlite" />
<None Remove="DbBackups\db-1699433494.sqlite" />
<None Remove="DbBackups\db-1699433493.sqlite" />
<None Remove="DbBackups\db-1699432892.sqlite" />
<None Remove="DbBackups\db-1699432891.sqlite" />
<None Remove="DbBackups\db-1699432622.sqlite" />
<None Remove="DbBackups\db-1699432621.sqlite" />
<None Remove="DbBackups\db-1699375972.sqlite" />
<None Remove="DbBackups\db-1699375971.sqlite" />
<None Remove="DbBackups\db-1699375970.sqlite" />
<None Remove="DbBackups\db-1699375582.sqlite" />
<None Remove="DbBackups\db-1699375581.sqlite" />
<None Remove="DbBackups\db-1699375265.sqlite" />
<None Remove="DbBackups\db-1699375264.sqlite" />
<None Remove="DbBackups\db-1699375174.sqlite" />
<None Remove="DbBackups\db-1699375173.sqlite" />
<None Remove="DbBackups\db-1699375167.sqlite" />
<None Remove="DbBackups\db-1699375166.sqlite" />
<None Remove="DbBackups\db-1699374877.sqlite" />
<None Remove="DbBackups\db-1699374876.sqlite" />
<None Remove="DbBackups\db-1699374338.sqlite" />
<None Remove="DbBackups\db-1699374337.sqlite" />
<None Remove="DbBackups\db-1699374216.sqlite" />
<None Remove="DbBackups\db-1699374215.sqlite" />
<None Remove="DbBackups\db-1699369902.sqlite" />
<None Remove="DbBackups\db-1699369901.sqlite" />
<None Remove="DbBackups\db-1699369278.sqlite" />
<None Remove="DbBackups\db-1699369277.sqlite" />
<None Remove="DbBackups\db-1699368950.sqlite" />
<None Remove="DbBackups\db-1699368949.sqlite" />
<None Remove="DbBackups\db-1699368806.sqlite" />
<None Remove="DbBackups\db-1699368805.sqlite" />
<None Remove="DbBackups\db-1699368804.sqlite" />
<None Remove="DbBackups\db-1699366271.sqlite" />
<None Remove="DbBackups\db-1699366256.sqlite" />
<None Remove="DbBackups\db-1699366255.sqlite" />
<None Remove="DbBackups\db-1699366240.sqlite" />
<None Remove="DbBackups\db-1699366239.sqlite" />
<None Remove="DbBackups\db-1699366132.sqlite" />
<None Remove="DbBackups\db-1699365906.sqlite" />
<None Remove="DbBackups\db-1699365905.sqlite" />
<None Remove="DbBackups\db-1698656873.sqlite" />
<None Remove="DbBackups\db-1698656872.sqlite" />
<None Remove="DbBackups\db-1698330524.sqlite" />
<None Remove="DbBackups\db-1698330511.sqlite" />
<None Remove="DbBackups\db-1698330510.sqlite" />
<None Remove="DbBackups\db-1698330455.sqlite" />
<None Remove="DbBackups\db-1698330444.sqlite" />
<None Remove="DbBackups\db-1698330406.sqlite" />
<None Remove="DbBackups\db-1698330386.sqlite" />
<None Remove="DbBackups\db-1698330385.sqlite" />
<None Remove="DbBackups\db-1698329746.sqlite" />
<None Remove="DbBackups\db-1698329745.sqlite" />
<None Remove="DbBackups\db-1698329744.sqlite" />
<None Remove="DbBackups\db-1698329652.sqlite" />
<None Remove="DbBackups\db-1698329603.sqlite" />
<None Remove="DbBackups\db-1698329346.sqlite" />
<None Remove="DbBackups\db-1698329331.sqlite" />
<None Remove="DbBackups\db-1698329329.sqlite" />
<None Remove="DbBackups\db-1698329274.sqlite" />
<None Remove="DbBackups\db-1698329086.sqlite" />
<None Remove="DbBackups\db-1698329070.sqlite" />
<None Remove="DbBackups\db-1698329067.sqlite" />
<None Remove="DbBackups\db-1698329009.sqlite" />
<None Remove="DbBackups\db-1698328961.sqlite" />
<None Remove="DbBackups\db-1698328621.sqlite" />
<None Remove="DbBackups\db-1698328605.sqlite" />
<None Remove="DbBackups\db-1698328557.sqlite" />
<None Remove="DbBackups\db-1698328538.sqlite" />
<None Remove="DbBackups\db-1698328537.sqlite" />
<None Remove="DbBackups\db-1698328516.sqlite" />
<None Remove="DbBackups\db-1698328504.sqlite" />
<None Remove="DbBackups\db-1698328489.sqlite" />
<None Remove="DbBackups\db-1698328461.sqlite" />
<None Remove="DbBackups\db-1698328447.sqlite" />
<None Remove="DbBackups\db-1698328381.sqlite" />
<None Remove="DbBackups\db-1698328203.sqlite" />
<None Remove="DbBackups\db-1698328201.sqlite" />
<None Remove="DbBackups\db-1698328184.sqlite" />
<None Remove="DbBackups\db-1698328174.sqlite" />
<None Remove="DbBackups\db-1698328173.sqlite" />
<None Remove="DbBackups\db-1698327908.sqlite" />
<None Remove="DbBackups\db-1698327870.sqlite" />
<None Remove="DbBackups\db-1698327855.sqlite" />
<None Remove="DbBackups\db-1698327854.sqlite" />
<None Remove="DbBackups\db-1698327853.sqlite" />
<None Remove="DbBackups\db-1698327737.sqlite" />
<None Remove="DbBackups\db-1698327658.sqlite" />
<None Remove="DbBackups\db-1698327641.sqlite" />
<None Remove="DbBackups\db-1698327640.sqlite" />
<None Remove="DbBackups\db-1698327639.sqlite" />
<None Remove="DbBackups\db-1698327576.sqlite" />
<None Remove="DbBackups\db-1698327461.sqlite" />
<None Remove="DbBackups\db-1698327450.sqlite" />
<None Remove="DbBackups\db-1698327449.sqlite" />
<None Remove="DbBackups\db-1698327398.sqlite" />
<None Remove="DbBackups\db-1698327351.sqlite" />
<None Remove="DbBackups\db-1698327339.sqlite" />
<None Remove="DbBackups\db-1698327338.sqlite" />
<None Remove="DbBackups\db-1698327227.sqlite" />
<None Remove="DbBackups\db-1698327194.sqlite" />
<None Remove="DbBackups\db-1698327133.sqlite" />
<None Remove="DbBackups\db-1698327071.sqlite" />
<None Remove="DbBackups\db-1698327022.sqlite" />
<None Remove="DbBackups\db-1698326991.sqlite" />
<None Remove="DbBackups\db-1698326990.sqlite" />
<None Remove="DbBackups\db-1698326807.sqlite" />
<None Remove="DbBackups\db-1698326334.sqlite" />
<None Remove="DbBackups\db-1698326333.sqlite" />
<None Remove="DbBackups\db-1698326332.sqlite" />
<None Remove="DbBackups\db-1698326302.sqlite" />
<None Remove="DbBackups\db-1698325689.sqlite" />
<None Remove="DbBackups\db-1698325688.sqlite" />
</ItemGroup>
<ItemGroup>
<Content Update="Resources/urlAndKey.json">
<CopyToPublishDirectory>Never</CopyToPublishDirectory>
</Content>
</ItemGroup>
<ItemGroup>
<Reference Include="RabbitMQ.Client">
<HintPath>..\..\..\..\..\..\.nuget\packages\rabbitmq.client\6.6.0\lib\netstandard2.0\RabbitMQ.Client.dll</HintPath>
</Reference>
</ItemGroup>
<ItemGroup>
<Compile Remove="DataTypes\CsvName.cs" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.5.002.0
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend", "Backend.csproj", "{161624D7-33B9-48B8-BA05-303DCFAEB03E}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{161624D7-33B9-48B8-BA05-303DCFAEB03E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{161624D7-33B9-48B8-BA05-303DCFAEB03E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{161624D7-33B9-48B8-BA05-303DCFAEB03E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{161624D7-33B9-48B8-BA05-303DCFAEB03E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {A377D3C5-E56D-4A16-AC4B-F3B0BB4CFCCE}
EndGlobalSection
EndGlobal

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,20 @@
namespace InnovEnergy.App.Backend.DataTypes;
public class Configuration
{
public Double MinimumSoC { get; set; }
public Double GridSetPoint { get; set; }
public CalibrationChargeType CalibrationChargeState { get; set; }
public DateTime CalibrationChargeDate { get; set; }
public String GetConfigurationString()
{
return $"MinimumSoC: {MinimumSoC}, GridSetPoint: {GridSetPoint}, CalibrationChargeState: {CalibrationChargeState}, CalibrationChargeDate: {CalibrationChargeDate}";
}
}
public enum CalibrationChargeType
{
RepetitivelyEvery,
AdditionallyOnce,
ChargePermanently
}

View File

@ -0,0 +1,24 @@
using SQLite;
namespace InnovEnergy.App.Backend.DataTypes;
public abstract class LogEntry
{
[PrimaryKey, AutoIncrement]
public Int64 Id { get; set; }
public Int64 InstallationId { get; set; }
public String Description { get; set; } = null!;
public String Date { get; set; } = null!;
public String Time { get; set; } = null!;
public String DeviceCreatedTheMessage{ get; set; } = null!;
public Boolean Seen { get; set; }
}
// Derived class for errors
public class Error : LogEntry
{
}
public class Warning : LogEntry
{
}

View File

@ -0,0 +1,3 @@
namespace InnovEnergy.App.Backend.DataTypes;
public class Folder : TreeNode { }

View File

@ -0,0 +1,49 @@
using SQLite;
namespace InnovEnergy.App.Backend.DataTypes;
public enum ProductType
{
Salimax = 0,
Salidomo = 1,
SodioHome =2
}
public enum StatusType
{
Offline = -1,
Green = 0,
Warning = 1,
Alarm = 2
}
public class Installation : TreeNode
{
//Each installation has 2 roles, a read role and a write role.
//There are 2 keys per role a public key and a secret
//Product can be 0 or 1, 0 for Salimax, 1 for Salidomo
public String Location { get; set; } = "";
public String Region { get; set; } = "";
public String Country { get; set; } = "";
public String VpnIp { get; set; } = "";
public String InstallationName { get; set; } = "";
public String S3Region { get; set; } = "sos-ch-dk-2";
public String S3Provider { get; set; } = "exo.io";
public String S3WriteKey { get; set; } = "";
public String S3Key { get; set; } = "";
public String S3WriteSecret { get; set; } = "";
public String S3Secret { get; set; } = "";
public int S3BucketId { get; set; } = 0;
public String ReadRoleId { get; set; } = "";
public String WriteRoleId { get; set; } = "";
public Boolean TestingMode { get; set; } = false;
public int Status { get; set; } = -1;
public int Product { get; set; } = (int)ProductType.Salimax;
public int Device { get; set; } = 0;
public string SerialNumber { get; set; } = "";
[Ignore]
public String OrderNumbers { get; set; }
public String VrmLink { get; set; } = "";
}

View File

@ -0,0 +1,445 @@
using System.Text.Json;
using InnovEnergy.Lib.S3Utils;
using InnovEnergy.Lib.S3Utils.DataTypes;
using System.Diagnostics.CodeAnalysis;
using System.Net;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Text;
using System.Text.Json.Nodes;
using InnovEnergy.App.Backend.Database;
namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class ExoCmd
{
[UnconditionalSuppressMessage("Trimming", "IL2026:Members annotated with 'RequiresUnreferencedCodeAttribute' require dynamic access otherwise can break functionality when trimming application code", Justification = "<Pending>")]
public static readonly S3Credentials S3Credentials = JsonSerializer.Deserialize<S3Credentials>(File.OpenRead("./Resources/exoscaleS3.json"))!;
private static Byte[] HmacSha256Digest(String message, String secret)
{
var encoding = new UTF8Encoding();
var keyBytes = encoding.GetBytes(secret);
var messageBytes = encoding.GetBytes(message);
var cryptographer = new System.Security.Cryptography.HMACSHA256(keyBytes);
var bytes = cryptographer.ComputeHash(messageBytes);
return bytes;
}
private static String BuildSignature(String method, String path, String? data, Int64 time)
{
var messageToSign = "";
messageToSign += method + " /v2/" + path + "\n";
messageToSign += data + "\n";
// query strings
messageToSign += "\n";
// headers
messageToSign += "\n";
messageToSign += time;
//Console.WriteLine("Message to sign:\n" + messageToSign);
var hmac = HmacSha256Digest(messageToSign, S3Credentials.Secret);
return Convert.ToBase64String(hmac);
}
private static String BuildSignature(String method, String path, Int64 time)
{
return BuildSignature(method, path, null, time);
}
public static async Task<JsonArray> GetAccessKeys()
{
var url = "https://api-ch-dk-2.exoscale.com/v2/api-key";
var method = "api-key";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
var authheader = "credential="+S3Credentials.Key+",signed-query-args="+",expires="+unixtime+",signature="+BuildSignature("GET", method, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var response = await client.GetAsync(url);
var responseString = await response.Content.ReadAsStringAsync();
var responseJson = JsonNode.Parse(responseString) ;
Console.WriteLine(responseJson["api-keys"].AsArray().Count);
return responseJson["api-keys"].AsArray();
}
public static async Task<(String,String)> CreateReadKey(this Installation installation)
{
var readRoleId = installation.ReadRoleId;
if (String.IsNullOrEmpty(readRoleId)
||! await CheckRoleExists(readRoleId))
{
readRoleId = await installation.CreateReadRole();
Thread.Sleep(4000); // Exoscale is to slow for us the role might not be there yet
}
return await CreateKey(installation, readRoleId);
}
private static async Task<Boolean> CheckRoleExists(String roleId)
{
const String url = "https://api-ch-dk-2.exoscale.com/v2/iam-role";
const String method = "iam-role";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
var authheader = "credential="+S3Credentials.Key+",signed-query-args="+",expires="+unixtime+",signature="+BuildSignature("GET", method, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var response = await client.GetAsync(url);
var responseString = await response.Content.ReadAsStringAsync();
return responseString.Contains(roleId);
}
private static async Task<(String,String)> CreateKey(Installation installation, String roleName)
{
var url = "https://api-ch-dk-2.exoscale.com/v2/api-key";
var method = "api-key";
var contentString = $$"""{"role-id": "{{roleName}}", "name":"{{ installation.BucketName()}}"}""";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 60;
var authheader = "credential=" + S3Credentials.Key + ",expires=" + unixtime + ",signature=" +
BuildSignature("POST", method, contentString, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var content = new StringContent(contentString, Encoding.UTF8, "application/json");
var response = await client.PostAsync(url, content);
if (response.StatusCode != HttpStatusCode.OK){
Console.WriteLine("Fuck");
}
//Console.WriteLine($"Created Key for {installation.InstallationName}");
var responseString = await response.Content.ReadAsStringAsync();
var responseJson = JsonNode.Parse(responseString) ;
return (responseJson!["key"]!.GetValue<String>(), responseJson!["secret"]!.GetValue<String>());
}
public static async Task<String> CreateReadRole(this Installation installation)
{
const String url = "https://api-ch-dk-2.exoscale.com/v2/iam-role";
const String method = "iam-role";
String rolename = installation.Product==(int)ProductType.Salimax?Db.Installations.Count(f => f.Product == (int)ProductType.Salimax) + installation.Name:Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name;
var contentString = $$"""
{
"name" : "{{rolename}}",
"policy" : {
"default-service-strategy": "deny",
"services": {
"sos": {
"type": "rules",
"rules": [
{
"expression": "operation == 'get-object' && resources.bucket.startsWith('{{installation.BucketName()}}')",
"action": "allow"
}
]
}
}
}
}
""";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
var authheader = "credential="+S3Credentials.Key+",signed-query-args="+",expires="+unixtime+",signature="+BuildSignature("POST", method, contentString, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var content = new StringContent(contentString, Encoding.UTF8, "application/json");
var response = await client.PostAsync(url, content);
var responseString = await response.Content.ReadAsStringAsync();
//Console.WriteLine(responseString);
//Put Role ID into database
var id = JsonNode.Parse(responseString)!["reference"]!["id"]!.GetValue<String>();
installation.ReadRoleId = id;
Db.Update(installation);
return id;
}
public static async Task<bool> RemoveReadRole(this Installation installation)
{
var roleId = installation.ReadRoleId;
var url = $"https://api-ch-dk-2.exoscale.com/v2/iam-role/{roleId}";
var method = $"iam-role/{roleId}";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 60;
var authheader = "credential=" + S3Credentials.Key + ",expires=" + unixtime + ",signature=" + BuildSignature("DELETE", method, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
try
{
var response = await client.DeleteAsync(url);
if (response.IsSuccessStatusCode)
{
Console.WriteLine($"Successfully deleted read role with ID {roleId}.");
return true;
}
Console.WriteLine($"Failed to delete read role. HTTP Status: {response.StatusCode}. Response: {await response.Content.ReadAsStringAsync()}");
return false;
}
catch (Exception ex)
{
Console.WriteLine($"Error occurred while deleting read role: {ex.Message}");
return false;
}
}
public static async Task<bool> RemoveWriteRole(this Installation installation)
{
var roleId = installation.WriteRoleId;
var url = $"https://api-ch-dk-2.exoscale.com/v2/iam-role/{roleId}";
var method = $"iam-role/{roleId}";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds() + 60;
var authheader = "credential=" + S3Credentials.Key + ",expires=" + unixtime + ",signature=" + BuildSignature("DELETE", method, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
try
{
var response = await client.DeleteAsync(url);
if (response.IsSuccessStatusCode)
{
Console.WriteLine($"Successfully deleted write role with ID {roleId}.");
return true;
}
Console.WriteLine($"Failed to delete write role. HTTP Status: {response.StatusCode}. Response: {await response.Content.ReadAsStringAsync()}");
return false;
}
catch (Exception ex)
{
Console.WriteLine($"Error occurred while deleting write role: {ex.Message}");
return false;
}
}
public static async Task<Boolean> RevokeReadKey(this Installation installation)
{
//Check exoscale documentation https://openapi-v2.exoscale.com/topic/topic-api-request-signature
var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3Key}";
var method = $"access-key/{installation.S3Key}";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
var authheader = "credential="+S3Credentials.Key+",expires="+unixtime+",signature="+BuildSignature("DELETE", method, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var response = await client.DeleteAsync(url);
return response.IsSuccessStatusCode;
}
public static async Task<Boolean> RevokeWriteKey(this Installation installation)
{
//Check exoscale documentation https://openapi-v2.exoscale.com/topic/topic-api-request-signature
var url = $"https://api-ch-dk-2.exoscale.com/v2/access-key/{installation.S3WriteKey}";
var method = $"access-key/{installation.S3WriteKey}";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
var authheader = "credential="+S3Credentials.Key+",expires="+unixtime+",signature="+BuildSignature("DELETE", method, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var response = await client.DeleteAsync(url);
return response.IsSuccessStatusCode;
}
public static async Task<(String, String)> CreateWriteKey(this Installation installation)
{
var writeRoleId = installation.WriteRoleId;
if (String.IsNullOrEmpty(writeRoleId)
|| !await CheckRoleExists(writeRoleId))
{
writeRoleId = await installation.CreateWriteRole();
Thread.Sleep(4000); // Exoscale is to slow for us the role might not be there yet
}
return await CreateKey(installation, writeRoleId);
}
public static async Task<String> CreateWriteRole(this Installation installation)
{
const String url = "https://api-ch-dk-2.exoscale.com/v2/iam-role";
const String method = "iam-role";
String rolename = installation.Product==(int)ProductType.Salimax?Db.Installations.Count(f => f.Product == (int)ProductType.Salimax) + installation.Name:Db.Installations.Count(f => f.Product == (int)ProductType.Salidomo) + installation.Name;
var contentString = $$"""
{
"name" : "WRITE{{rolename}}",
"policy" : {
"default-service-strategy": "deny",
"services": {
"sos": {
"type": "rules",
"rules":[{
"action" : "allow",
"expression": "resources.bucket.startsWith('{{installation.BucketName()}}')"
}]
}
}
}
}
""";
var unixtime = DateTimeOffset.UtcNow.ToUnixTimeSeconds()+60;
var authheader = "credential="+S3Credentials.Key+",signed-query-args="+",expires="+unixtime+",signature="+BuildSignature("POST", method, contentString, unixtime);
var client = new HttpClient();
client.DefaultRequestHeaders.Authorization =
new AuthenticationHeaderValue("EXO2-HMAC-SHA256", authheader);
var content = new StringContent(contentString, Encoding.UTF8, "application/json");
var response = await client.PostAsync(url, content);
var responseString = await response.Content.ReadAsStringAsync();
//Console.WriteLine(responseString);
//Put Role ID into database
var id = JsonNode.Parse(responseString)!["reference"]!["id"]!.GetValue<String>();
installation.WriteRoleId = id;
;
Db.Update(installation);
return id;
}
public static async Task<Boolean> CreateBucket(this Installation installation)
{
var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
return await s3Region.PutBucket(installation.BucketName()) != null;
}
public static async Task<Boolean> DeleteBucket(this Installation installation)
{
var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
return await s3Region.DeleteBucket(installation.BucketName()) ;
}
public static async Task<Boolean> SendConfig(this Installation installation, Configuration config)
{
// This looks hacky but here we grab the vpn-Ip of the installation by its installation Name (e.g. Salimax0001)
// From the vpn server (here salidomo, but we use the vpn home ip for future-proofing)
// using var client = new HttpClient();
// var webRequest = client.GetAsync("10.2.0.1/vpnstatus.txt");
// var text = webRequest.ToString();
// var lines = text!.Split(new [] { Environment.NewLine }, StringSplitOptions.None);
// var vpnIp = lines.First(l => l.Contains(installation.InstallationName)).Split(",")[1];
//
// // Writing the config to a file and then sending that file with rsync sounds inefficient
// // We should find a better solution...
// // TODO The VPN server should do this not the backend!!!
// await File.WriteAllTextAsync("./config.json", config);
// var result = await Cli.Wrap("rsync")
// .WithArguments("./config.json")
// .AppendArgument($@"root@{vpnIp}:/salimax")
// .ExecuteAsync();
// return result.ExitCode == 200;
var maxRetransmissions = 4;
UdpClient udpClient = new UdpClient();
udpClient.Client.ReceiveTimeout = 2000;
int port = 9000;
Console.WriteLine("Trying to reach installation with IP: " + installation.VpnIp);
//Try at most MAX_RETRANSMISSIONS times to reach an installation.
for (int j = 0; j < maxRetransmissions; j++)
{
//string message = "This is a message from RabbitMQ server, you can subscribe to the RabbitMQ queue";
byte[] data = Encoding.UTF8.GetBytes(JsonSerializer.Serialize<Configuration>(config));
udpClient.Send(data, data.Length, installation.VpnIp, port);
//Console.WriteLine($"Sent UDP message to {installation.VpnIp}:{port}: {config}");
Console.WriteLine($"Sent UDP message to {installation.VpnIp}:{port}"+" GridSetPoint is "+config.GridSetPoint +" and MinimumSoC is "+config.MinimumSoC);
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Parse(installation.VpnIp), port);
try
{
byte[] replyData = udpClient.Receive(ref remoteEndPoint);
string replyMessage = Encoding.UTF8.GetString(replyData);
Console.WriteLine("Received " + replyMessage + " from installation " + installation.VpnIp);
return true;
}
catch (SocketException ex)
{
if (ex.SocketErrorCode == SocketError.TimedOut){Console.WriteLine("Timed out waiting for a response. Retry...");}
else
{
Console.WriteLine("Error: " + ex.Message);
return false;
}
}
}
return false;
//var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!);
//var url = s3Region.Bucket(installation.BucketName()).Path("config.json");
//return await url.PutObject(config);
}
}

View File

@ -0,0 +1,100 @@
using InnovEnergy.App.Backend.Database;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class FolderMethods
{
public static IEnumerable<User> UsersWithAccess(this Folder folder)
{
var direct = folder.UsersWithDirectAccess();
var inherited = folder.UsersWithInheritedAccess();
return direct.Concat(inherited);
}
public static IEnumerable<User> UsersWithDirectAccess(this Folder folder)
{
return Db
.FolderAccess
.Where(a => a.FolderId == folder.Id)
.Select(a => Db.GetUserById(a.UserId))
.NotNull();
}
public static IEnumerable<User> UsersWithInheritedAccess(this Folder folder)
{
return folder
.Ancestors()
.SelectMany(f => f.UsersWithDirectAccess())
.NotNull();
}
private static IEnumerable<Folder> ChildFolders(this Folder parent)
{
// Unsafe can give back loops
return Db
.Folders
.Where(f => f.ParentId == parent.Id);
}
public static IEnumerable<Folder> UniqueChildFolders(this Folder parent)
{
//var set = new HashSet<Folder>(Db.Folders, EqualityComparer<Folder>.Default);
return ChildFolders(parent);
}
public static IEnumerable<Installation> ChildInstallations(this Folder parent)
{
return Db
.Installations
.Where(f => f.ParentId == parent.Id);
}
public static IEnumerable<Folder> DescendantFolders(this Folder parent)
{
Console.WriteLine("Parent is "+parent.Id+" looking for descendant folders");
return parent
.TraverseDepthFirstPreOrder(UniqueChildFolders)
.Skip(1); // skip self
}
public static IEnumerable<Folder> DescendantFoldersAndSelf(this Folder parent)
{
return parent
.TraverseDepthFirstPreOrder(UniqueChildFolders);
}
public static Boolean IsDescendantOf(this Folder folder, Folder ancestor)
{
return folder
.Ancestors()
.Any(u => u.Id == ancestor.Id);
}
public static IEnumerable<Folder> Ancestors(this Folder folder)
{
return folder
.Unfold(Parent)
.Skip(1); // skip self
}
public static Folder? Parent(this Folder folder)
{
return IsRoot(folder)
? null
: Db.GetFolderById(folder.ParentId);
}
public static Boolean IsRoot(this Folder folder)
{
return folder.ParentId <= 0
&& Db.GetFolderById(folder.Id)?.Id == 0; // might have been 0 because it is a relative root
}
}

View File

@ -0,0 +1,161 @@
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.Relations;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class InstallationMethods
{
private static readonly String BucketNameSalt =
// Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == ""
// ? "stage" :"3e5b3069-214a-43ee-8d85-57d72000c19d";
"3e5b3069-214a-43ee-8d85-57d72000c19d";
private static readonly String SalidomoBucketNameSalt = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e";
public static String BucketName(this Installation installation)
{
if (installation.Product == (int)ProductType.Salimax)
{
return $"{installation.S3BucketId}-{BucketNameSalt}";
}
return $"{installation.S3BucketId}-{SalidomoBucketNameSalt}";
}
public static async Task<Boolean> RenewS3Credentials(this Installation installation)
{
if(!installation.S3Key.IsNullOrEmpty())
await installation.RevokeReadKey();
var (key,secret) = await installation.CreateReadKey();
installation.S3Key = key;
installation.S3Secret = secret;
if (installation.S3WriteKey == "" || installation.S3WriteSecret == "")
{
var (writeKey,writeSecret) = await installation.CreateWriteKey();
installation.S3WriteSecret = writeSecret;
installation.S3WriteKey = writeKey;
}
return Db.Update(installation);
}
public static IEnumerable<User> UsersWithAccess(this Installation installation)
{
return installation
.UsersWithDirectAccess()
.Concat(installation.UsersWithInheritedAccess());
}
public static IEnumerable<User> UsersWithDirectAccess(this Installation installation)
{
return Db
.InstallationAccess
.Where(a => a.InstallationId == installation.Id)
.Select(a => Db.GetUserById(a.UserId))
.NotNull();
}
public static IEnumerable<User> UsersWithInheritedAccess(this Installation installation)
{
return installation
.Ancestors()
.SelectMany(f => f.UsersWithDirectAccess())
.NotNull();
}
public static IEnumerable<Folder> Ancestors(this Installation installation)
{
var parentFolder = Parent(installation);
if (parentFolder is null)
return Enumerable.Empty<Folder>();
return parentFolder
.Ancestors()
.Prepend(parentFolder);
}
public static Folder? Parent(this Installation installation)
{
if (installation.ParentId <= 0) // relative root
{
var i = Db.GetInstallationById(installation.Id);
if (i is null)
return null;
installation = i;
}
return Db.GetFolderById(installation.ParentId);
}
public static Installation HideWriteKeyIfUserIsNotAdmin(this Installation installation, int userIsAdmin)
{
if(userIsAdmin==2)
return installation;
installation.S3WriteKey = "";
installation.S3WriteSecret = "";
return installation;
}
public static Boolean WasMoved(this Installation installation)
{
var existingInstallation = Db.GetInstallationById(installation.Id);
return existingInstallation is not null
&& existingInstallation.ParentId != installation.ParentId;
}
public static Boolean Exists(this Installation installation)
{
return Db.Installations.Any(i => i.Id == installation.Id);
}
public static String GetOrderNumbers(this Installation installation)
{
return Db.OrderNumber2Installation
.Where(i => i.InstallationId == installation.Id)
.Select(i => i.OrderNumber)
.ToReadOnlyList().JoinWith(",");
}
public static Installation FillOrderNumbers(this Installation installation)
{
installation.OrderNumbers = installation.GetOrderNumbers();
return installation;
}
public static Boolean SetOrderNumbers(this Installation installation)
{
var relations = Db.OrderNumber2Installation.Where(i => i.InstallationId == installation.Id).ToList();
foreach (var orderNumber in installation.OrderNumbers.Split(","))
{
var rel = relations.FirstOrDefault(i => i.OrderNumber == orderNumber);
if ( rel != null) {relations.Remove(rel); continue;}
var o2I = new OrderNumber2Installation
{
OrderNumber = orderNumber,
InstallationId = installation.Id
};
Db.Create(o2I);
}
foreach (var rel in relations)
{
Db.Delete(rel);
}
return true;
}
}

View File

@ -0,0 +1,430 @@
using System.Diagnostics;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.Relations;
using InnovEnergy.App.Backend.Websockets;
using InnovEnergy.Lib.Utils;
using Org.BouncyCastle.Asn1.X509;
namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class SessionMethods
{
public static Boolean Create(this Session? session, Folder? folder)
{
var user = session?.User;
return user is not null
&& folder is not null
&& user.UserType!=0
&& user.HasAccessTo(folder.Parent())
&& Db.Create(folder) // TODO: these two in a transaction
&& Db.Create(new FolderAccess { UserId = user.Id, FolderId = folder.Id });
}
public static Boolean Update(this Session? session, Folder? folder)
{
var user = session?.User;
var original = Db.GetFolderById(folder?.Id);
return user is not null
&& folder is not null
&& original is not null
&& user.UserType !=0
&& user.HasAccessTo(folder)
&& folder
.WithParentOf(original) // prevent moving
.Apply(Db.Update);
}
public static Boolean MoveFolder(this Session? session, Int64 folderId, Int64 parentId)
{
var user = session?.User;
var folder = Db.GetFolderById(folderId);
var parent = Db.GetFolderById(parentId);
return user is not null
&& folder is not null
&& user.UserType !=0
&& user.HasAccessTo(folder)
&& user.HasAccessTo(parent)
&& folder
.Do(() => folder.ParentId = parentId)
.Apply(Db.Update);
}
public static Boolean MoveInstallation(this Session? session, Int64 installationId, Int64 parentId)
{
var user = session?.User;
var installation = Db.GetInstallationById(installationId);
var parent = Db.GetFolderById(parentId);
if(installation == null || installation.ParentId == parentId) return false;
return user is not null
&& user.UserType !=0
&& user.HasAccessTo(installation)
&& user.HasAccessTo(parent)
&& installation
.Do(() => installation.ParentId = parentId)
.Apply(Db.Update);
}
public static async Task RunScriptInBackground(this Session? session, String vpnIp, Int64 batteryNode,String version,Int64 product)
{
Console.WriteLine("-----------------------------------Start updating firmware-----------------------------------");
string scriptPath = (product == (int)ProductType.Salimax)
? "/home/ubuntu/backend/uploadBatteryFw/update_firmware_Salimax.sh"
: "/home/ubuntu/backend/uploadBatteryFw/update_firmware_Salidomo.sh";
await Task.Run(() =>
{
var process = new Process();
process.StartInfo.FileName = "/bin/bash";
process.StartInfo.Arguments = $"{scriptPath} {vpnIp} {batteryNode} {version}";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true;
process.Start();
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
Console.WriteLine(output);
});
Console.WriteLine("-----------------------------------Stop updating firmware-----------------------------------");
}
public static async Task RunDownloadLogScript(this Session? session, String vpnIp, Int64 batteryNode,Int64 product)
{
Console.WriteLine("-----------------------------------Start downloading battery log-----------------------------------");
string scriptPath = (product == (int)ProductType.Salimax)
? "/home/ubuntu/backend/downloadBatteryLog/download_bms_log_Salimax.sh"
: "/home/ubuntu/backend/downloadBatteryLog/download_bms_log_Salidomo.sh";
await Task.Run(() =>
{
var process = new Process
{
StartInfo = new ProcessStartInfo
{
FileName = "/bin/bash",
Arguments = $"{scriptPath} {vpnIp} {batteryNode}",
UseShellExecute = false,
RedirectStandardOutput = true
}
};
process.Start();
var output = process.StandardOutput.ReadToEnd();
process.WaitForExit();
Console.WriteLine(output);
});
Console.WriteLine("-----------------------------------Stop downloading battery log-----------------------------------");
}
public static async Task<Boolean> SendInstallationConfig(this Session? session, Int64 installationId, Configuration configuration)
{
var user = session?.User;
var installation = Db.GetInstallationById(installationId);
return user is not null
&& installation is not null
&& user.UserType !=0
&& user.HasAccessTo(installation)
&& await installation.SendConfig(configuration);
}
public static async Task<Boolean> InsertUserAction(this Session? session, UserAction action)
{
var user = session?.User;
if (user is null || user.UserType == 0)
return false;
action.UserName = user.Name;
var installation = Db.GetInstallationById(action.InstallationId);
installation.TestingMode = action.TestingMode;
installation.Apply(Db.Update);
WebsocketManager.InformWebsocketsForInstallation(action.InstallationId);
// Save the configuration change to the database
Db.HandleAction(action);
return true;
}
public static async Task<Boolean> UpdateUserAction(this Session? session, UserAction action)
{
var user = session?.User;
if (user is null || user.UserType == 0)
return false;
var installation = Db.GetInstallationById(action.InstallationId);
if (installation.TestingMode != action.TestingMode)
{
installation.TestingMode = action.TestingMode;
installation.Apply(Db.Update);
WebsocketManager.InformWebsocketsForInstallation(action.InstallationId);
}
Db.UpdateAction(action);
return true;
}
public static async Task<Boolean> DeleteUserAction(this Session? session, Int64 actionId)
{
var user = session?.User;
if (user is null || user.UserType == 0)
return false;
var action = Db.GetActionById(actionId);
if (action.TestingMode)
{
var installation = Db.GetInstallationById(action.InstallationId);
installation.TestingMode = false;
installation.Apply(Db.Update);
WebsocketManager.InformWebsocketsForInstallation(action.InstallationId);
}
Db.Delete(action);
Console.WriteLine("---------------Deleted the Action in the database-----------------");
return true;
}
public static Boolean Delete(this Session? session, Folder? folder)
{
var user = session?.User;
return user is not null
&& folder is not null
&& user.UserType !=0
&& user.HasAccessTo(folder)
&& Db.Delete(folder);
}
public static async Task<Boolean> Create(this Session? session, Installation? installation)
{
var user = session?.User;
//Salimax installation
if (installation.Product == (int)ProductType.Salimax)
{
return user is not null
&& user.UserType != 0
&& user.HasAccessToParentOf(installation)
&& Db.Create(installation) // TODO: these two in a transaction
&& installation.SetOrderNumbers()
&& Db.Create(new InstallationAccess { UserId = user.Id, InstallationId = installation.Id })
&& await installation.CreateBucket()
&& await installation.RenewS3Credentials();
}
if (installation.Product == (int)ProductType.Salidomo)
{
return user is not null
&& user.UserType != 0
&& user.HasAccessToParentOf(installation)
&& Db.Create(installation)
&& await installation.CreateBucket()
&& await installation.RenewS3Credentials();
}
if (installation.Product == (int)ProductType.SodioHome)
{
return user is not null
&& user.UserType != 0
&& user.HasAccessToParentOf(installation)
&& Db.Create(installation);
}
return false;
}
public static Boolean Update(this Session? session, Installation? installation)
{
var user = session?.User;
var original = Db.GetInstallationById(installation?.Id);
//Salimax installation
if (installation.Product == (int)ProductType.Salimax)
{
return user is not null
&& installation is not null
&& original is not null
&& user.UserType !=0
&& user.HasAccessTo(installation)
&& installation.SetOrderNumbers()
&& installation
.WithParentOf(original) // prevent moving
.Apply(Db.Update);
}
return user is not null
&& installation is not null
&& original is not null
&& user.UserType !=0
&& user.HasAccessToParentOf(installation)
&& installation
.Apply(Db.Update);
}
public static async Task<Boolean> Delete(this Session? session, Installation? installation)
{
var user = session?.User;
if (user is not null
&& installation is not null
&& user.UserType != 0)
{
if (installation.Product is (int)ProductType.Salimax or (int)ProductType.Salidomo)
{
return
Db.Delete(installation)
&& await installation.RevokeReadKey()
&& await installation.RevokeWriteKey()
&& await installation.RemoveReadRole()
&& await installation.RemoveWriteRole()
&& await installation.DeleteBucket();
}
else
{
return Db.Delete(installation);
}
}
return false;
}
public static Boolean Create(this Session? session, User newUser)
{
var sessionUser = session?.User;
var userAlreadyExists = Db.GetUserByEmail(newUser.Email);
return sessionUser is not null
&& userAlreadyExists is null
&& sessionUser.UserType !=0
&& newUser
.WithParent(sessionUser)
.Do(() => newUser.MustResetPassword = true)
.Do(() => newUser.Password = null)
.Apply(Db.Create);
// && Mailer.Mailer.SendVerificationMessage(newUser);
// .Do(() => newUser.Password = newUser.SaltAndHashPassword(newUser.Password))
//Send Email to new user to verify email and set password
}
public static Boolean Update(this Session? session, User? editedUser)
{
var sessionUser = session?.User;
var originalUser = Db.GetUserById(editedUser?.Id);
return editedUser is not null
&& sessionUser is not null
&& originalUser is not null
&& sessionUser.UserType !=0
&& sessionUser.HasAccessTo(originalUser)
&& editedUser
.WithParentOf(originalUser) // prevent moving
.WithNameOf(originalUser)
.WithPasswordOf(originalUser)
.Apply(Db.Update);
}
public static Boolean UpdatePassword(this Session? session, String? newPassword)
{
var sessionUser = session?.User;
return sessionUser is not null
&& sessionUser
.Do(() => sessionUser.Password = sessionUser.SaltAndHashPassword(newPassword))
.Do(() => sessionUser.MustResetPassword = false)
.Apply(Db.Update);
}
public static Boolean Delete(this Session? session, User? userToDelete)
{
var sessionUser = session?.User;
return sessionUser is not null
&& userToDelete is not null
&& sessionUser.UserType !=0
&& sessionUser.HasAccessTo(userToDelete)
&& Db.Delete(userToDelete);
}
public static Boolean GrantUserAccessTo(this Session? session, User? user, Installation? installation)
{
var sessionUser = session?.User;
return sessionUser is not null
&& installation is not null
&& user is not null
&& user.IsDescendantOf(sessionUser)
&& sessionUser.HasAccessTo(installation)
&& !user.HasAccessTo(installation)
&& Db.Create(new InstallationAccess { UserId = user.Id, InstallationId = installation.Id });
}
public static Boolean GrantUserAccessTo(this Session? session, User? user, Folder? folder)
{
var sessionUser = session?.User;
return sessionUser is not null
&& folder is not null
&& user is not null
&& user.IsDescendantOf(sessionUser)
&& sessionUser.HasAccessTo(folder)
&& !user.HasAccessTo(folder)
&& Db.Create(new FolderAccess { UserId = user.Id, FolderId = folder.Id });
}
public static Boolean RevokeUserAccessTo(this Session? session, User? user, Installation? installation)
{
var sessionUser = session?.User;
return sessionUser is not null
&& installation is not null
&& user is not null
&& user.IsDescendantOf(sessionUser)
&& sessionUser.HasAccessTo(installation)
&& user.HasAccessTo(installation)
&& Db.InstallationAccess.Delete(a => a.UserId == user.Id && a.InstallationId == installation.Id) > 0;
}
public static Boolean RevokeUserAccessTo(this Session? session, User? user, Folder? folder)
{
var sessionUser = session?.User;
return sessionUser is not null
&& folder is not null
&& user is not null
&& user.IsDescendantOf(sessionUser)
&& sessionUser.HasAccessTo(folder)
&& user.HasAccessTo(folder)
&& Db.FolderAccess.Delete(a => a.UserId == user.Id && a.FolderId == folder.Id) > 0;
}
public static Boolean Logout(this Session? session)
{
return session is not null
&& Db.Sessions.Delete(s => s.Token == session.Token) > 0;
}
}

View File

@ -0,0 +1,48 @@
using InnovEnergy.App.Backend.Database;
namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class TreeNodeMethods
{
public static T WithParentOf<T>(this T treeNode, T other) where T: TreeNode
{
treeNode.ParentId = other.ParentId;
return treeNode;
}
public static T WithNameOf<T>(this T treeNode, T other) where T: TreeNode
{
treeNode.Name = other.Name;
return treeNode;
}
public static T WithParent<T>(this T treeNode, T other) where T: TreeNode
{
treeNode.ParentId = other.Id;
return treeNode;
}
public static T HideParentIfUserHasNoAccessToParent<T>(this T node, User? accessingUser)
{
if (accessingUser is not null && node is TreeNode treeNode && !accessingUser.HasAccessToParentOf(treeNode))
{
treeNode.ParentId = 0;
}
return node;
}
public static TreeNode FillOrderNumbers(this TreeNode treeNode)
{
if (treeNode is Installation installation)
{
installation.FillOrderNumbers();
}
return treeNode;
}
}

View File

@ -0,0 +1,261 @@
using System.Security.Cryptography;
using System.Web;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.Lib.Mailer;
using InnovEnergy.Lib.Utils;
using Convert = System.Convert;
using static System.Text.Encoding;
namespace InnovEnergy.App.Backend.DataTypes.Methods;
public static class UserMethods
{
public static IEnumerable<Installation> AccessibleInstallations(this User user,int product)
{
var direct = user.DirectlyAccessibleInstallations().ToList().Where(f=>f.Product==product);
var fromFolders = user
.AccessibleFolders()
.SelectMany(u => u.ChildInstallations()).ToList().Where(f=>f.Product==product);
return direct
.Concat(fromFolders)
.Distinct();
}
public static IEnumerable<Installation> AccessibleInstallations(this User user)
{
var direct = user.DirectlyAccessibleInstallations().ToList();
var fromFolders = user
.AccessibleFolders()
.SelectMany(u => u.ChildInstallations()).ToList();
return direct
.Concat(fromFolders)
.Distinct();
}
public static IEnumerable<Folder> AccessibleFolders(this User user)
{
return user
.DirectlyAccessibleFolders()
.SelectMany(f => f.DescendantFolders().Prepend(f))
.Distinct();
}
public static IEnumerable<TreeNode> AccessibleFoldersAndInstallations(this User user,int product)
{
var folders = user.AccessibleFolders() as IEnumerable<TreeNode>;
user.AccessibleInstallations(product).ForEach(i => i.FillOrderNumbers());
var installations = user.AccessibleInstallations(product);
return folders.Concat(installations);
}
public static IEnumerable<TreeNode> AccessibleFoldersAndInstallations(this User user)
{
var folders = user.AccessibleFolders() as IEnumerable<TreeNode>;
user.AccessibleInstallations().ForEach(i => i.FillOrderNumbers());
var installations = user.AccessibleInstallations();
return folders.Concat(installations);
}
public static IEnumerable<Installation> DirectlyAccessibleInstallations(this User user)
{
return Db
.InstallationAccess
.Where(r => r.UserId == user.Id)
.Select(r => r.InstallationId)
.Select(i => Db.GetInstallationById(i))
.NotNull()
.Do(i => i.HideParentIfUserHasNoAccessToParent(user)); // hide inaccessible parents from calling user
}
public static IEnumerable<Folder> DirectlyAccessibleFolders(this User user)
{
return Db
.FolderAccess
.Where(r => r.UserId == user.Id)
.Select(r => r.FolderId)
.Select(i => Db.GetFolderById(i))
.NotNull()
.Do(f => f.HideParentIfUserHasNoAccessToParent(user)); // hide inaccessible parents from calling user;
}
public static IEnumerable<User> ChildUsers(this User parent)
{
return Db
.Users
.Where(f => f.ParentId == parent.Id);
}
public static IEnumerable<User> DescendantUsers(this User parent)
{
return parent
.TraverseDepthFirstPreOrder(ChildUsers)
.Skip(1); // skip self
}
public static Boolean IsDescendantOf(this User user, User ancestor)
{
return user
.Ancestors()
.Any(u => u.Id == ancestor.Id);
}
private static IEnumerable<User> Ancestors(this User user)
{
return user
.Unfold(Parent)
.Skip(1); // skip self
}
public static Boolean VerifyPassword(this User user, String? password)
{
return password is not null
&& Db.GetUserByEmail(user.Email)?.Password == user.SaltAndHashPassword(password);
}
public static String SaltAndHashPassword(this User user, String password)
{
var dataToHash = $"{password}{user.Salt()}";
return dataToHash
.Apply(UTF8.GetBytes)
.Apply(SHA256.HashData)
.Apply(Convert.ToBase64String);
}
public static User? Parent(this User u)
{
return u.IsRoot()
? null
: Db.GetUserById(u.ParentId);
}
public static Boolean IsRoot(this User user)
{
return user.ParentId <= 0
&& Db.GetUserById(user.Id)?.Id == 0; // might have been 0 because it is a relative root
}
public static Boolean HasDirectAccessTo(this User user, Folder folder)
{
return Db
.FolderAccess
.Any(r => r.FolderId == folder.Id && r.UserId == user.Id);
}
public static Boolean HasAccessTo(this User user, Folder? folder)
{
if (folder is null)
return false;
return user.HasDirectAccessTo(folder)
|| folder
.Ancestors()
.Any(user.HasDirectAccessTo);
}
public static Boolean HasDirectAccessTo(this User user, Installation installation)
{
return Db
.InstallationAccess
.Any(r => r.UserId == user.Id && r.InstallationId == installation.Id);
}
public static Boolean HasAccessTo(this User user, Installation? installation)
{
if (installation is null)
return false;
return user.HasDirectAccessTo(installation)
|| installation
.Ancestors()
.Any(user.HasDirectAccessTo);
}
public static Boolean HasAccessTo(this User user, User? other)
{
if (other is null)
return false;
return other.Id == user.Id
|| other
.Ancestors()
.Contains(user);
}
public static Boolean HasAccessToParentOf(this User user, TreeNode? other)
{
return other?.Type switch
{
"Installation" => user.HasAccessTo(Db.GetFolderById(other.ParentId)),
"User" => user.HasAccessTo(Db.GetUserById(other.ParentId)),
"Folder" => user.HasAccessTo(Db.GetFolderById(other.ParentId)),
_ => false
};
}
private static String Salt(this User user)
{
// + id => salt unique per user
// + InnovEnergy => globally unique
return $"{user.Id}InnovEnergy";
}
public static User WithPasswordOf(this User user, User other)
{
user.Password = other.Password;
return user;
}
public static User HidePassword(this User user)
{
user.Password = "";
return user;
}
public static Task SendEmail(this User user, String subject, String body)
{
return Mailer.Send(user.Name, user.Email, subject, body);
}
public static Task SendPasswordResetEmail(this User user, String token)
{
const String subject = "Reset the password of your Inesco Energy Account";
const String resetLink = "https://monitor.innov.energy/api/ResetPassword"; // TODO: move to settings file
var encodedToken = HttpUtility.UrlEncode(token);
var body = $"Dear {user.Name}\n" +
$"To reset your password " +
$"please open this link:{resetLink}?token={encodedToken}";
return user.SendEmail(subject, body);
}
public static Task SendNewUserWelcomeMessage(this User user)
{
const String subject = "Your new Inesco Energy Account";
var resetLink = $"https://monitor.innov.energy/?username={user.Email}"; // TODO: move to settings file
var body = $"Dear {user.Name}\n" +
$"To set your password and log in to your " +
$"Inesco Energy Account open this link:{resetLink}";
return user.SendEmail(subject, body);
}
}

View File

@ -0,0 +1,23 @@
using System.Diagnostics.CodeAnalysis;
namespace InnovEnergy.App.Backend.DataTypes;
public abstract partial class TreeNode
{
public override Boolean Equals(Object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((TreeNode)obj);
}
protected Boolean Equals(TreeNode other) => Id == other.Id;
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
public override Int32 GetHashCode() => Id.GetHashCode();
public static Boolean operator ==(TreeNode? left, TreeNode? right) => Equals(left, right);
public static Boolean operator !=(TreeNode? left, TreeNode? right) => !Equals(left, right);
}

View File

@ -0,0 +1,25 @@
using System.Diagnostics.CodeAnalysis;
namespace InnovEnergy.App.Backend.DataTypes;
public abstract partial class TreeNode
{
private sealed class IdEqualityComparer : IEqualityComparer<TreeNode>
{
public Boolean Equals(TreeNode x, TreeNode y)
{
if (ReferenceEquals(x, y)) return true;
if (ReferenceEquals(x, null)) return false;
if (ReferenceEquals(y, null)) return false;
if (x.GetType() != y.GetType()) return false;
return x.Id == y.Id;
}
public Int32 GetHashCode(TreeNode obj)
{
return obj.Id.GetHashCode();
}
}
public static IEqualityComparer<TreeNode> IdComparer { get; } = new IdEqualityComparer();
}

View File

@ -0,0 +1,19 @@
using SQLite;
namespace InnovEnergy.App.Backend.DataTypes;
public abstract partial class TreeNode
{
//This is the parent class of each relation. It has an autoincrement Id, name, information, parent Id and Type.
//Ignore means: "Do not map this property to a database column."
[PrimaryKey, AutoIncrement]
public Int64 Id { get; set; }
public virtual String Name { get; set; } = ""; // overridden by User (unique)
public String Information { get; set; } = ""; // unstructured random info
[Indexed]
public Int64 ParentId { get; set; }
[Ignore]
public String Type => GetType().Name;
}

View File

@ -0,0 +1,17 @@
using SQLite;
namespace InnovEnergy.App.Backend.DataTypes;
public class User : TreeNode
{
[Unique]
public String Email { get; set; } = null!;
public int UserType { get; set; } = 0;
public Boolean MustResetPassword { get; set; } = false;
public String? Password { get; set; } = null!;
[Unique]
public override String Name { get; set; } = null!;
}

View File

@ -0,0 +1,20 @@
using SQLite;
namespace InnovEnergy.App.Backend.DataTypes;
public class UserAction
{
[PrimaryKey, AutoIncrement]
public Int64 Id { get; set; } // Primary key for the table, auto-incremented
[Indexed]
public String UserName { get; set; } = null!;// User Name who made the configuration change
public Int64 InstallationId { get; set; } // Installation ID where the configuration change is made
public Boolean TestingMode { get; set; }
public DateTime Timestamp { get; set; } // Timestamp of the configuration change
public String Description { get; set; } = null!;// Serialized string representing the new configuration
}

View File

@ -0,0 +1,10 @@
namespace InnovEnergy.App.Backend.DataTypes;
public class WebsocketMessage
{
public int id { get; set; }
public int status { get; set; }
public Boolean testingMode { get; set; }
}

View File

@ -0,0 +1,152 @@
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.Relations;
namespace InnovEnergy.App.Backend.Database;
public static partial class Db
{
private static Boolean Insert(Object obj)
{
var success = Connection.Insert(obj) > 0;
if (success) Backup();
return success;
}
public static Boolean Create(Installation installation)
{
// The bucket Id it calculated as follows: It is 1 + the maximum bucket id of all the existing installations of the same product
// SQLite wrapper is smart and *modifies* t's Id to the one generated (autoincrement) by the insertion
installation.S3BucketId = Installations.Where(inst => inst.Product == installation.Product).Max(inst => (int?)inst.S3BucketId)+1 ?? 0;
return Insert(installation);
}
public static Boolean Create(Error error)
{
return Insert(error);
}
public static Boolean Create(Warning warning)
{
return Insert(warning);
}
public static Boolean Create(Folder folder)
{
return Insert(folder);
}
public static Boolean Create(User user)
{
return Insert(user);
}
public static Boolean Create(Session session)
{
return Insert(session);
}
public static Boolean Create(InstallationAccess installationAccess)
{
return Insert(installationAccess);
}
public static Boolean Create(FolderAccess folderAccess)
{
return Insert(folderAccess);
}
public static Boolean Create(OrderNumber2Installation o2i)
{
return Insert(o2i);
}
public static Boolean Create(UserAction action)
{
return Insert(action);
}
public static void HandleAction(UserAction newAction)
{
//Find the total number of actions for this installation
var totalActions = UserActions.Count(action => action.InstallationId == newAction.InstallationId);
//If there are 100 actions, remove the one with the oldest timestamp
if (totalActions == 100)
{
var oldestAction =
UserActions.Where(action => action.InstallationId == newAction.InstallationId)
.OrderBy(action => action.Timestamp)
.FirstOrDefault();
//Remove the old action
Delete(oldestAction);
//Add the new action
Create(newAction);
}
else
{
Console.WriteLine("---------------Added the new Action to the database-----------------");
Create(newAction);
}
}
//This function is called from the RabbitMQ manager when a new error arrives to the database.
//We keep only the last 100 errors for each installation. If we already have stored 100 errors, we delete the older one and we insert the new one.
public static void HandleError(Error newError,int installationId)
{
//Find the total number of errors for this installation
var totalErrors = Errors.Count(error => error.InstallationId == installationId);
//If there are 100 errors, remove the one with the oldest timestamp
if (totalErrors == 100)
{
var oldestError =
Errors.Where(error => error.InstallationId == installationId)
.OrderBy(error => error.Date)
.FirstOrDefault();
//Remove the old error
Delete(oldestError);
//Add the new error
Create(newError);
}
else
{
Console.WriteLine("---------------Added the new Alarm to the database-----------------");
Create(newError);
}
}
public static void HandleWarning(Warning newWarning,int installationId)
{
//Find the total number of warnings for this installation
var totalWarnings = Warnings.Count(warning => warning.InstallationId == installationId);
//If there are 100 warnings, remove the one with the oldest timestamp
if (totalWarnings == 100)
{
var oldestWarning =
Warnings.Where(warning => warning.InstallationId == installationId)
.OrderBy(warning => warning.Date)
.FirstOrDefault();
//Remove the old warning
Delete(oldestWarning);
//Add the new warning
Create(newWarning);
}
else
{
Console.WriteLine("---------------Added the new Warning to the database-----------------");
Create(newWarning);
}
}
}

View File

@ -0,0 +1,310 @@
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.DataTypes.Methods;
using InnovEnergy.App.Backend.Relations;
using InnovEnergy.Lib.S3Utils;
using InnovEnergy.Lib.S3Utils.DataTypes;
using InnovEnergy.Lib.Utils;
using SQLite;
using SQLiteConnection = SQLite.SQLiteConnection;
namespace InnovEnergy.App.Backend.Database;
//The methods of the Db class are located in multiple files (Create.cs, Read,cs, Delete.cs, Update.cs)
//That's why the class definition is partial
public static partial class Db
{
private static SQLiteConnection Connection { get; } = InitConnection();
public static TableQuery<Session> Sessions => Connection.Table<Session>();
public static TableQuery<Folder> Folders => Connection.Table<Folder>();
public static TableQuery<Installation> Installations => Connection.Table<Installation>();
public static TableQuery<User> Users => Connection.Table<User>();
public static TableQuery<FolderAccess> FolderAccess => Connection.Table<FolderAccess>();
public static TableQuery<InstallationAccess> InstallationAccess => Connection.Table<InstallationAccess>();
public static TableQuery<OrderNumber2Installation> OrderNumber2Installation => Connection.Table<OrderNumber2Installation>();
public static TableQuery<Error> Errors => Connection.Table<Error>();
public static TableQuery<Warning> Warnings => Connection.Table<Warning>();
public static TableQuery<UserAction> UserActions => Connection.Table<UserAction>();
public static void Init()
{
//Used to force static constructor
//Since this class is static, we call Init method from the Program.cs to initialize all the fields of the class
//When a class is loaded, the fields are initialized before the constructor's code is executed.
//The TableQuery fields are lazy meaning that they will be initialized when they get accessed
//The connection searches for the latest backup and binds all the tables to it.
}
//This is the constructor of the class
static Db()
{
Connection.RunInTransaction(() =>
{
Connection.CreateTable<User>();
Connection.CreateTable<Installation>();
Connection.CreateTable<Folder>();
Connection.CreateTable<FolderAccess>();
Connection.CreateTable<InstallationAccess>();
Connection.CreateTable<Session>();
Connection.CreateTable<OrderNumber2Installation>();
Connection.CreateTable<Error>();
Connection.CreateTable<Warning>();
Connection.CreateTable<UserAction>();
});
//UpdateKeys();
CleanupSessions().SupressAwaitWarning();
DeleteSnapshots().SupressAwaitWarning();
}
private static SQLiteConnection InitConnection()
{
var latestDb = new DirectoryInfo("DbBackups")
.GetFiles()
.OrderBy(f => f.LastWriteTime)
.Last().Name;
Console.WriteLine("latestdb is "+latestDb);
//This is the file connection from the DbBackups folder
var fileConnection = new SQLiteConnection("DbBackups/" + latestDb);
//Create a table if it does not exist
fileConnection.CreateTable<User>();
fileConnection.CreateTable<Installation>();
fileConnection.CreateTable<Folder>();
fileConnection.CreateTable<FolderAccess>();
fileConnection.CreateTable<InstallationAccess>();
fileConnection.CreateTable<Session>();
fileConnection.CreateTable<OrderNumber2Installation>();
fileConnection.CreateTable<Error>();
fileConnection.CreateTable<Warning>();
fileConnection.CreateTable<UserAction>();
return fileConnection;
//return CopyDbToMemory(fileConnection);
}
public static void BackupDatabase()
{
var filename = "db-" + DateTimeOffset.UtcNow.ToUnixTimeSeconds() + ".sqlite";
Connection.Backup("DbBackups/" + filename);
}
//Delete all except 10 snapshots every 24 hours.
private static async Task DeleteSnapshots()
{
while (true)
{
try
{
var files = new DirectoryInfo("DbBackups")
.GetFiles()
.OrderByDescending(f => f.LastWriteTime);
var filesToDelete = files.Skip(10);
foreach (var file in filesToDelete)
{
Console.WriteLine("File to delete is " + file.Name);
file.Delete();
}
}
catch(Exception e)
{
Console.WriteLine("An error has occured when cleaning database snapshots, exception is:\n"+e);
}
await Task.Delay(TimeSpan.FromHours(24));
}
}
//Delete all expired sessions every half an hour. An expired session is a session remained for more than 1 day.
private static async Task CleanupSessions()
{
while (true)
{
try
{
var deadline = DateTime.Now.AddDays(-Session.MaxAge.Days);
foreach (var session in Sessions)
{
if (session.LastSeen < deadline)
{
Console.WriteLine("Need to remove session of user id " + session.User.Name + "last time is "+session.LastSeen);
}
}
Sessions.Delete(s => s.LastSeen < deadline);
}
catch(Exception e)
{
Console.WriteLine("An error has occured when cleaning stale sessions, exception is:\n"+e);
}
await Task.Delay(TimeSpan.FromHours(0.5));
}
}
private static async Task RemoveNonExistingKeys()
{
while (true)
{
try
{
var validReadKeys = Installations
.Select(i => i.S3Key)
.Distinct()
.ToList();
var validWriteKeys = Installations
.Select(i => i.S3WriteKey)
.Distinct()
.ToList();
Console.WriteLine("VALID READ KEYS");
for (int i = 0; i < validReadKeys.Count; i++)
{
Console.WriteLine(validReadKeys[i]);
}
Console.WriteLine("VALID WRITE KEYS");
for (int i = 0; i < validReadKeys.Count; i++)
{
Console.WriteLine(validWriteKeys[i]);
}
const String provider = "exo.io";
var S3keys = await ExoCmd.GetAccessKeys();
foreach (var keyMetadata in S3keys)
{
if (keyMetadata["key"].ToString()!="EXOa0b53cf10517307cec1bf00e" && !validReadKeys.Contains(keyMetadata["key"].ToString()) && !validWriteKeys.Contains(keyMetadata["key"].ToString()))
{
//await ExoCmd.RevokeReadKey(keyMetadata["key"].ToString());
Console.WriteLine("Deleted key "+keyMetadata["key"]);
}
}
}
catch(Exception e)
{
Console.WriteLine("An error has occured when updating S3 keys, exception is:\n"+e);
}
await Task.Delay(TimeSpan.FromHours(24));
}
}
private static async Task UpdateKeys()
{
while (true)
{
try
{
await UpdateS3Urls();
}
catch(Exception e)
{
Console.WriteLine("An error has occured when updating S3 keys, exception is:\n"+e);
}
await RemoveNonExistingKeys();
await Task.Delay(TimeSpan.FromHours(24));
}
}
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;
}
private static async Task UpdateS3Urls()
{
var regions = Installations
.Select(i => i.S3Region)
.Distinct()
.ToList();
const String provider = "exo.io";
Console.WriteLine("-----------------------UPDATED READ KEYS-------------------------------------------------------------------");
foreach (var region in regions)
{
var s3Region = new S3Region($"https://{region}.{provider}", ExoCmd.S3Credentials!);
var bucketList = await s3Region.ListAllBuckets();
var installations = from bucket in bucketList.Buckets
from installation in Installations
where installation.BucketName() == bucket.BucketName
select installation;
foreach (var installation in installations)
{
await installation.RenewS3Credentials();
}
}
}
public static async Task<Boolean> SendPasswordResetEmail(User user, String sessionToken)
{
try
{
await user.SendPasswordResetEmail(sessionToken);
return true;
}
catch
{
return false;
}
}
public static async Task<Boolean> SendNewUserEmail(User user)
{
try
{
await user.SendNewUserWelcomeMessage();
return true;
}
catch
{
return false;
}
}
public static Boolean DeleteUserPassword(User user)
{
user.Password = "";
user.MustResetPassword = true;
return Update(user);
}
}

View File

@ -0,0 +1,144 @@
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.DataTypes.Methods;
using InnovEnergy.App.Backend.Relations;
namespace InnovEnergy.App.Backend.Database;
public static partial class Db
{
//Since we do not want to stress the memory in the VM a lot, we make a snapshot of the database every 100 transactions.
private static int _backupCounter = 0;
private static void Backup()
{
_backupCounter++;
if (_backupCounter > 100)
{
_backupCounter = 0;
BackupDatabase();
}
}
public static Boolean Delete(Folder folder)
{
var deleteSuccess= RunTransaction(DeleteFolderAndAllItsDependencies);
if (deleteSuccess)
{
Backup();
}
return deleteSuccess;
Boolean DeleteFolderAndAllItsDependencies()
{
return folder
.DescendantFoldersAndSelf()
.All(DeleteDescendantFolderAndItsDependencies);
}
Boolean DeleteDescendantFolderAndItsDependencies(Folder f)
{
FolderAccess .Delete(r => r.FolderId == f.Id);
Installations.Delete(r => r.ParentId == f.Id);
var delete = Folders.Delete(r => r.Id == f.Id);
return delete>0;
}
}
public static Boolean Delete(Error errorToDelete)
{
var deleteSuccess = RunTransaction(DeleteError);
if (deleteSuccess)
Backup();
return deleteSuccess;
Boolean DeleteError()
{
return Errors.Delete(error => error.Id == errorToDelete.Id) >0;
}
}
public static Boolean Delete(UserAction actionToDelete)
{
var deleteSuccess = RunTransaction(DeleteAction);
if (deleteSuccess)
Backup();
return deleteSuccess;
Boolean DeleteAction()
{
return UserActions.Delete(action => action.Id == actionToDelete.Id) >0;
}
}
public static Boolean Delete(Warning warningToDelete)
{
var deleteSuccess = RunTransaction(DeleteWarning);
if (deleteSuccess)
Backup();
return deleteSuccess;
Boolean DeleteWarning()
{
return Warnings.Delete(warning => warning.Id == warningToDelete.Id) >0;
}
}
public static Boolean Delete(Installation installation)
{
var deleteSuccess = RunTransaction(DeleteInstallationAndItsDependencies);
if (deleteSuccess)
Backup();
return deleteSuccess;
Boolean DeleteInstallationAndItsDependencies()
{
InstallationAccess.Delete(i => i.InstallationId == installation.Id);
if (installation.Product == (int)ProductType.Salimax)
{
//For Salimax, delete the OrderNumber2Installation entries associated with this installation id.
OrderNumber2Installation.Delete(i => i.InstallationId == installation.Id);
}
return Installations.Delete(i => i.Id == installation.Id) > 0;
}
}
public static Boolean Delete(User user)
{
var deleteSuccess = RunTransaction(DeleteUserAndHisDependencies);
if (deleteSuccess)
Backup();
return deleteSuccess;
Boolean DeleteUserAndHisDependencies()
{
FolderAccess .Delete(u => u.UserId == user.Id);
InstallationAccess.Delete(u => u.UserId == user.Id);
return Users.Delete(u => u.Id == user.Id) > 0;
}
}
#pragma warning disable CS0618
// private!!
private static Boolean Delete(Session session)
{
var delete = Sessions.Delete(s => s.Id == session.Id) > 0;
if (delete)
Backup();
return delete;
}
public static void Delete(OrderNumber2Installation relation)
{
OrderNumber2Installation.Delete(s => s.InstallationId == relation.InstallationId && s.OrderNumber == relation.OrderNumber);
}
}

View File

@ -0,0 +1,59 @@
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.Relations;
namespace InnovEnergy.App.Backend.Database;
public static partial class Db
{
//In this file, we provide all the methods that can be used in order to retrieve information from the database (read)
public static Folder? GetFolderById(Int64? id)
{
return Folders
.FirstOrDefault(f => f.Id == id);
}
public static Installation? GetInstallationById(Int64? id)
{
return Installations
.FirstOrDefault(i => i.Id == id);
}
public static UserAction? GetActionById(Int64? id)
{
return UserActions
.FirstOrDefault(i => i.Id == id);
}
public static User? GetUserById(Int64? id)
{
return Users
.FirstOrDefault(u => u.Id == id);
}
public static User? GetUserByEmail(String email)
{
return Users
.FirstOrDefault(u => u.Email == email);
}
public static Session? GetSession(String token)
{
//This method is called in almost every controller function.
//After logging in, the frontend receives a session object which contains a token. For all the future REST API calls, this token is used for session authentication.
var session = Sessions
.FirstOrDefault(s => s.Token == token);
if (session is null)
{
return null;
}
if (!session.Valid)
{
Delete(session);
return null;
}
return session;
}
}

View File

@ -0,0 +1,60 @@
using InnovEnergy.App.Backend.DataTypes;
namespace InnovEnergy.App.Backend.Database;
public static partial class Db
{
//We can execute the updates manually for each table, but we prefer the abstract way using Connection.Update method
//We pass an object as an argument and the Connection will connect this object with the corresponding table.
//The update is being done based on the primary id of the object.
private static Boolean Update(Object obj)
{
var success = Connection.Update(obj) > 0;
if(success) Backup();
return success;
}
public static Boolean Update(Folder folder)
{
return Update(obj: folder);
}
public static Boolean Update(Error error)
{
return Update(obj: error);
}
public static Boolean Update(Warning warning)
{
return Update(obj: warning);
}
public static Boolean Update(Installation installation)
{
return Update(obj: installation);
}
public static Boolean Update(User user)
{
var originalUser = GetUserById(user.Id);
if (originalUser is null) return false;
// these columns must not be modified!
user.ParentId = originalUser.ParentId;
user.Name = originalUser.Name;
return Update(obj: user);
}
public static void UpdateAction(UserAction updatedAction)
{
var existingAction = UserActions.FirstOrDefault(action => action.Id == updatedAction.Id);
if (existingAction != null)
{
Update(updatedAction);
}
}
}

View File

@ -0,0 +1,16 @@
namespace InnovEnergy.App.Backend;
public class Exceptions : Exception
{
public String Type { get; set; }
public String Detail { get; set; }
public String Instance { get; set; }
public Int32? Status { get; set; }
public Exceptions(Int32? status, String type, String detail, String instance)
{
Type = type;
Detail = detail;
Instance = instance;
Status = status;
}
}

View File

@ -0,0 +1,8 @@
{
"SmtpServerUrl" : "mail.agenturserver.de",
"SmtpUsername" : "p518526p69",
"SmtpPassword" : "i;b*xqm4iB5uhl",
"SmtpPort" : 587,
"SenderName" : "InnovEnergy",
"SenderAddress" : "noreply@innov.energy"
}

View File

@ -0,0 +1,92 @@
using System.Diagnostics;
using Flurl.Http;
using Hellang.Middleware.ProblemDetails;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.Websockets;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.App.Backend;
public static class Program
{
public static async Task Main(String[] args)
{
//First, we initialize the database. This is an empty constructor of the Db class that will be called.
//In addition, we initialize WatchDog in order to restart the backend service in case of failure.
//Finally, we start all the backend services. We call the InitializeEnvironment function of RabbitMqManager to create the queue (factory/connection)
//Then, we generate a consumer that binds to the queue. This is a separate async Task so it must not be awaited (it acts as a separate thread).
//Finally, we call the MonitorSalimaxInstallationTable and MonitorSalidomoInstallationTable from the WebsocketManager class.
//Those methods will build in-memory data structures to track the connected frontends and update them regarding the offline installations.
Watchdog.NotifyReady();
Db.Init();
var builder = WebApplication.CreateBuilder(args);
RabbitMqManager.InitializeEnvironment();
RabbitMqManager.StartRabbitMqConsumer().SupressAwaitWarning();
WebsocketManager.MonitorSalimaxInstallationTable().SupressAwaitWarning();
WebsocketManager.MonitorSalidomoInstallationTable().SupressAwaitWarning();
builder.Services.AddControllers();
builder.Services.AddProblemDetails(setup =>
{
//This includes the stacktrace in Development Env
setup.IncludeExceptionDetails = (_, _) => builder.Environment.IsDevelopment() || builder.Environment.IsStaging();
//This handles our Exceptions
setup.Map<Exceptions>(exception => new ProblemDetails
{
Detail = exception.Detail,
Status = exception.Status,
Type = exception.Type,
Instance = exception.Instance
});
});
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", OpenApiInfo);
c.UseAllOfToExtendReferenceSchemas();
c.SupportNonNullableReferenceTypes();
});
var app = builder.Build();
app.Use(async (context, next) =>
{
context.Request.WriteLine();
await next(context);
});
app.UseWebSockets();
app.UseForwardedHeaders(new ForwardedHeadersOptions
{
ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseCors(p => p.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod()) ;
//app.UseHttpsRedirection();
app.MapControllers();
app.UseProblemDetails();
app.Run();
}
private static OpenApiInfo OpenApiInfo { get; } = new OpenApiInfo
{
Title = "Innesco Backend API",
Version = "v1"
};
}

View File

@ -0,0 +1,17 @@
{
"$schema": "https://json.schemastore.org/launchsettings.json",
"$SdkResolverGlobalJsonPath": "",
"profiles": {
"Backend": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchUrl": "swagger",
"applicationUrl": "http://localhost:7087",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development",
"HOME":"~/backend"
}
}
}
}

View File

@ -0,0 +1,9 @@
using SQLite;
namespace InnovEnergy.App.Backend.Relations;
public class FolderAccess : Relation<Int64, Int64>
{
[Indexed] public Int64 UserId { get => Left ; init => Left = value;}
[Indexed] public Int64 FolderId { get => Right; init => Right = value;}
}

View File

@ -0,0 +1,9 @@
using SQLite;
namespace InnovEnergy.App.Backend.Relations;
public class InstallationAccess : Relation<Int64, Int64>
{
[Indexed] public Int64 UserId { get => Left ; init => Left = value;}
[Indexed] public Int64 InstallationId { get => Right; init => Right = value;}
}

View File

@ -0,0 +1,10 @@
using SQLite;
namespace InnovEnergy.App.Backend.Relations;
public class OrderNumber2Installation : Relation<String, Int64>
{
[Indexed] public String OrderNumber { get => Left ; set => Left = value;}
[Indexed] public Int64 InstallationId { get => Right; set => Right = value;}
}

View File

@ -0,0 +1,34 @@
using System.Diagnostics.CodeAnalysis;
using SQLite;
namespace InnovEnergy.App.Backend.Relations;
public abstract class Relation<L,R>
{
[PrimaryKey, AutoIncrement]
[Obsolete("Do not use for any business logic")]
public Int64 Id { get; set; }
[Ignore] protected L Left { get; set; } = default!;
[Ignore] protected R Right { get; set; } = default!;
protected Boolean Equals(Relation<L, R> other)
{
return EqualityComparer<L>.Default.Equals(Left, other.Left)
&& EqualityComparer<R>.Default.Equals(Right, other.Right);
}
public override Boolean Equals(Object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
return obj.GetType() == GetType() && Equals((Relation<L, R>)obj);
}
[SuppressMessage("ReSharper", "NonReadonlyMemberInGetHashCode")]
public override Int32 GetHashCode() => HashCode.Combine(Left, Right);
public static Boolean operator ==(Relation<L, R>? left, Relation<L, R>? right) => Equals(left, right);
public static Boolean operator !=(Relation<L, R>? left, Relation<L, R>? right) => !Equals(left, right);
}

View File

@ -0,0 +1,57 @@
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.App.Backend.DataTypes.Methods;
using SQLite;
namespace InnovEnergy.App.Backend.Relations;
public class Session : Relation<String, Int64>
{
public static TimeSpan MaxAge { get; } = TimeSpan.FromDays(1);
[Unique ] public String Token { get => Left ; init => Left = value;}
[Indexed] public Int64 UserId { get => Right; init => Right = value;}
[Indexed] public DateTime LastSeen { get; set; }
public Boolean AccessToSalimax { get; set; } = false;
public Boolean AccessToSalidomo { get; set; } = false;
public Boolean AccessToSodioHome { get; set; } = false;
[Ignore] public Boolean Valid => DateTime.Now - LastSeen <=MaxAge ;
// Private backing field
private User? _User;
[Ignore] public User User
{
get => _User ??= Db.GetUserById(UserId)!;
set => _User =value;
}
[Obsolete("To be used only by deserializer")]
public Session()
{}
//We need to return a session object to the frontend. Only the public fields can be included.
//For this reason, we use the public User User. It is a public field but ignored, so it can be included to the object returned
//to the frontend but it will not get inserted to the database.
//When we initialize it like that: User = Db.GetUserById(user.Id)!, the set will be called and the private member will be initialized as well.
//What if the getSession method is called from another function of the controller?
//GetSession will retrieve a session object from the database, but this does not have the metadata included (the private fields and the ignored public fields)
//Thus, the get will be called and the private field _User will be initialized on the fly.
public Session(User user)
{
User = Db.GetUserById(user.Id)!;
Token = CreateToken();
UserId = user.Id;
LastSeen = DateTime.Now;
AccessToSalimax = user.AccessibleInstallations(product: (int)ProductType.Salimax).ToList().Count > 0;
AccessToSalidomo = user.AccessibleInstallations(product: (int)ProductType.Salidomo).ToList().Count > 0;
AccessToSodioHome = user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count > 0;
}
private static String CreateToken()
{
return Guid.NewGuid().ToString("N");
}
}

View File

@ -0,0 +1,9 @@
<?xml version="1.0" ?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>HEAD</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>

View File

@ -0,0 +1,4 @@
#!/bin/bash
sudo systemctl restart backend
sudo cp -rf ~/frontend/build/*
sudo npm install -g serve/var/www/html/monitor.innov.energy/html/

View File

@ -0,0 +1,4 @@
{
"Key": "EXOa0b53cf10517307cec1bf00e",
"Secret": "7MbZBQbHDJ1-eRsZH47BEbRvPaTT7io8H7OGqFujdQ4"
}

View File

@ -0,0 +1,4 @@
{
"Key": "EXO1abcb772bf43ab72951ba1dc",
"Secret": "_ym1KsGBSp90S5dwhZn18XD-u9Y4ghHvyIxg5gv5fHw"
}

View File

@ -0,0 +1,4 @@
{
"Key": "EXOb6d6dc1880cdd51f1ebc6692",
"Secret": "kpIey4QJlQFuWG_WoTazcY7kBEjN2f_ll2cDBeg64m4"
}

View File

@ -0,0 +1,4 @@
{
"Key": "EXO87ca85e29dd412f1238f1cf0",
"Secret": "-T9TAqy9a3-0-xj7HKsFFJOCcxfRpcnL6OW5oOrOcWU"
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,6 @@
{
"Url": "mail.agenturserver.de",
"Port": 587,
"Username": "p518526p69",
"Password": "i;b*xqm4iB5uhl"
}

View File

@ -0,0 +1,6 @@
{
"ReadOnlyS3Key": "EXO44d2979c8e570eae81ead564",
"ReadOnlyS3Secret": "55MAqyO_FqUmh7O64VIO0egq50ERn_WIAWuc2QC44QU" ,
"ReadWriteS3Key": "EXO87ca85e29dd412f1238f1cf0",
"ReadWriteS3Secret": "-T9TAqy9a3-0-xj7HKsFFJOCcxfRpcnL6OW5oOrOcWU"
}

View File

@ -0,0 +1,11 @@
namespace InnovEnergy.App.Backend.Websockets;
public class AlarmOrWarning
{
public required String Date { get; set; }
public required String Time { get; set; }
public required String Description { get; set; }
public required String CreatedBy { get; set; }
}

View File

@ -0,0 +1,10 @@
using System.Net.WebSockets;
namespace InnovEnergy.App.Backend.Websockets;
public class InstallationInfo
{
public int Status { get; set; }
public DateTime Timestamp { get; set; }
public int Product { get; set; }
public List<WebSocket> Connections { get; } = new List<WebSocket>();
}

View File

@ -0,0 +1,179 @@
using System.Text;
using System.Text.Json;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.Lib.Utils;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace InnovEnergy.App.Backend.Websockets;
public static class RabbitMqManager
{
public static ConnectionFactory Factory = null!;
public static IConnection Connection = null!;
public static IModel Channel = null!;
//This function will be called from the Backend/Program.cs
public static void InitializeEnvironment()
{
string vpnServerIp = "10.2.0.11";
//Subscribe to RabbitMq queue as a consumer
Factory = new ConnectionFactory
{
HostName = vpnServerIp,
Port = 5672,
VirtualHost = "/",
UserName = "consumer",
Password = "faceaddb5005815199f8366d3d15ff8a",
};
Connection = Factory.CreateConnection();
Channel = Connection.CreateModel();
Console.WriteLine("Middleware subscribed to RabbitMQ queue, ready for receiving messages");
Channel.QueueDeclare(queue: "statusQueue", durable: true, exclusive: false, autoDelete: false, arguments: null);
}
public static async Task StartRabbitMqConsumer()
{
//Wait to receive a message from an installation
var consumer = new EventingBasicConsumer(Channel);
consumer.Received += (_, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
//A message can be an alarm, a warning or a heartbit
StatusMessage? receivedStatusMessage = JsonSerializer.Deserialize<StatusMessage>(message);
lock (WebsocketManager.InstallationConnections)
{
//Consumer received a message
if (receivedStatusMessage != null)
{
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == receivedStatusMessage.Product && f.S3BucketId == receivedStatusMessage.InstallationId);
int installationId = (int)installation.Id;
//This is a heartbit message, just update the timestamp for this installation.
//There is no need to notify the corresponding front-ends.
//Every 15 iterations(30 seconds), the installation sends a heartbit message to the queue.
if (receivedStatusMessage.Type == MessageType.Heartbit)
{
//Do not do anything here, just for debugging purposes.
}
else
{
//Traverse the Warnings list, and store each of them to the database
if (receivedStatusMessage.Warnings != null)
{
foreach (var warning in receivedStatusMessage.Warnings)
{
Warning newWarning = new Warning
{
InstallationId = installationId,
Description = warning.Description,
Date = warning.Date,
Time = warning.Time,
DeviceCreatedTheMessage = warning.CreatedBy,
Seen = false
};
//Create a new warning and add it to the database
//Console.WriteLine("Add a warning for installation "+installationId);
Db.HandleWarning(newWarning, installationId);
}
}
//Traverse the Alarm list, and store each of them to the database
if (receivedStatusMessage.Alarms != null)
{
string monitorLink;
if (installation.Product == (int)ProductType.Salimax)
{
monitorLink =
$"https://monitor.innov.energy/installations/list/installation/{installation.S3BucketId}/batteryview";
}
else
{
monitorLink =
$"https://monitor.innov.energy/salidomo_installations/list/installation/{installation.S3BucketId}/batteryview";
}
foreach (var alarm in receivedStatusMessage.Alarms)
{
Error newError = new Error
{
InstallationId = installation.Id,
Description = alarm.Description,
Date = alarm.Date,
Time = alarm.Time,
DeviceCreatedTheMessage = alarm.CreatedBy,
Seen = false
};
//Console.WriteLine("Add an alarm for installation "+installationId);
// Send replace battery email to support team if this alarm is "NeedToReplaceBattery"
if (alarm.Description == "2 or more string are disabled")
{
Console.WriteLine("Send replace battery email to the support team for installation "+installationId);
string recipient = "support@innov.energy";
string subject = $"Battery Alarm from {installation.InstallationName}: 2 or more strings broken";
string text = $"Dear InnovEnergy Support Team,\n" +
$"\n"+
$"Installation Name: {installation.InstallationName}\n"+
$"\n"+
$"Installation Monitor Link: {monitorLink}\n"+
$"\n"+
$"Please exchange: {alarm.CreatedBy}\n"+
$"\n"+
$"Error created date and time: {alarm.Date} {alarm.Time}\n"+
$"\n"+
$"Thank you for your great support:)";
//Disable this function now
//Mailer.Send("InnovEnergy Support Team", recipient, subject, text);
}
//Create a new error and add it to the database
Db.HandleError(newError, installationId);
}
}
}
Int32 prevStatus;
//This installation id does not exist in our in-memory data structure, add it.
if (!WebsocketManager.InstallationConnections.ContainsKey(installationId))
{
prevStatus = -2;
//Console.WriteLine("Create new empty list for installation: " + installationId);
WebsocketManager.InstallationConnections[installationId] = new InstallationInfo
{
Status = receivedStatusMessage.Status,
Timestamp = DateTime.Now,
Product = installation.Product
};
}
else
{
prevStatus = WebsocketManager.InstallationConnections[installationId].Status;
WebsocketManager.InstallationConnections[installationId].Status = receivedStatusMessage.Status;
WebsocketManager.InstallationConnections[installationId].Timestamp = DateTime.Now;
}
installation.Status = receivedStatusMessage.Status;
installation.Apply(Db.Update);
//Console.WriteLine("----------------------------------------------");
//If the status has changed, update all the connected front-ends regarding this installation
if(prevStatus != receivedStatusMessage.Status && WebsocketManager.InstallationConnections[installationId].Connections.Count > 0)
{
WebsocketManager.InformWebsocketsForInstallation(installationId);
}
}
}
};
Channel.BasicConsume(queue: "statusQueue", autoAck: true, consumer: consumer);
}
}

View File

@ -0,0 +1,19 @@
namespace InnovEnergy.App.Backend.Websockets;
public class StatusMessage
{
public required Int32 InstallationId { get; set; }
public required Int32 Product { get; set; }
public required Int32 Status { get; set; }
public required MessageType Type { get; set; }
public List<AlarmOrWarning>? Warnings { get; set; }
public List<AlarmOrWarning>? Alarms { get; set; }
public Int32 Timestamp { get; set; }
}
public enum MessageType
{
AlarmOrWarning,
Heartbit
}

View File

@ -0,0 +1,238 @@
using System.Net;
using System.Net.Sockets;
using System.Net.WebSockets;
using System.Text;
using System.Text.Json;
using InnovEnergy.App.Backend.Database;
using InnovEnergy.App.Backend.DataTypes;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.App.Backend.Websockets;
public static class WebsocketManager
{
public static Dictionary<Int64, InstallationInfo> InstallationConnections = new Dictionary<Int64, InstallationInfo>();
//Every 1 minute, check the timestamp of the latest received message for every installation.
//If the difference between the two timestamps is more than two minutes, we consider this Salimax installation unavailable.
public static async Task MonitorSalimaxInstallationTable()
{
while (true){
lock (InstallationConnections){
Console.WriteLine("MONITOR SALIMAX INSTALLATIONS\n");
foreach (var installationConnection in InstallationConnections){
if (installationConnection.Value.Product==(int)ProductType.Salimax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)){
// Console.WriteLine("Installation ID is "+installationConnection.Key);
// Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp);
// Console.WriteLine("diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
installationConnection.Value.Status = (int)StatusType.Offline;
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.Salimax && f.Id == installationConnection.Key);
installation.Status = (int)StatusType.Offline;
installation.Apply(Db.Update);
if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);}
}
}
Console.WriteLine("FINISHED MONITORING SALIMAX INSTALLATIONS\n");
}
await Task.Delay(TimeSpan.FromMinutes(1));
}
}
//Every 1 minute, check the timestamp of the latest received message for every installation.
//If the difference between the two timestamps is more than 1 hour, we consider this Salidomo installation unavailable.
public static async Task MonitorSalidomoInstallationTable()
{
while (true){
Console.WriteLine("TRY TO LOCK FOR MONITOR SALIDOMO INSTALLATIONS\n");
lock (InstallationConnections){
Console.WriteLine("MONITOR SALIDOMO INSTALLATIONS\n");
foreach (var installationConnection in InstallationConnections)
{
//Console.WriteLine("Installation ID is "+installationConnection.Key);
if (installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) < TimeSpan.FromMinutes(60)){
Console.WriteLine("Installation ID is "+installationConnection.Key + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
}
if (installationConnection.Value.Product==(int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60))
{
//Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp);
//Console.WriteLine("timestamp now is is "+(DateTime.Now));
Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.Salidomo && f.Id == installationConnection.Key);
Console.WriteLine("Installation ID is "+installation.Name + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp));
installation.Status = (int)StatusType.Offline;
installation.Apply(Db.Update);
installationConnection.Value.Status = (int)StatusType.Offline;
if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);}
else{Console.WriteLine("NONE IS CONNECTED TO THAT INSTALLATION-------------------------------------------------------------");}
}
}
Console.WriteLine("FINISHED WITH UPDATING\n");
}
await Task.Delay(TimeSpan.FromMinutes(1));
}
}
//Inform all the connected websockets regarding installation "installationId"
public static void InformWebsocketsForInstallation(Int64 installationId)
{
var installation = Db.GetInstallationById(installationId);
var installationConnection = InstallationConnections[installationId];
Console.WriteLine("Update all the connected websockets for installation " + installation.Name);
var jsonObject = new
{
id = installationId,
status = installationConnection.Status,
testingMode = installation.TestingMode
};
string jsonString = JsonSerializer.Serialize(jsonObject);
byte[] dataToSend = Encoding.UTF8.GetBytes(jsonString);
foreach (var connection in installationConnection.Connections)
{
connection.SendAsync(
new ArraySegment<byte>(dataToSend, 0, dataToSend.Length),
WebSocketMessageType.Text,
true, // Indicates that this is the end of the message
CancellationToken.None
);
}
}
public static async Task HandleWebSocketConnection(WebSocket currentWebSocket)
{
var buffer = new byte[4096];
try
{
while (currentWebSocket.State == WebSocketState.Open)
{
//Listen for incoming messages on this WebSocket
var result = await currentWebSocket.ReceiveAsync(buffer, CancellationToken.None);
if (result.MessageType != WebSocketMessageType.Text)
continue;
var message = Encoding.UTF8.GetString(buffer, 0, result.Count);
var installationIds = JsonSerializer.Deserialize<int[]>(message);
//This is a ping message to keep the connection alive, reply with a pong
if (installationIds[0] == -1)
{
var jsonObject = new
{
id = -1,
status = -1
};
var jsonString = JsonSerializer.Serialize(jsonObject);
var dataToSend = Encoding.UTF8.GetBytes(jsonString);
currentWebSocket.SendAsync(dataToSend,
WebSocketMessageType.Text,
true,
CancellationToken.None
);
continue;
}
//Received a new message from this websocket.
//We have a HandleWebSocketConnection per connected frontend
lock (InstallationConnections)
{
List<WebsocketMessage> dataToSend = new List<WebsocketMessage>();
//Each front-end will send the list of the installations it wants to access
//If this is a new key (installation id), initialize the list for this key and then add the websocket object for this client
//Then, report the status of each requested installation to the front-end that created the websocket connection
foreach (var installationId in installationIds)
{
var installation = Db.GetInstallationById(installationId);
if (!InstallationConnections.ContainsKey(installationId))
{
//Since we keep all the changes to the database, in case that the backend reboots, we need to update the in-memory data structure.
//Thus, if the status is -1, we put an old timestamp, otherwise, we put the most recent timestamp.
//We store everything to the database, because when the backend reboots, we do not want to wait until all the installations send the heartbit messages.
//We want the in memory data structure to be up to date immediately.
InstallationConnections[installationId] = new InstallationInfo
{
Status = installation.Status,
Timestamp = installation.Status==(int)StatusType.Offline ? DateTime.Now.AddDays(-1) : DateTime.Now,
Product = installation.Product
};
}
InstallationConnections[installationId].Connections.Add(currentWebSocket);
var jsonObject = new WebsocketMessage
{
id = installationId,
status = InstallationConnections[installationId].Status,
testingMode = installation.TestingMode
};
dataToSend.Add(jsonObject);
}
var jsonString = JsonSerializer.Serialize(dataToSend);
var encodedDataToSend = Encoding.UTF8.GetBytes(jsonString);
currentWebSocket.SendAsync(encodedDataToSend,
WebSocketMessageType.Text,
true, // Indicates that this is the end of the message
CancellationToken.None
);
Console.WriteLine("Printing installation connection list");
Console.WriteLine("----------------------------------------------");
foreach (var installationConnection in InstallationConnections)
{
Console.WriteLine("Installation ID: " + installationConnection.Key + " Number of Connections: " + installationConnection.Value.Connections.Count);
}
Console.WriteLine("----------------------------------------------");
}
}
lock (InstallationConnections)
{
//When the front-end terminates the connection, the following code will be executed
Console.WriteLine("The connection has been terminated");
foreach (var installationConnection in InstallationConnections)
{
if (installationConnection.Value.Connections.Contains(currentWebSocket))
{
installationConnection.Value.Connections.Remove(currentWebSocket);
}
}
}
await currentWebSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Connection closed by server", CancellationToken.None);
lock (InstallationConnections)
{
//Print the installationConnections dictionary after deleting a websocket
Console.WriteLine("Print the installation connections list after deleting a websocket");
Console.WriteLine("----------------------------------------------");
foreach (var installationConnection in InstallationConnections)
{
Console.WriteLine("Installation ID: " + installationConnection.Key + " Number of Connections: " + installationConnection.Value.Connections.Count);
}
Console.WriteLine("----------------------------------------------");
}
}
catch (Exception ex)
{
Console.WriteLine("WebSocket error: " + ex.Message);
}
}
}

Binary file not shown.

View File

@ -0,0 +1,5 @@
#To deploy to the monitor server, uncomment the following line
dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@194.182.190.208:~/backend && ssh ubuntu@194.182.190.208 'sudo systemctl restart backend'
#To deploy to the stage server, uncomment the following line
#dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@91.92.154.141:~/backend && ssh ubuntu@91.92.154.141 'sudo systemctl restart backend'

View File

@ -0,0 +1 @@
dotnet publish Backend.csproj -c Release -r linux-x64 --self-contained true -p:PublishTrimmed=false && rsync -av bin/Release/net6.0/linux-x64/publish/ ubuntu@91.92.154.141:~/backend && ssh ubuntu@91.92.154.141 'sudo systemctl restart backend'

View File

@ -0,0 +1,95 @@
using CliWrap;
using CliWrap.Buffered;
using InnovEnergy.Lib.Utils;
using static InnovEnergy.App.BmsTunnel.CliPrograms;
namespace InnovEnergy.App.BmsTunnel;
using Nodes = IReadOnlyList<Byte>;
public static class BatteryTty
{
const String DevDir = "/dev";
public static async Task<String?> GetTty()
{
Console.WriteLine("searching battery connection...");
return await GetTtyFromDBus()
?? await GetTtyFromDev();
}
private static async Task<String?> GetTtyFromDev()
{
var ttys = await FileSystem
.Local
.GetFiles(DevDir, FileType.CharacterDevice);
var candidateTtys = ttys
.Where(f => f.StartsWith(DevDir + "/ttyUSB"))
.NotNull()
.ToList();
if (!candidateTtys.Any())
{
Console.WriteLine("no USB converter cable attached!");
return null;
}
if (candidateTtys.Count == 1)
return candidateTtys[0];
return "Select TTY:".ChooseFrom(candidateTtys);
}
private static async Task<String?> GetTtyFromDBus()
{
var tty = await LsDBus
.ExecuteBufferedAsync()
.Select(ParseBatteryTty);
if (tty == null)
return null;
Console.WriteLine("found battery on DBus");
return $"{DevDir}/{tty}";
}
private static CommandTask<Nodes> GetNodes(String tty)
{
const String defaultArgs = "--system --print-reply --type=method_call / com.victronenergy.BusItem.GetValue";
return DBusSend
.AppendArgument($"--dest=com.victronenergy.battery.{tty}")
.AppendArgument(defaultArgs)
.ExecuteBufferedAsync()
.Select(ParseBatteryNodes);
}
private static Nodes ParseBatteryNodes(BufferedCommandResult result)
{
return result
.StandardOutput
.Split(Environment.NewLine)
.Where(l => l.Contains("_Battery/"))
.Select(l => l.Split('/')[1])
.Where(n => Byte.TryParse(n, out _))
.Select(Byte.Parse)
.Distinct()
.OrderBy(n => n)
.ToList();
}
private static String? ParseBatteryTty(BufferedCommandResult result)
{
return result
.StandardOutput
.Split(Environment.NewLine)
.Where(l => l.Contains("com.victronenergy.battery."))
.SelectMany(l => l.Split('.'))
.LastOrDefault();
}
}

View File

@ -0,0 +1,191 @@
using System.IO.Ports;
using System.Text;
using CliWrap.Buffered;
using InnovEnergy.Lib.Utils;
namespace InnovEnergy.App.BmsTunnel;
public class BmsTunnel : IDisposable
{
private SerialPort SerialPort { get; }
public String Tty { get; }
public Byte Node { get; set; }
private const Int32 BaudRate = 115200;
private const Int32 DataBits = 8;
private const Parity Parity = System.IO.Ports.Parity.Even;
private const StopBits StopBits = System.IO.Ports.StopBits.One;
private const Int32 CrcLength = 2;
private const Byte TunnelCode = 0x41;
private const String CrcError = "?? CRC FAILED";
public BmsTunnel(String tty, Byte node)
{
Tty = tty;
Node = node;
StopSerialStarter();
SerialPort = new SerialPort(Tty, BaudRate, Parity, DataBits, StopBits);
SerialPort.ReadTimeout = 100;
SerialPort.Open();
}
private IEnumerable<Byte> Header
{
get
{
yield return Node;
yield return TunnelCode;
}
}
private static IEnumerable<Byte> NewLine
{
get
{
yield return 0x0D;
}
}
public IEnumerable<String> SendCommand(String command)
{
var reply = SendSingleCommand(command);
while (!reply.StartsWith("??"))
{
yield return reply;
if (reply.EndsWith("chars answered. Ready."))
yield break;
reply = GetMore();
}
if (reply == CrcError)
{
yield return "";
yield return CrcError.Substring(3);
}
}
private String GetMore() => SendSingleCommand("");
private String SendSingleCommand(String command)
{
var payload = Header
.Concat(CommandToBytes(command))
.ToList();
var crc = CalcCrc(payload);
payload.AddRange(crc);
SerialPort.Write(payload.ToArray(), 0, payload.Count);
var response = Enumerable
.Range(0, 255)
.Select(ReadByte)
.TakeWhile(b => b >= 0)
.Select(Convert.ToByte)
.ToArray();
if (!CheckCrc(response))
{
// TODO: this should go into outer loop instead of returning magic value CrcError
//Console.WriteLine(BitConverter.ToString(response).Replace("-", " "));
return CrcError;
}
return response
.Skip(2)
.TakeWhile(b => b != 0x0D)
.ToArray()
.Apply(Encoding.ASCII.GetString);
Int32 ReadByte<T>(T _)
{
try
{
return SerialPort.ReadByte();
}
catch (TimeoutException)
{
return -1;
}
}
}
private static IReadOnlyList<Byte> CalcCrc(IEnumerable<Byte> data)
{
UInt16 crc = 0xFFFF;
foreach (var b in data)
{
crc ^= b;
for (var bit = 0; bit < 8; bit++)
{
var bit0 = (crc & 0x0001) != 0;
crc >>= 1;
if (bit0) crc ^= 0xA001;
}
}
var hi = 0xFF & crc;
var lo = (crc >> 8) & 0xFF;
return new[] {(Byte) hi, (Byte) lo}; // big endian
}
private static Boolean CheckCrc(IReadOnlyList<Byte> data)
{
var expectedCrc = data.SkipLast(CrcLength).Apply(CalcCrc);
var actualCrc = data.TakeLast(CrcLength);
return actualCrc.SequenceEqual(expectedCrc);
}
private static IEnumerable<Byte> CommandToBytes(String command)
{
if (command == "")
return Enumerable.Empty<Byte>();
return command
.Apply(Encoding.ASCII.GetBytes)
.Concat(NewLine);
}
private void StopSerialStarter()
{
CliPrograms.StopTty
.WithArguments(Tty)
.ExecuteBufferedAsync()
.Task
.Wait(3000);
}
private void StartSerialStarter()
{
CliPrograms.StartTty
.WithArguments(Tty)
.ExecuteBufferedAsync()
.Task
.Wait(3000);
}
public void Dispose()
{
SerialPort.Dispose();
StartSerialStarter();
}
}

View File

@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../InnovEnergy.App.props" />
<PropertyGroup>
<RootNamespace>InnovEnergy.App.BmsTunnel</RootNamespace>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="CliWrap" Version="3.6.0" />
<PackageReference Include="System.IO.Ports" Version="7.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="../../Lib/Utils/Utils.csproj" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,11 @@
using CliWrap;
namespace InnovEnergy.App.BmsTunnel;
public static class CliPrograms
{
public static Command LsDBus { get; } = Cli.Wrap("/opt/innovenergy/scripts/lsdbus");
public static Command DBusSend { get; } = Cli.Wrap("/usr/bin/dbus-send");
public static Command StartTty { get; } = Cli.Wrap("/opt/victronenergy/serial-starter/start-tty.sh");
public static Command StopTty { get; } = Cli.Wrap("/opt/victronenergy/serial-starter/stop-tty.sh");
}

View File

@ -0,0 +1,85 @@
// dotnet publish BmsTunnel.csproj -c Release -r linux-arm -p:PublishSingleFile=true --self-contained true && \
// rsync -av bin/Release/net6.0/linux-arm/publish/ root@10.2.1.6:/home/root/tunnel && clear && \
// ssh root@10.2.1.6 /home/root/tunnel/BmsTunnel
using InnovEnergy.Lib.Utils;
using static System.String;
namespace InnovEnergy.App.BmsTunnel;
public static class Program
{
private const Byte DefaultNode = 2;
public static async Task<Int32> Main(String[] args)
{
var tty = await BatteryTty.GetTty();
if (tty is null)
return 2;
Console.WriteLine("\nstarting BMS tunnel\n");
using var tunnel = new BmsTunnel(tty, 2);
ExplainNode();
ExplainExit();
//Console.WriteLine("");
while (true)
{
//Console.WriteLine("");
Console.Write($"node{tunnel.Node}> ");
var cmd = Console.ReadLine()?.ToUpper().Trim();
if (IsNullOrEmpty(cmd))
continue;
if (cmd.StartsWith("/"))
{
var exit = ProcessLocalCommand(cmd);
if (exit)
break;
continue;
}
tunnel.SendCommand(cmd).Skip(1).ForEach(Console.WriteLine);
}
Boolean ProcessLocalCommand(String cmd)
{
cmd = cmd.TrimStart('/').Trim().ToUpper();
if (cmd == "EXIT")
return true;
if (cmd.StartsWith("NODE "))
ChangeNode(cmd);
else
Console.WriteLine("unrecognized command");
return false;
}
return 0;
void ChangeNode(String cmd)
{
var ndStr = cmd[5..].Trim();
if (!Byte.TryParse(ndStr, out var newNode))
{
ExplainNode();
return;
}
tunnel.Node = newNode;
}
}
private static void ExplainExit() => Console.WriteLine("/exit exit bms cli");
private static void ExplainNode() => Console.WriteLine("/node <nb> change to node number <nb>");
}

View File

@ -0,0 +1,21 @@
#!/bin/bash
csproj="BmsTunnel.csproj"
exe="BmsTunnel"
remote="10.2.1.6"
#remote="10.2.2.152"
netVersion="net6.0"
platform="linux-arm"
config="Release"
host="root@$remote"
dir="/data/innovenergy/$exe"
set -e
dotnet publish "$csproj" -c $config -r $platform -p:SuppressTrimmAnalysisWarnings=true -p:PublishSingleFile=true -p:PublishTrimmed=true -p:DebugType=None -p:DebugSymbols=false --self-contained true
rsync -av "bin/$config/$netVersion/$platform/publish/" "$host:$dir"
clear
ssh "$host" "$dir/$exe"

View File

@ -0,0 +1,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="../InnovEnergy.App.props" />
<PropertyGroup>
<RootNamespace>InnovEnergy.App.Collector</RootNamespace>
<IsTrimmable>false</IsTrimmable>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="../../Lib/Utils/Utils.csproj" />
<ProjectReference Include="../../Lib/WebServer/WebServer.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.Linq.Async" Version="6.0.1" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,343 @@
using System.Net;
using System.Text;
using InnovEnergy.App.Collector.Influx;
using InnovEnergy.App.Collector.Records;
using InnovEnergy.App.Collector.Utils;
using InnovEnergy.Lib.Utils;
using InnovEnergy.Lib.Utils.Net;
using Convert = System.Convert;
namespace InnovEnergy.App.Collector;
using Data = IReadOnlyList<String>;
public static class BatteryDataParser
{
public static IReadOnlyList<BatteryRecord> ParseDatagram(UdpDatagram datagram)
{
return ParseV3Datagram(datagram);
// if (IsV4Payload(buffer))
// return ParseV4Datagram(endPoint, buffer);
// throw new Exception($"Wrong protocol header: Expected '{Settings.ProtocolV3}'");
}
private static Boolean IsV4Payload(Byte[] buffer)
{
return buffer
.ParseLengthValueEncoded()
.First()
.ToArray()
.Apply(Encoding.UTF8.GetString)
.Equals(Settings.ProtocolV4);
}
private static Boolean IsV3Payload(IEnumerable<Byte> buffer)
{
return buffer
.ToArray(Settings.ProtocolV3.Length)
.Apply(Encoding.UTF8.GetString)
.Equals(Settings.ProtocolV3);
}
// private static LineProtocolPayload ParseV4Datagram(IPEndPoint endPoint, Byte[] buffer)
// {
// var timeOfArrival = DateTime.UtcNow; // influx wants UTC
//
// BatteryDataParserV4.ParseV4Datagram(endPoint, buffer);
// return new LineProtocolPayload();
// }
private static IReadOnlyList<BatteryRecord> ParseV3Datagram(UdpDatagram datagram)
{
var timeOfArrival = DateTime.UtcNow;
var data = datagram
.Payload
.ToArray()
.Apply(Encoding.UTF8.GetString)
.Split('\n');
data.ParseProtocolVersion().Apply(CheckProtocolId);
var installationName = data.ParseInstallationName();
return ParseBatteryRecords(data, installationName, timeOfArrival, datagram.EndPoint);
}
private static String ParseString(this Data data, Int32 i) => data[i].Trim();
private static UInt16 ParseUInt16(this Data data, Int32 i) => UInt16.Parse(ParseString(data, i));
private static UInt16 ParseUInt16Register(this Data data, Int32 register)
{
var i = register.RegToIndex();
return data.ParseUInt16(i);
}
private static UInt32 ParseUInt32Register(this Data data, Int32 register)
{
var lo = ParseUInt16Register(data, register);
var hi = ParseUInt16Register(data, register + 1);
return Convert.ToUInt32(((hi << 16) | lo) & UInt32.MaxValue);
}
private static UInt64 ParseUInt64Register(this Data data, Int32 register)
{
return Enumerable
.Range(register, 4)
.Reverse()
.Select(data.ParseUInt16Register)
.Aggregate(0ul, (a, b) => a << 16 | b);
}
private static Decimal ParseDecimalRegister(this Data data,
Int32 register,
Decimal scaleFactor = 1,
Decimal offset = 0)
{
var i = register.RegToIndex();
Int32 n = data.ParseUInt16(i);
if (n >= 0x8000)
n -= 0x10000; // fiamm stores their integers signed AND with sign-offset @#%^&!
return (Convert.ToDecimal(n) + offset) * scaleFactor; // according fiamm doc
}
private static Int32 RegToIndex(this Int32 register) => register - 992;
private static Leds ParseLeds(this Data data, String installation, String batteryId)
{
var ledBitmap = ParseUInt16Register(data, 1004);
LedState Led(Int32 n) => (LedState) (ledBitmap >> (n * 2) & 0b11);
return new Leds
{
Installation = installation,
BatteryId = batteryId,
Green = Led(0),
Amber = Led(1),
Blue = Led(2),
Red = Led(3),
};
}
private static IoStatus ParseIoStatus(this Data data, String installation, String batteryId)
{
var ioStatusBitmap = data.ParseUInt16Register(1013);
Boolean IoStatus(Int32 b) => (ioStatusBitmap >> b & 1) > 0;
return new IoStatus
{
Installation = installation,
BatteryId = batteryId,
MainSwitchClosed = IoStatus(0),
AlarmOutActive = IoStatus(1),
InternalFanActive = IoStatus(2),
VoltMeasurementAllowed = IoStatus(3),
AuxRelay = IoStatus(4),
RemoteState = IoStatus(5),
HeatingOn = IoStatus(6),
};
}
private static Warnings ParseWarnings(this Data data, String installation, String batteryId)
{
var warningsBitmap = data.ParseUInt64Register(1005);
Boolean Warning(Int32 b) => (warningsBitmap >> b & 1ul) > 0;
return new Warnings
{
Installation = installation,
BatteryId = batteryId,
TaM1 = Warning(1),
TbM1 = Warning(4),
VBm1 = Warning(6),
VBM1 = Warning(8),
IDM1 = Warning(10),
vsM1 = Warning(24),
iCM1 = Warning(26),
iDM1 = Warning(28),
MID1 = Warning(30),
BLPW = Warning(32),
Ah_W = Warning(35),
MPMM = Warning(38),
TCMM = Warning(39),
TCdi = Warning(40),
LMPW = Warning(44)
};
}
private static Alarms ParseAlarms(this Data data, String installation, String batteryId)
{
var alarmsBitmap = data.ParseUInt64Register(1009);
Boolean Alarm(Int32 b) => (alarmsBitmap >> b & 1ul) > 0;
return new Alarms
{
Installation = installation,
BatteryId = batteryId,
Tam = Alarm(0),
TaM2 = Alarm(2),
Tbm = Alarm(3),
TbM2 = Alarm(5),
VBm2 = Alarm(7),
VBM2 = Alarm(9),
IDM2 = Alarm(11),
ISOB = Alarm(12),
MSWE = Alarm(13),
FUSE = Alarm(14),
HTRE = Alarm(15),
TCPE = Alarm(16),
CME = Alarm(18),
HWFL = Alarm(19),
HWEM = Alarm(20),
ThM = Alarm(21),
vsm1 = Alarm(22),
vsm2 = Alarm(23),
vsM2 = Alarm(25),
iCM2 = Alarm(27),
iDM2 = Alarm(29),
MID2 = Alarm(31),
CCBF = Alarm(33),
AhFL = Alarm(34),
TbCM = Alarm(36),
HTFS = Alarm(42),
DATA = Alarm(43),
LMPA = Alarm(45),
HEBT = Alarm(46),
};
}
private static BatteryStatus ParseBatteryStatus(this Data data,
String installation,
String batteryId,
Decimal temperature,
Warnings warnings,
Alarms alarms,
DateTime lastSeen,
IPEndPoint endPoint)
{
var activeWarnings = Active(warnings);
var activeAlarms = Active(alarms);
return new BatteryStatus
{
InstallationName = installation,
BatteryId = batteryId,
HardwareVersion = data.ParseString(3),
FirmwareVersion = data.ParseString(4),
BmsVersion = data.ParseString(5),
AmpereHours = data.ParseUInt16(6),
Soc = data.ParseDecimalRegister(1053, 0.1m),
Voltage = data.ParseDecimalRegister(999, 0.01m),
Current = data.ParseDecimalRegister(1000, 0.01m, -10000m),
BusVoltage = data.ParseDecimalRegister(1001, 0.01m),
Temperature = temperature,
RtcCounter = data.ParseUInt32Register(1050),
IpAddress = endPoint.Address.ToString(),
Port = endPoint.Port,
// stuff below really should be done by Grafana/InfluxDb, but not possible (yet)
// aka hacks to get around limitations of Grafana/InfluxDb
NumberOfWarnings = activeWarnings.Count,
NumberOfAlarms = activeAlarms.Count,
WarningsBitmap = data.ParseUInt64Register(1005),
AlarmsBitmap = data.ParseUInt64Register(1009),
LastSeen = lastSeen.ToInfluxTime()
};
static IReadOnlyCollection<String> Active(BatteryRecord record) => record
.GetFields()
.Where(f => f.value is true)
.Select(f => f.key)
.ToList();
}
private static Temperatures ParseTemperatures(this Data data, String installation, String batteryId)
{
return new Temperatures
{
Installation = installation,
BatteryId = batteryId,
Battery = data.ParseDecimalRegister(1003, 0.1m, -400m),
Board = data.ParseDecimalRegister(1014, 0.1m, -400m),
Center = data.ParseDecimalRegister(1015, 0.1m, -400m),
Lateral1 = data.ParseDecimalRegister(1016, 0.1m, -400m),
Lateral2 = data.ParseDecimalRegister(1017, 0.1m, -400m),
CenterHeaterPwm = data.ParseDecimalRegister(1018, 0.1m),
LateralHeaterPwm = data.ParseDecimalRegister(1019, 0.1m),
};
}
private static String CheckProtocolId(String ascii)
{
var protocolId = ascii.Substring(0, Settings.ProtocolV3.Length);
if (protocolId != Settings.ProtocolV3)
throw new Exception($"Wrong protocol header: Expected '{Settings.ProtocolV3}' but got '{protocolId}'");
return protocolId;
}
private static IReadOnlyList<BatteryRecord> ParseBatteryRecords(Data data,
String installation,
DateTime lastSeen,
IPEndPoint endPoint)
{
var batteryId = data.ParseBatteryId();
var warnings = data.ParseWarnings (installation, batteryId);
var alarms = data.ParseAlarms (installation, batteryId);
var leds = data.ParseLeds (installation, batteryId);
var temperatures = data.ParseTemperatures (installation, batteryId);
var ioStatus = data.ParseIoStatus (installation, batteryId);
var batteryStatus = data.ParseBatteryStatus(installation,
batteryId,
temperatures.Battery,
warnings,
alarms,
lastSeen,
endPoint);
return new BatteryRecord[]
{
batteryStatus,
temperatures,
leds,
ioStatus,
warnings,
alarms
};
}
private static String ParseProtocolVersion (this Data data) => data.ParseString(0);
private static String ParseInstallationName(this Data data) => data.ParseString(1);
private static String ParseBatteryId (this Data data) => data.ParseString(2);
}

View File

@ -0,0 +1,365 @@
using System.Net;
using System.Text;
using InnovEnergy.App.Collector.Influx;
using InnovEnergy.App.Collector.Records;
using InnovEnergy.App.Collector.Utils;
using InnovEnergy.Lib.Utils;
using Convert = System.Convert;
// NOT (YET) USED
namespace InnovEnergy.App.Collector;
using Data = IReadOnlyList<String>;
public static class BatteryDataParserV4
{
// public static LineProtocolPayload ParseV4Datagram(IPEndPoint endPoint, Byte[] buffer)
// {
// var timeOfArrival = DateTime.UtcNow; // influx wants UTC
//
// Log.Info($"Got V4 datagram from {endPoint}");
//
// using var enumerator = buffer.ParseLengthValueEncoded().GetEnumerator();
//
// var protocolVersion = enumerator
// .Next()
// .ToArray()
// .Apply(Encoding.UTF8.GetString)
// .Equals(Settings.ProtocolV4);
//
// var nBatteries = enumerator.NextByte();
//
// foreach (var _ in Enumerable.Range(0, nBatteries))
// {
// ParseBattery(enumerator);
// }
//
//
// return new LineProtocolPayload();
// }
private static void ParseBattery(IEnumerator<ArraySegment<Byte>> e)
{
// var hardwareVersion = e.NextString();
// var firmwareVersion = e.NextString();
// var bmsVersion = e.NextString();
//
// var modbusData = e.Next();
//
// Int32 ReadRegisterAtIndex(Int32 index) => (modbusData[index * 2] << 8) + modbusData[index * 2 + 1];
// Int32 ReadRegister(Int32 register) => ReadRegisterAtIndex(register - 999);
//
// Double ReadDouble(Int32 register, Double scaleFactor = 1, Double offset = 0)
// {
// var value = ReadRegisterAtIndex(register - 999);
//
// if (value > 0x8000)
// value -= 0x10000; // fiamm stores their integers signed AND with sign-offset @#%^&!
//
// return (value + offset) * scaleFactor;
// }
// TODO
}
private static Byte NextByte(this IEnumerator<ArraySegment<Byte>> enumerator)
{
return enumerator.Next().Single();
}
private static String NextString(this IEnumerator<ArraySegment<Byte>> enumerator)
{
return enumerator
.Next()
.ToArray()
.Apply(Encoding.UTF8.GetString);
}
private static String ParseString(this Data data, Int32 i) => data[i].Trim();
private static UInt16 ParseUInt16(this Data data, Int32 i) => UInt16.Parse(ParseString(data, i));
private static UInt16 ParseUInt16Register(this Data data, Int32 register)
{
var i = register.RegToIndex();
return data.ParseUInt16(i);
}
private static UInt32 ParseUInt32Register(this Data data, Int32 register)
{
var lo = ParseUInt16Register(data, register);
var hi = ParseUInt16Register(data, register + 1);
return Convert.ToUInt32(lo | (hi << 16));
}
private static UInt64 ParseUInt64Register (this Data data, Int32 register)
{
return Enumerable
.Range(0, 4)
.Select(i => Convert.ToUInt64(data.ParseUInt16Register(register + i)) << (i * 16))
.Aggregate(0ul, (a, b) => a + b); // Sum() does not work for UInt64 :(
}
private static Decimal ParseDecimalRegister(this Data data,
Int32 register,
Decimal scaleFactor = 1,
Decimal offset = 0)
{
var i = register.RegToIndex();
UInt32 n = data.ParseUInt16(i);
if (n >= 0x8000)
n -= 0x10000; // fiamm stores their integers signed AND with sign-offset @#%^&!
return (Convert.ToDecimal(n) + offset) * scaleFactor; // according fiamm doc
}
private static Int32 RegToIndex(this Int32 register) => register - 992;
private static Leds ParseLeds(this Data data, String installation, String batteryId)
{
var ledBitmap = ParseUInt16Register(data, 1004);
LedState Led(Int32 n) => (LedState) (ledBitmap >> (n * 2) & 0b11);
return new Leds
{
Installation = installation,
BatteryId = batteryId,
Green = Led(0),
Amber = Led(1),
Blue = Led(2),
Red = Led(3),
};
}
private static IoStatus ParseIoStatus(this Data data, String installation, String batteryId)
{
var ioStatusBitmap = data.ParseUInt16Register(1013);
Boolean IoStatus(Int32 b) => (ioStatusBitmap >> b & 1) > 0;
return new IoStatus
{
Installation = installation,
BatteryId = batteryId,
MainSwitchClosed = IoStatus(0),
AlarmOutActive = IoStatus(1),
InternalFanActive = IoStatus(2),
VoltMeasurementAllowed = IoStatus(3),
AuxRelay = IoStatus(4),
RemoteState = IoStatus(5),
HeatingOn = IoStatus(6),
};
}
private static Warnings ParseWarnings(this Data data, String installation, String batteryId)
{
var warningsBitmap = data.ParseUInt64Register(1005);
Boolean Warning(Int32 b) => (warningsBitmap >> b & 1ul) > 0;
return new Warnings
{
Installation = installation,
BatteryId = batteryId,
TaM1 = Warning(1),
TbM1 = Warning(4),
VBm1 = Warning(6),
VBM1 = Warning(8),
IDM1 = Warning(10),
vsM1 = Warning(24),
iCM1 = Warning(26),
iDM1 = Warning(28),
MID1 = Warning(30),
BLPW = Warning(32),
Ah_W = Warning(35),
MPMM = Warning(38),
TCMM = Warning(39),
TCdi = Warning(40),
LMPW = Warning(44)
};
}
private static Alarms ParseAlarms(this Data data, String installation, String batteryId)
{
var alarmsBitmap = data.ParseUInt64Register(1009);
Boolean Alarm(Int32 b) => (alarmsBitmap >> b & 1ul) > 0;
return new Alarms
{
Installation = installation,
BatteryId = batteryId,
Tam = Alarm(0),
TaM2 = Alarm(2),
Tbm = Alarm(3),
TbM2 = Alarm(5),
VBm2 = Alarm(7),
VBM2 = Alarm(9),
IDM2 = Alarm(11),
ISOB = Alarm(12),
MSWE = Alarm(13),
FUSE = Alarm(14),
HTRE = Alarm(15),
TCPE = Alarm(16),
CME = Alarm(18),
HWFL = Alarm(19),
HWEM = Alarm(20),
ThM = Alarm(21),
vsm1 = Alarm(22),
vsm2 = Alarm(23),
vsM2 = Alarm(25),
iCM2 = Alarm(27),
iDM2 = Alarm(29),
MID2 = Alarm(31),
CCBF = Alarm(33),
AhFL = Alarm(34),
TbCM = Alarm(36),
HTFS = Alarm(42),
DATA = Alarm(43),
LMPA = Alarm(45),
HEBT = Alarm(46),
};
}
private static BatteryStatus ParseBatteryStatus(this Data data,
String installation,
String batteryId,
Decimal temperature,
Warnings warnings,
Alarms alarms,
DateTime lastSeen,
IPEndPoint endPoint)
{
var activeWarnings = Active(warnings);
var activeAlarms = Active(alarms);
return new BatteryStatus
{
InstallationName = installation,
BatteryId = batteryId,
HardwareVersion = data.ParseString(3),
FirmwareVersion = data.ParseString(4),
BmsVersion = data.ParseString(5),
AmpereHours = data.ParseUInt16(6),
Soc = data.ParseDecimalRegister(1053, 0.1m),
Voltage = data.ParseDecimalRegister(999, 0.01m),
Current = data.ParseDecimalRegister(1000, 0.01m, -10000m),
BusVoltage = data.ParseDecimalRegister(1001, 0.01m),
Temperature = temperature,
RtcCounter = data.ParseUInt32Register(1050),
IpAddress = endPoint.Address.ToString(),
Port = endPoint.Port,
// stuff below really should be done by Grafana/InfluxDb, but not possible (yet)
// aka hacks to get around limitations of Grafana/InfluxDb
NumberOfWarnings = activeWarnings.Count,
NumberOfAlarms = activeAlarms.Count,
WarningsBitmap = data.ParseUInt64Register(1005),
AlarmsBitmap = data.ParseUInt64Register(1009),
LastSeen = lastSeen.ToInfluxTime()
};
static IReadOnlyCollection<String> Active(BatteryRecord record) => record
.GetFields()
.Where(f => f.value is Boolean b && b)
.Select(f => f.key)
.ToList();
}
private static Temperatures ParseTemperatures(this Data data, String installation, String batteryId)
{
return new Temperatures
{
Installation = installation,
BatteryId = batteryId,
Battery = data.ParseDecimalRegister(1003, 0.1m, -400m),
Board = data.ParseDecimalRegister(1014, 0.1m, -400m),
Center = data.ParseDecimalRegister(1015, 0.1m, -400m),
Lateral1 = data.ParseDecimalRegister(1016, 0.1m, -400m),
Lateral2 = data.ParseDecimalRegister(1017, 0.1m, -400m),
CenterHeaterPwm = data.ParseDecimalRegister(1018, 0.1m),
LateralHeaterPwm = data.ParseDecimalRegister(1019, 0.1m),
};
}
// private static LineProtocolPayload CreatePayload(params LineProtocolPoint[] points) =>
// CreatePayload((IEnumerable<LineProtocolPoint>) points);
//
//
// private static LineProtocolPayload CreatePayload(IEnumerable<LineProtocolPoint> points)
// {
// var payload = new LineProtocolPayload();
//
// foreach (var point in points)
// payload.Add(point);
//
// return payload;
// }
private static String CheckProtocolId(String ascii)
{
var protocolId = ascii.Substring(0, Settings.ProtocolV3.Length);
if (protocolId != Settings.ProtocolV3)
throw new Exception($"Wrong protocol header: Expected '{Settings.ProtocolV3}' but got '{protocolId}'");
return protocolId;
}
private static IReadOnlyList<BatteryRecord> ParseBatteryRecords(Data data,
String installation,
DateTime lastSeen,
IPEndPoint endPoint)
{
var batteryId = data.ParseBatteryId();
var warnings = data.ParseWarnings (installation, batteryId);
var alarms = data.ParseAlarms (installation, batteryId);
var leds = data.ParseLeds (installation, batteryId);
var temperatures = data.ParseTemperatures (installation, batteryId);
var ioStatus = data.ParseIoStatus (installation, batteryId);
var batteryStatus = data.ParseBatteryStatus(installation,
batteryId,
temperatures.Battery,
warnings,
alarms,
lastSeen,
endPoint);
return new BatteryRecord[]
{
batteryStatus,
temperatures,
leds,
ioStatus,
warnings,
alarms
};
}
private static String ParseProtocolVersion (this Data data) => data.ParseString(0);
private static String ParseInstallationName(this Data data) => data.ParseString(1);
private static String ParseBatteryId (this Data data) => data.ParseString(2);
}

View File

@ -0,0 +1,21 @@
using static System.AttributeTargets;
#nullable disable
namespace InnovEnergy.App.Collector.Influx;
[AttributeUsage(Property)]
public class FieldAttribute : Attribute
{
public FieldAttribute(Type type)
{
Type = type;
}
public FieldAttribute()
{
Type = null;
}
public Type Type { get; }
}

View File

@ -0,0 +1,92 @@
using System.Text;
using InnovEnergy.App.Collector.Utils;
using InnovEnergy.Lib.Utils;
using static System.Globalization.CultureInfo;
using static InnovEnergy.App.Collector.Influx.LineProtocolSyntax;
namespace InnovEnergy.App.Collector.Influx;
public static class InfluxRecord
{
public static IEnumerable<(String key, String value)> GetTags(this Object record)
{
return record
.GetProperties()
.Where(p => p.HasAttribute<TagAttribute>())
.Where(p => p.IsReadable)
.Select(p => (key: p.Name, value: ConvertTag(p)));
}
public static IEnumerable<(String key, Object? value)> GetFields(this Object record)
{
return record
.GetProperties()
.Where(p => p.HasAttribute<FieldAttribute>())
.Where(p => p.IsReadable)
.Select(p => (key: p.Name, value: ConvertField(p)));
}
private static Object? ConvertField(Property p)
{
var value = p.Get();
var type = p.GetAttributes<FieldAttribute>().Single().Type;
return type != null
? Convert.ChangeType(value, type, InvariantCulture)
: value;
}
private static String ConvertTag(Property p)
{
return p.Get()?.ToString()!;
}
public static String Serialize(this Object record)
{
var sb = new StringBuilder();
record.GetType() // Measurement Name, TODO: NameAttribute
.Name
.Apply(EscapeName)
.Apply(sb.Append);
var tags = record
.GetTags()
.Where(t => !String.IsNullOrEmpty(t.value))
.OrderBy(t => t.key);
foreach (var (key, value) in tags)
{
sb.Append(',');
sb.Append(EscapeName(key));
sb.Append('=');
sb.Append(EscapeName(value));
}
var fieldDelimiter = ' ';
foreach (var (key, value) in record.GetFields())
{
sb.Append(fieldDelimiter);
fieldDelimiter = ',';
sb.Append(EscapeName(key));
sb.Append('=');
sb.Append(FormatValue(value!));
}
// TODO: timestamp handling
// let the DB add the timestamp
// if (point.UtcTimestamp != null)
// {
// sb.Append(' ');
// sb.Append(FormatTimestamp(point.UtcTimestamp.Value));
// }
return sb.ToString();
}
}

View File

@ -0,0 +1,76 @@
using System.Diagnostics.CodeAnalysis;
using static System.Globalization.CultureInfo;
namespace InnovEnergy.App.Collector.Influx;
internal static class LineProtocolSyntax
{
private static readonly DateTime Origin = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
public static String EscapeName(String nameOrKey)
{
return nameOrKey
.Replace("=", "\\=")
.Replace(" ", "\\ ")
.Replace(",", "\\,");
}
[SuppressMessage("ReSharper", "RedundantVerbatimPrefix")]
public static String FormatValue(Object value)
{
return value switch
{
String @string => FormatString (@string),
Single @single => FormatFloat (@single),
Double @double => FormatFloat (@double),
Int32 @int32 => FormatInteger (@int32),
UInt32 @uInt32 => FormatInteger (@uInt32),
Int64 @int64 => FormatInteger (@int64),
UInt64 @uInt64 => FormatInteger (@uInt64),
Decimal @decimal => FormatFloat (@decimal),
Int16 @int16 => FormatInteger (@int16),
UInt16 @uInt16 => FormatInteger (@uInt16),
SByte @sByte => FormatInteger (@sByte),
Byte @byte => FormatInteger (@byte),
Boolean @boolean => FormatBoolean (@boolean),
TimeSpan @timeSpan => FormatTimespan(@timeSpan),
_ => FormatString (value.ToString())
};
}
private static String FormatInteger<T>(T i) where T: IFormattable
{
return FormatFloat(i) + "i";
}
private static String FormatFloat<T>(T f) where T: IFormattable
{
return f.ToString(format: null, InvariantCulture);
}
private static String FormatTimespan(TimeSpan timeSpan)
{
return timeSpan
.TotalMilliseconds
.ToString(InvariantCulture);
}
private static String FormatBoolean(Boolean b) => b ? "t" : "f";
private static String FormatString(Object? o)
{
var s = o?.ToString();
return s is null
? "<null>"
: "\"" + s.Replace("\"", "\\\"") + "\"";
}
public static String FormatTimestamp(DateTime utcTimestamp)
{
var t = utcTimestamp - Origin;
return (t.Ticks * 100L).ToString(InvariantCulture);
}
}

View File

@ -0,0 +1,8 @@
using static System.AttributeTargets;
namespace InnovEnergy.App.Collector.Influx;
[AttributeUsage(Property)]
public class TagAttribute : Attribute
{
}

View File

@ -0,0 +1,173 @@
using System.Net.Sockets;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Text.Json;
using InnovEnergy.App.Collector.Influx;
using InnovEnergy.App.Collector.Records;
using InnovEnergy.Lib.Utils;
using InnovEnergy.Lib.Utils.Net;
using InnovEnergy.Lib.WebServer;
using static System.Text.Encoding;
using static InnovEnergy.Lib.Utils.ExceptionHandling;
namespace InnovEnergy.App.Collector;
// dotnet publish Collector.csproj -c Release -r linux-x64 -p:PublishTrimmed=false -p:PublishSingleFile=true --self-contained true ; scp ./bin/Release/net6.0/linux-x64/publish/* ig@salidomo.innovenergy.ch:~/collector
internal record BatteryData
(
String Installation,
String Battery,
Double EnergyCapacity,
Double EnergyStored,
Double Power
);
internal static class Program
{
//private static readonly Logger Logger = new Logger(Settings.LoggingEndPoint);
private static UdpClient _incomingSocket = new UdpClient(Settings.IncomingEndPoint);
private static UdpClient _dbSocket = new UdpClient();
private static readonly Subject<BatteryData> Batteries = new Subject<BatteryData>();
private static readonly JsonSerializerOptions JsonOptions = new JsonSerializerOptions { WriteIndented = true };
public static void Main(String[] args)
{
Task.Run(ServeJsonStats);
while (true)
ProcessDatagram();
// ReSharper disable once FunctionNeverReturns
}
private static void ServeJsonStats()
{
var json = "";
Batteries.ObserveOn(TaskPoolScheduler.Default)
.Buffer(TimeSpan.FromSeconds(4))
.Select(b => b.GroupBy(d => d.Battery).Select(d => d.First()).ToList())
.Select(ToJson)
.Subscribe(j => json = j);
HttpResponse ServeRequest(HttpRequest httpRequest)
{
return new HttpResponse
{
Content = json.Apply(UTF8.GetBytes),
ContentType = ContentType.ApplicationJson,
Headers = new[] { new HttpHeader("Access-Control-Allow-Origin", "*") }
};
}
WebServer.ServeOnLocalHost(3333, ServeRequest);
}
private static String ToJson(IReadOnlyCollection<BatteryData> batteryData)
{
var nInstallations = batteryData.GroupBy(d => d.Installation).Count();
var nBatteries = batteryData.Count;
var energyStored = batteryData.Sum(d => d.EnergyStored).Apply(Math.Round);
var energyCapacity = batteryData.Sum(d => d.EnergyCapacity).Apply(Math.Round);
var chargingPower = batteryData.Where(d => d.Power > 0).Sum(d => d.Power / 1000).Apply(Math.Round);
var dischargingPower = batteryData.Where(d => d.Power < 0).Sum(d => -d.Power / 1000).Apply(Math.Round);
var json = new
{
nInstallations,
nBatteries,
energyStored_kWh = energyStored,
energyCapacity_kWh = energyCapacity,
chargingPower_kW = chargingPower,
dischargingPower_kW = dischargingPower,
};
return JsonSerializer.Serialize(json, JsonOptions);
// Console.WriteLine($"nInstallations : {nInstallations}");
// Console.WriteLine($"nBatteries : {nBatteries}");
// Console.WriteLine($"energyStored : {Math.Round(energyStored)} kWh");
// Console.WriteLine($"energyCapacity : {Math.Round(energyCapacity)} kWh");
// Console.WriteLine($"chargingPower : {Math.Round(chargingPower / 1000)} kW");
// Console.WriteLine($"dischargingPower: {Math.Round(dischargingPower/ 1000)} kW");
}
private static void ProcessDatagram()
{
ReadDatagram()
.ThenTry(ParseDatagram)
.ThenTry(SendToDb)
.OnErrorDo(Console.WriteLine);
}
private static Try<Byte[]> ParseDatagram(UdpDatagram datagram)
{
Byte[] Parse()
{
var batteryRecords = BatteryDataParser
.ParseDatagram(datagram);
if (batteryRecords.FirstOrDefault() is BatteryStatus bs)
{
var battery = bs.InstallationName + bs.BatteryId;
var capacity = 48.0/1000 * bs.AmpereHours;
var energyStored = (Double) bs.Soc / 100 * capacity;
var power = bs.Current * bs.Voltage;
var data = new BatteryData(bs.InstallationName,
battery,
capacity,
energyStored,
(Double)power);
Batteries.OnNext(data);
}
return batteryRecords
.Select(InfluxRecord.Serialize)
.JoinLines()
.Apply(UTF8.GetBytes);
}
return Try(Parse)
.OnErrorLog("ParseDatagram failed " + datagram.EndPoint.Address);
}
private static Try<UdpDatagram> ReadDatagram()
{
return Try(_incomingSocket.ReadDatagram)
.OnErrorLog("Failed to read from UDP socket")
.OnErrorDo(ResetIncomingSocket);
}
private static Try<Int32> SendToDb(Byte[] data)
{
Int32 Send() => _dbSocket.SendDatagram(data, Settings.DbEndPoint);
return Try(Send)
.OnErrorLog("SendToDb failed")
.OnErrorDo(ResetDbSocket);
}
private static void ResetDbSocket(Exception e)
{
_dbSocket.Dispose();
_dbSocket = new UdpClient();
}
private static void ResetIncomingSocket(Exception e)
{
_incomingSocket.Dispose();
_incomingSocket = new UdpClient(Settings.IncomingEndPoint);
}
}

View File

@ -0,0 +1,43 @@
using InnovEnergy.App.Collector.Influx;
// ReSharper disable IdentifierTypo
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable InconsistentNaming
namespace InnovEnergy.App.Collector.Records;
public class Alarms : BatteryRecord
{
[Tag] public required String Installation { get; init; }
[Tag] public required String BatteryId { get; init; }
[Field(typeof(Int32))] public required Boolean Tam { get; init; }
[Field(typeof(Int32))] public required Boolean TaM2 { get; init; }
[Field(typeof(Int32))] public required Boolean Tbm { get; init; }
[Field(typeof(Int32))] public required Boolean TbM2 { get; init; }
[Field(typeof(Int32))] public required Boolean VBm2 { get; init; }
[Field(typeof(Int32))] public required Boolean VBM2 { get; init; }
[Field(typeof(Int32))] public required Boolean IDM2 { get; init; }
[Field(typeof(Int32))] public required Boolean MSWE { get; init; }
[Field(typeof(Int32))] public required Boolean FUSE { get; init; }
[Field(typeof(Int32))] public required Boolean HTRE { get; init; }
[Field(typeof(Int32))] public required Boolean TCPE { get; init; }
[Field(typeof(Int32))] public required Boolean CME { get; init; }
[Field(typeof(Int32))] public required Boolean HWFL { get; init; }
[Field(typeof(Int32))] public required Boolean HWEM { get; init; }
[Field(typeof(Int32))] public required Boolean ThM { get; init; }
[Field(typeof(Int32))] public required Boolean vsm1 { get; init; }
[Field(typeof(Int32))] public required Boolean vsm2 { get; init; }
[Field(typeof(Int32))] public required Boolean vsM2 { get; init; }
[Field(typeof(Int32))] public required Boolean iCM2 { get; init; }
[Field(typeof(Int32))] public required Boolean iDM2 { get; init; }
[Field(typeof(Int32))] public required Boolean MID2 { get; init; }
[Field(typeof(Int32))] public required Boolean CCBF { get; init; }
[Field(typeof(Int32))] public required Boolean AhFL { get; init; }
[Field(typeof(Int32))] public required Boolean TbCM { get; init; }
[Field(typeof(Int32))] public required Boolean HTFS { get; init; }
[Field(typeof(Int32))] public required Boolean DATA { get; init; }
[Field(typeof(Int32))] public required Boolean ISOB { get; init; }
[Field(typeof(Int32))] public required Boolean LMPA { get; init; }
[Field(typeof(Int32))] public required Boolean HEBT { get; init; }
}

View File

@ -0,0 +1,4 @@
namespace InnovEnergy.App.Collector.Records;
public abstract class BatteryRecord
{}

View File

@ -0,0 +1,32 @@
using InnovEnergy.App.Collector.Influx;
namespace InnovEnergy.App.Collector.Records;
public class BatteryStatus : BatteryRecord
{
[Tag] public required String InstallationName { get; init; }
[Tag] public required String BatteryId { get; init; }
[Field] public required String HardwareVersion { get; init; }
[Field] public required String FirmwareVersion { get; init; }
[Field] public required String BmsVersion { get; init; }
[Field] public required UInt32 AmpereHours { get; init; }
[Field] public required UInt32 RtcCounter { get; init; }
[Field] public required Decimal Voltage { get; init; }
[Field] public required Decimal Current { get; init; }
[Field] public required Decimal BusVoltage { get; init; }
[Field] public required Decimal Soc { get; init; }
[Field] public required Decimal Temperature { get; init; }
[Field] public required Int32 NumberOfWarnings { get; init; }
[Field] public required Int32 NumberOfAlarms { get; init; }
[Field] public required UInt64 WarningsBitmap { get; init; }
[Field] public required UInt64 AlarmsBitmap { get; init; }
[Field] public required Int64 LastSeen { get; init; }
[Field] public required String IpAddress { get; init; }
[Field] public required Int32 Port { get; init; }
}

View File

@ -0,0 +1,18 @@
using InnovEnergy.App.Collector.Influx;
namespace InnovEnergy.App.Collector.Records;
public class IoStatus : BatteryRecord
{
[Tag] public required String Installation { get; init; }
[Tag] public required String BatteryId { get; init; }
[Field(typeof(Int32))] public required Boolean MainSwitchClosed { get; init; }
[Field(typeof(Int32))] public required Boolean AlarmOutActive { get; init; }
[Field(typeof(Int32))] public required Boolean InternalFanActive { get; init; }
[Field(typeof(Int32))] public required Boolean VoltMeasurementAllowed { get; init; }
[Field(typeof(Int32))] public required Boolean AuxRelay { get; init; }
[Field(typeof(Int32))] public required Boolean RemoteState { get; init; }
[Field(typeof(Int32))] public required Boolean HeatingOn { get; init; }
}

View File

@ -0,0 +1,26 @@
using InnovEnergy.App.Collector.Influx;
// ReSharper disable UnusedAutoPropertyAccessor.Global
// ReSharper disable UnusedMember.Global
namespace InnovEnergy.App.Collector.Records;
public class Leds : BatteryRecord
{
[Tag] public required String Installation { get; init; }
[Tag] public required String BatteryId { get; init; }
[Field] public required LedState Green { get; set; }
[Field] public required LedState Amber { get; set; }
[Field] public required LedState Blue { get; set; }
[Field] public required LedState Red { get; set; }
}
public enum LedState : byte
{
Off = 0b00,
On = 0b01,
BlinkingSlow = 0b10,
BlinkingFast = 0b11,
}

View File

@ -0,0 +1,21 @@
using InnovEnergy.App.Collector.Influx;
namespace InnovEnergy.App.Collector.Records;
#pragma warning disable CS8618
public class Temperatures : BatteryRecord
{
[Tag] public String Installation { get; set; }
[Tag] public String BatteryId { get; set; }
[Field] public Decimal Battery { get; init; }
[Field] public Decimal Board { get; init; }
[Field] public Decimal Center { get; init; }
[Field] public Decimal Lateral1 { get; init; }
[Field] public Decimal Lateral2 { get; init; }
[Field] public Decimal CenterHeaterPwm { get; init; }
[Field] public Decimal LateralHeaterPwm { get; init; }
}

Some files were not shown because too many files have changed in this diff Show More