Merge remote-tracking branch 'origin/main'
This commit is contained in:
commit
b0117e5148
|
@ -75,14 +75,14 @@ public static class SessionMethods
|
|||
|
||||
await Task.Run(() =>
|
||||
{
|
||||
Process process = new Process();
|
||||
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();
|
||||
string output = process.StandardOutput.ReadToEnd();
|
||||
var output = process.StandardOutput.ReadToEnd();
|
||||
process.WaitForExit();
|
||||
Console.WriteLine(output);
|
||||
});
|
||||
|
@ -143,7 +143,7 @@ public static class SessionMethods
|
|||
|
||||
|
||||
//Salimax installation
|
||||
if (installation.Product==0)
|
||||
if (installation.Product == 0)
|
||||
{
|
||||
return user is not null
|
||||
&& user.UserType != 0
|
||||
|
@ -156,7 +156,7 @@ public static class SessionMethods
|
|||
|
||||
}
|
||||
|
||||
if (installation.Product==1)
|
||||
if (installation.Product == 1)
|
||||
{
|
||||
return user is not null
|
||||
&& user.UserType != 0
|
||||
|
@ -175,7 +175,7 @@ public static class SessionMethods
|
|||
|
||||
var original = Db.GetInstallationById(installation?.Id);
|
||||
//Salimax installation
|
||||
if (installation.Product==0)
|
||||
if (installation.Product == 0)
|
||||
{
|
||||
|
||||
return user is not null
|
||||
|
|
|
@ -227,8 +227,8 @@ public static class Aggregator
|
|||
Console.WriteLine($"Max SOC: {aggregatedData.MaxSoc}");
|
||||
Console.WriteLine($"Min SOC: {aggregatedData.MinSoc}");
|
||||
|
||||
Console.WriteLine($"ChargingBatteryPower: {aggregatedData.DischargingBatteryPower}");
|
||||
Console.WriteLine($"DischargingBatteryBattery: {aggregatedData.ChargingBatteryPower}");
|
||||
Console.WriteLine($"DischargingBatteryBattery: {aggregatedData.DischargingBatteryPower}");
|
||||
Console.WriteLine($"ChargingBatteryPower: {aggregatedData.ChargingBatteryPower}");
|
||||
|
||||
Console.WriteLine($"SumGridExportPower: {aggregatedData.GridExportPower}");
|
||||
Console.WriteLine($"SumGridImportPower: {aggregatedData.GridImportPower}");
|
||||
|
|
|
@ -679,34 +679,35 @@ internal static class Program
|
|||
// This is temporary for Wittman, but now it's for all Instalattion
|
||||
await File.WriteAllTextAsync("/var/www/html/status.csv", csv.SplitLines().Where(l => !l.Contains("Secret")).JoinLines());
|
||||
|
||||
var response = await request.PutAsync(new StringContent(csv));
|
||||
|
||||
// Compress CSV data to a byte array
|
||||
byte[] compressedBytes;
|
||||
using (var memoryStream = new MemoryStream())
|
||||
{
|
||||
//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
|
||||
using (var entryStream = entry.Open())
|
||||
using (var writer = new StreamWriter(entryStream))
|
||||
{
|
||||
writer.Write(csv);
|
||||
}
|
||||
}
|
||||
|
||||
compressedBytes = memoryStream.ToArray();
|
||||
}
|
||||
|
||||
// Encode the compressed byte array as a Base64 string
|
||||
string base64String = Convert.ToBase64String(compressedBytes);
|
||||
|
||||
// Create StringContent from Base64 string
|
||||
var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64");
|
||||
|
||||
// Upload the compressed data (ZIP archive) to S3
|
||||
var response = await request.PutAsync(stringContent);
|
||||
|
||||
// byte[] compressedBytes;
|
||||
// using (var memoryStream = new MemoryStream())
|
||||
// {
|
||||
// //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
|
||||
// using (var entryStream = entry.Open())
|
||||
// using (var writer = new StreamWriter(entryStream))
|
||||
// {
|
||||
// writer.Write(csv);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// compressedBytes = memoryStream.ToArray();
|
||||
// }
|
||||
//
|
||||
// // Encode the compressed byte array as a Base64 string
|
||||
// string base64String = Convert.ToBase64String(compressedBytes);
|
||||
//
|
||||
// // Create StringContent from Base64 string
|
||||
// var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64");
|
||||
//
|
||||
// // Upload the compressed data (ZIP archive) to S3
|
||||
// var response = await request.PutAsync(stringContent);
|
||||
//
|
||||
if (response.StatusCode != 200)
|
||||
{
|
||||
Console.WriteLine("ERROR: PUT");
|
||||
|
|
|
@ -70,7 +70,8 @@ public record S3Config
|
|||
// CanonicalizedResource;
|
||||
|
||||
|
||||
contentType = "application/base64; charset=utf-8";
|
||||
//contentType = "application/base64; charset=utf-8";
|
||||
contentType = "text/plain; charset=utf-8";
|
||||
|
||||
var payload = $"{method}\n{md5Hash}\n{contentType}\n{date}\n/{bucket.Trim('/')}/{s3Path.Trim('/')}";
|
||||
using var hmacSha1 = new HMACSHA1(UTF8.GetBytes(s3Secret));
|
||||
|
|
|
@ -77,12 +77,13 @@ public class AmptDevices
|
|||
Current = busCurrent
|
||||
};
|
||||
|
||||
// flatten the 2 strings of each SO into one array
|
||||
var strings = soStati.SelectMany(GetStrings).ToArray(nStrings);
|
||||
// flatten the output strings of each SO into one array
|
||||
var strings = soStati.SelectMany(GetDc).ToArray(nStrings);
|
||||
|
||||
return new AmptStatus
|
||||
{
|
||||
Dc = dc,
|
||||
NbrOfStrings = nStringOptimizers,
|
||||
Strings = strings,
|
||||
DcWh = dailyOutputEnergy
|
||||
};
|
||||
|
@ -106,6 +107,16 @@ public class AmptDevices
|
|||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<DcBus> GetDc(StringOptimizerRegisters r)
|
||||
{
|
||||
// hardcoded: every SO has 2 strings (produced like this by AMPT)
|
||||
|
||||
yield return new()
|
||||
{
|
||||
Voltage = r.Voltage,
|
||||
Current = r.Current,
|
||||
};
|
||||
}
|
||||
|
||||
private static IEnumerable<ModbusDevice<StringOptimizerRegisters>> StringOptimizers(ModbusClient modbusClient)
|
||||
{
|
||||
|
|
|
@ -6,6 +6,7 @@ namespace InnovEnergy.Lib.Devices.AMPT;
|
|||
public class AmptStatus : IMppt
|
||||
{
|
||||
public required DcBus Dc { get; init; }
|
||||
public required UInt16 NbrOfStrings { get; init; }
|
||||
public required IReadOnlyList<DcBus> Strings { get; init; }
|
||||
public required Double DcWh { get; init; } //Daily integrated string output energy in Wh
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/*using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
|
@ -49,83 +49,4 @@ public class Iem3KGridMeterDevice: ModbusDevice<Iem3KGridMeterRegisters>
|
|||
}
|
||||
|
||||
|
||||
}*/
|
||||
|
||||
using InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||
using InnovEnergy.Lib.Protocols.Modbus.Clients;
|
||||
using InnovEnergy.Lib.Protocols.Modbus.Slaves;
|
||||
using InnovEnergy.Lib.Utils;
|
||||
using System;
|
||||
|
||||
namespace InnovEnergy.Lib.Devices.IEM3kGridMeter
|
||||
{
|
||||
public class Iem3KGridMeterDevice : ModbusDevice<Iem3KGridMeterRegisters>
|
||||
{
|
||||
private readonly string _hostname;
|
||||
private readonly ushort _port;
|
||||
private readonly byte _slaveId;
|
||||
|
||||
public Iem3KGridMeterDevice(string hostname, ushort port = 502, byte slaveId = 1)
|
||||
: this(new TcpChannel(hostname, port), slaveId)
|
||||
{
|
||||
_hostname = hostname ?? throw new ArgumentNullException(nameof(hostname));
|
||||
_port = port;
|
||||
_slaveId = slaveId;
|
||||
}
|
||||
|
||||
private Iem3KGridMeterDevice(TcpChannel channel, byte slaveId = 1)
|
||||
: base(new ModbusTcpClient(channel, slaveId))
|
||||
{
|
||||
_hostname = channel.Host;
|
||||
_port = channel.Port;
|
||||
_slaveId = slaveId;
|
||||
Console.WriteLine($"Initializing Iem3KGridMeterDevice with channel: {channel.Host}:{channel.Port}");
|
||||
}
|
||||
|
||||
public Iem3KGridMeterDevice(ModbusClient client)
|
||||
: base(client)
|
||||
{
|
||||
if (client is ModbusTcpClient tcpClient)
|
||||
{
|
||||
_hostname = tcpClient.Channel.Host;
|
||||
_port = tcpClient.Channel.Port;
|
||||
_slaveId = tcpClient.SlaveId;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new ArgumentException("Invalid client type", nameof(client));
|
||||
}
|
||||
Console.WriteLine("Initializing Iem3KGridMeterDevice with ModbusClient");
|
||||
}
|
||||
|
||||
public new Iem3KGridMeterRegisters? Read()
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.WriteLine($"Attempting to read data from {_hostname}:{_port} with slaveId {_slaveId}");
|
||||
return base.Read();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to read data from {nameof(Iem3KGridMeterDevice)}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public new void Write(Iem3KGridMeterRegisters registers)
|
||||
{
|
||||
try
|
||||
{
|
||||
base.Write(registers);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"Failed to write data to {nameof(Iem3KGridMeterDevice)}: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ using System.Net.Sockets;
|
|||
using InnovEnergy.Lib.Protocols.Modbus.Protocol;
|
||||
using InnovEnergy.Lib.Utils.Net;
|
||||
|
||||
/*namespace InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||
namespace InnovEnergy.Lib.Protocols.Modbus.Channels;
|
||||
|
||||
public class TcpChannel : ConnectionChannel<TcpClient>
|
||||
{
|
||||
|
@ -82,104 +82,4 @@ public class TcpChannel : ConnectionChannel<TcpClient>
|
|||
var array = data.ToArray();
|
||||
tcpClient.GetStream().Write(array, 0, array.Length);
|
||||
}
|
||||
}*/
|
||||
|
||||
using System;
|
||||
using System.Net.Sockets;
|
||||
|
||||
namespace InnovEnergy.Lib.Protocols.Modbus.Channels
|
||||
{
|
||||
public class TcpChannel : Channel, IDisposable
|
||||
{
|
||||
public string Host { get; }
|
||||
public ushort Port { get; }
|
||||
|
||||
private const int TimeoutMs = 500; // TODO: parametrize
|
||||
private Socket? Socket { get; set; }
|
||||
private byte[] Buffer { get; }
|
||||
|
||||
public TcpChannel(string hostname, ushort port)
|
||||
{
|
||||
Host = hostname ?? throw new ArgumentNullException(nameof(hostname));
|
||||
Port = port;
|
||||
Buffer = new byte[8192]; // Buffer size can be adjusted
|
||||
}
|
||||
|
||||
public override IReadOnlyList<byte> Read(int nBytes)
|
||||
{
|
||||
if (Socket == null)
|
||||
throw new InvalidOperationException("Socket is not connected.");
|
||||
|
||||
var buffer = new byte[nBytes];
|
||||
int bytesRead = 0;
|
||||
|
||||
while (bytesRead < nBytes)
|
||||
{
|
||||
var read = Socket.Receive(buffer, bytesRead, nBytes - bytesRead, SocketFlags.None);
|
||||
if (read == 0)
|
||||
throw new Exception("Socket closed.");
|
||||
|
||||
bytesRead += read;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public override void Write(IReadOnlyList<byte> bytes)
|
||||
{
|
||||
if (Socket == null)
|
||||
throw new InvalidOperationException("Socket is not connected.");
|
||||
|
||||
Socket.Send(bytes.ToArray(), SocketFlags.None);
|
||||
}
|
||||
|
||||
public void Connect()
|
||||
{
|
||||
if (Socket != null)
|
||||
return;
|
||||
|
||||
Socket = new Socket(SocketType.Stream, ProtocolType.Tcp)
|
||||
{
|
||||
Blocking = true,
|
||||
NoDelay = true,
|
||||
LingerState = new LingerOption(false, 0),
|
||||
ReceiveTimeout = TimeoutMs,
|
||||
SendTimeout = TimeoutMs
|
||||
};
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
cts.CancelAfter(TimeoutMs);
|
||||
|
||||
try
|
||||
{
|
||||
Socket.ConnectAsync(Host, Port).Wait(TimeoutMs);
|
||||
}
|
||||
catch
|
||||
{
|
||||
Socket = null;
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public void Disconnect()
|
||||
{
|
||||
if (Socket == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Socket.Close();
|
||||
}
|
||||
finally
|
||||
{
|
||||
Socket = null;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
Disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace InnovEnergy.Lib.Protocols.Modbus.Clients;
|
|||
using UInt16s = IReadOnlyCollection<UInt16>;
|
||||
using Booleans = IReadOnlyCollection<Boolean>;
|
||||
|
||||
/*public class ModbusTcpClient : ModbusClient
|
||||
public class ModbusTcpClient : ModbusClient
|
||||
{
|
||||
public const UInt16 DefaultPort = 502;
|
||||
private UInt16 _Id;
|
||||
|
@ -184,171 +184,4 @@ using Booleans = IReadOnlyCollection<Boolean>;
|
|||
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
||||
}
|
||||
|
||||
}*/
|
||||
|
||||
public class ModbusTcpClient : ModbusClient
|
||||
{
|
||||
public const ushort DefaultPort = 502;
|
||||
private ushort _Id;
|
||||
public TcpChannel Channel { get; }
|
||||
|
||||
public ModbusTcpClient(TcpChannel channel, byte slaveId) : base(channel, slaveId)
|
||||
{
|
||||
Channel = channel;
|
||||
Channel.Connect();
|
||||
}
|
||||
|
||||
private ushort NextId() => unchecked(++_Id);
|
||||
|
||||
public override MbData ReadCoils(ushort readAddress, ushort nValues)
|
||||
{
|
||||
var id = NextId(); // TODO: check response id
|
||||
|
||||
var cmd = new ReadCoilsCommandFrame(SlaveId, readAddress, nValues);
|
||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||
|
||||
Channel.Write(frm.Data);
|
||||
|
||||
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
||||
var rxHdr = new MbapHeader(hData);
|
||||
|
||||
var rxFrm = Channel
|
||||
.Read(rxHdr.FrameLength)
|
||||
.ToArray()
|
||||
.Apply(ReadCoilsResponseFrame.Parse)
|
||||
.Apply(cmd.VerifyResponse);
|
||||
|
||||
return new MbData(rxFrm.Coils.RawData, readAddress, Endian);
|
||||
}
|
||||
|
||||
public override MbData ReadDiscreteInputs(ushort readAddress, ushort nValues)
|
||||
{
|
||||
var id = NextId(); // TODO: check response id
|
||||
|
||||
var cmd = new ReadDiscreteInputsCommandFrame(SlaveId, readAddress, nValues);
|
||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||
|
||||
Channel.Write(frm.Data);
|
||||
|
||||
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
||||
var rxHdr = new MbapHeader(hData);
|
||||
|
||||
var rxFrm = Channel
|
||||
.Read(rxHdr.FrameLength)
|
||||
.ToArray()
|
||||
.Apply(ReadDiscreteInputsResponseFrame.Parse)
|
||||
.Apply(cmd.VerifyResponse);
|
||||
|
||||
return new MbData(rxFrm.Inputs.RawData, readAddress, Endian);
|
||||
}
|
||||
|
||||
public override MbData ReadInputRegisters(ushort readAddress, ushort nValues)
|
||||
{
|
||||
var id = NextId(); // TODO: check response id
|
||||
|
||||
var cmd = new ReadInputRegistersCommandFrame(SlaveId, readAddress, nValues);
|
||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||
|
||||
Channel.Write(frm.Data);
|
||||
|
||||
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
||||
var rxHdr = new MbapHeader(hData);
|
||||
|
||||
var rxFrm = Channel
|
||||
.Read(rxHdr.FrameLength)
|
||||
.ToArray()
|
||||
.Apply(ReadInputRegistersResponseFrame.Parse)
|
||||
.Apply(cmd.VerifyResponse);
|
||||
|
||||
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
||||
}
|
||||
|
||||
public override MbData ReadHoldingRegisters(ushort readAddress, ushort nValues)
|
||||
{
|
||||
var id = NextId(); // TODO: check response id
|
||||
|
||||
var cmd = new ReadHoldingRegistersCommandFrame(SlaveId, readAddress, nValues);
|
||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||
|
||||
Channel.Write(frm.Data);
|
||||
|
||||
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
||||
var rxHdr = new MbapHeader(hData);
|
||||
|
||||
var rxFrm = Channel
|
||||
.Read(rxHdr.FrameLength)
|
||||
.ToArray()
|
||||
.Apply(ReadHoldingRegistersResponseFrame.Parse)
|
||||
.Apply(cmd.VerifyResponse);
|
||||
|
||||
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
||||
}
|
||||
|
||||
public override ushort WriteCoils(ushort writeAddress, Booleans coils)
|
||||
{
|
||||
var id = NextId(); // TODO: check response id
|
||||
var cmd = new WriteCoilsCommandFrame(SlaveId, writeAddress, coils);
|
||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||
|
||||
Channel.Write(frm.Data);
|
||||
|
||||
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
||||
var rxHdr = new MbapHeader(hData);
|
||||
|
||||
var rxFrm = Channel
|
||||
.Read(rxHdr.FrameLength)
|
||||
.ToArray()
|
||||
.Apply(WriteCoilsResponseFrame.Parse)
|
||||
.Apply(cmd.VerifyResponse);
|
||||
|
||||
return rxFrm.NbWritten;
|
||||
}
|
||||
|
||||
public override ushort WriteRegisters(ushort writeAddress, UInt16s values)
|
||||
{
|
||||
var id = NextId(); // TODO: check response id
|
||||
var cmd = new WriteRegistersCommandFrame(SlaveId, writeAddress, values);
|
||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||
|
||||
Channel.Write(frm.Data);
|
||||
|
||||
var hData = Channel.Read(MbapHeader.Size).ToArray();
|
||||
var rxHdr = new MbapHeader(hData);
|
||||
|
||||
var rxFrm = Channel
|
||||
.Read(rxHdr.FrameLength)
|
||||
.ToArray()
|
||||
.Apply(WriteRegistersResponseFrame.Parse)
|
||||
.Apply(cmd.VerifyResponse);
|
||||
|
||||
return rxFrm.NbWritten;
|
||||
}
|
||||
|
||||
public override MbData ReadWriteRegisters(ushort readAddress, ushort nbToRead, ushort writeAddress, UInt16s registersToWrite)
|
||||
{
|
||||
var id = NextId(); // TODO: check response id
|
||||
|
||||
var cmd = new ReadWriteRegistersCommandFrame(SlaveId, readAddress, nbToRead, writeAddress, registersToWrite);
|
||||
|
||||
var hdr = new MbapHeader(id, cmd.Data.Count);
|
||||
var frm = new ModbusTcpFrame(hdr, cmd);
|
||||
|
||||
Channel.Write(frm.Data);
|
||||
|
||||
var hData = Enumerable.ToArray(Channel.Read(MbapHeader.Size));
|
||||
var rxHdr = new MbapHeader(hData);
|
||||
|
||||
var fData = Enumerable.ToArray(Channel.Read(rxHdr.FrameLength));
|
||||
var rxFrm = fData
|
||||
.Apply(ReadWriteRegistersResponseFrame.Parse)
|
||||
.Apply(cmd.VerifyResponse);
|
||||
|
||||
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian);
|
||||
}
|
||||
}
|
|
@ -11,10 +11,12 @@
|
|||
"overview": "overview",
|
||||
"manage": "manage",
|
||||
"batteryview": "batteryview",
|
||||
"pvview": "pvview",
|
||||
"log": "log",
|
||||
"live": "live",
|
||||
"information": "information",
|
||||
"configuration": "configuration",
|
||||
"history": "history",
|
||||
"mainstats": "mainstats",
|
||||
"detailed_view": "detailed_view/"
|
||||
}
|
||||
|
|
|
@ -246,8 +246,8 @@ function MainStats(props: MainStatsProps) {
|
|||
chartOverview: BatteryOverviewInterface;
|
||||
}> = transformInputToBatteryViewData(
|
||||
props.s3Credentials,
|
||||
UnixTime.fromTicks(startX),
|
||||
UnixTime.fromTicks(endX)
|
||||
UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)),
|
||||
UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2))
|
||||
);
|
||||
|
||||
resultPromise
|
||||
|
|
|
@ -15,7 +15,7 @@ import {
|
|||
Typography,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import React, { useState } from 'react';
|
||||
import React, { useContext, useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import Button from '@mui/material/Button';
|
||||
import { Close as CloseIcon } from '@mui/icons-material';
|
||||
|
@ -29,6 +29,8 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
|
|||
import dayjs from 'dayjs';
|
||||
import axiosConfig from '../../../Resources/axiosConfig';
|
||||
import utc from 'dayjs/plugin/utc';
|
||||
import { Action } from '../../../interfaces/S3Types';
|
||||
import { UserContext } from '../../../contexts/userContext';
|
||||
|
||||
interface ConfigurationProps {
|
||||
values: TopologyValues;
|
||||
|
@ -82,6 +84,8 @@ function Configuration(props: ConfigurationProps) {
|
|||
const [updated, setUpdated] = useState(false);
|
||||
const [dateSelectionError, setDateSelectionError] = useState('');
|
||||
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
|
||||
const context = useContext(UserContext);
|
||||
const { currentUser, setUser } = context;
|
||||
|
||||
const [formValues, setFormValues] = useState<ConfigurationValues>({
|
||||
minimumSoC: props.values.minimumSoC[0].value,
|
||||
|
@ -133,6 +137,13 @@ function Configuration(props: ConfigurationProps) {
|
|||
.toDate()
|
||||
};
|
||||
|
||||
const historyAction: Action = {
|
||||
configuration: configurationToSend,
|
||||
date: new Date().toISOString().split('T')[0], // Gets the current date in YYYY-MM-DD format
|
||||
time: new Date().toISOString().split('T')[1].split('.')[0], // Gets the current time in HH:MM:SS format
|
||||
user: currentUser.name
|
||||
};
|
||||
|
||||
// console.log('will send ', dayjs(formValues.calibrationChargeDate));
|
||||
|
||||
setLoading(true);
|
||||
|
@ -149,10 +160,23 @@ function Configuration(props: ConfigurationProps) {
|
|||
});
|
||||
|
||||
if (res) {
|
||||
const historyRes = await axiosConfig
|
||||
.post(
|
||||
`/UpdateActionHistory?installationId=${props.id}`,
|
||||
historyAction
|
||||
)
|
||||
.catch((err) => {
|
||||
if (err.response) {
|
||||
setError(true);
|
||||
setLoading(false);
|
||||
}
|
||||
});
|
||||
if (historyRes) {
|
||||
setUpdated(true);
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const handleOkOnErrorDateModal = () => {
|
||||
|
|
|
@ -0,0 +1,275 @@
|
|||
import React, { useContext, useEffect, useState } from 'react';
|
||||
import {
|
||||
Alert,
|
||||
Card,
|
||||
Container,
|
||||
Divider,
|
||||
Grid,
|
||||
IconButton,
|
||||
useTheme
|
||||
} from '@mui/material';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import axiosConfig from '../../../Resources/axiosConfig';
|
||||
import { AxiosError, AxiosResponse } from 'axios/index';
|
||||
import routes from '../../../Resources/routes.json';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { TokenContext } from '../../../contexts/tokenContext';
|
||||
import { Action } from '../../../interfaces/S3Types';
|
||||
|
||||
interface HistoryProps {
|
||||
errorLoadingS3Data: boolean;
|
||||
id: number;
|
||||
}
|
||||
|
||||
function HistoryOfActions(props: HistoryProps) {
|
||||
const theme = useTheme();
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
|
||||
const [history, setHistory] = useState<Action[]>([]);
|
||||
const navigate = useNavigate();
|
||||
const tokencontext = useContext(TokenContext);
|
||||
const { removeToken } = tokencontext;
|
||||
|
||||
useEffect(() => {
|
||||
axiosConfig
|
||||
.get(`/GetHistoryForInstallation?id=${props.id}`)
|
||||
.then((res: AxiosResponse<Action[]>) => {
|
||||
setHistory(res.data);
|
||||
})
|
||||
.catch((err: AxiosError) => {
|
||||
if (err.response && err.response.status == 401) {
|
||||
removeToken();
|
||||
navigate(routes.login);
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Container maxWidth="xl">
|
||||
<Grid container>
|
||||
<Grid item xs={12} md={12}>
|
||||
{history.length > 0 && (
|
||||
<Card sx={{ marginTop: '10px' }}>
|
||||
<Divider />
|
||||
<div>
|
||||
<div
|
||||
style={{
|
||||
height: '40px',
|
||||
marginBottom: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="dimgrey"
|
||||
fontWeight="bold"
|
||||
fontSize="1rem"
|
||||
gutterBottom
|
||||
noWrap
|
||||
>
|
||||
<FormattedMessage id="user" defaultMessage="User" />
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="dimgrey"
|
||||
fontWeight="bold"
|
||||
fontSize="1rem"
|
||||
gutterBottom
|
||||
noWrap
|
||||
>
|
||||
<FormattedMessage id="date" defaultMessage="Date" />
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
color="dimgrey"
|
||||
fontWeight="bold"
|
||||
fontSize="1rem"
|
||||
gutterBottom
|
||||
noWrap
|
||||
>
|
||||
<FormattedMessage id="time" defaultMessage="Time" />
|
||||
</Typography>
|
||||
</div>
|
||||
{/*<div*/}
|
||||
{/* style={{*/}
|
||||
{/* flex: 1,*/}
|
||||
{/* marginTop: '15px',*/}
|
||||
{/* display: 'flex',*/}
|
||||
{/* alignItems: 'center',*/}
|
||||
{/* justifyContent: 'center'*/}
|
||||
{/* }}*/}
|
||||
{/*>*/}
|
||||
{/* <Typography*/}
|
||||
{/* variant="body1"*/}
|
||||
{/* color="dimgrey"*/}
|
||||
{/* fontWeight="bold"*/}
|
||||
{/* fontSize="1rem"*/}
|
||||
{/* gutterBottom*/}
|
||||
{/* noWrap*/}
|
||||
{/* >*/}
|
||||
{/* <FormattedMessage id="seen" defaultMessage="Seen" />*/}
|
||||
{/* </Typography>*/}
|
||||
{/*</div>*/}
|
||||
</div>
|
||||
<Divider />
|
||||
<div style={{ maxHeight: '400px', overflowY: 'auto' }}>
|
||||
{history.map((action, index) => (
|
||||
<>
|
||||
<Divider />
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
height: '40px',
|
||||
marginBottom: '10px',
|
||||
display: 'flex',
|
||||
alignItems: 'center'
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight="bold"
|
||||
color="text.primary"
|
||||
gutterBottom
|
||||
noWrap
|
||||
>
|
||||
{action.user}
|
||||
</Typography>
|
||||
</div>
|
||||
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight="bold"
|
||||
color="text.primary"
|
||||
gutterBottom
|
||||
noWrap
|
||||
>
|
||||
{action.date}
|
||||
</Typography>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
flex: 1,
|
||||
marginTop: '15px',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
}}
|
||||
>
|
||||
<Typography
|
||||
variant="body1"
|
||||
fontWeight="bold"
|
||||
color="text.primary"
|
||||
gutterBottom
|
||||
noWrap
|
||||
>
|
||||
{action.time}
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!props.errorLoadingS3Data && history.length == 0 && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: '20px'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="nohistory"
|
||||
defaultMessage="There is no history of actions"
|
||||
/>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ marginLeft: '4px' }}
|
||||
></IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
</Grid>
|
||||
</Grid>
|
||||
|
||||
<Grid item xs={12} md={12} style={{ marginBottom: '20px' }}>
|
||||
{props.errorLoadingS3Data && (
|
||||
<Alert
|
||||
severity="error"
|
||||
sx={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
marginTop: '20px'
|
||||
}}
|
||||
>
|
||||
<FormattedMessage
|
||||
id="cannotloadloggingdata"
|
||||
defaultMessage="Cannot load logging data"
|
||||
/>
|
||||
<IconButton
|
||||
color="inherit"
|
||||
size="small"
|
||||
sx={{ marginLeft: '4px' }}
|
||||
></IconButton>
|
||||
</Alert>
|
||||
)}
|
||||
</Grid>
|
||||
</Container>
|
||||
);
|
||||
}
|
||||
|
||||
export default HistoryOfActions;
|
|
@ -23,6 +23,7 @@ import routes from '../../../Resources/routes.json';
|
|||
import Information from '../Information/Information';
|
||||
import BatteryView from '../BatteryView/BatteryView';
|
||||
import { UserType } from '../../../interfaces/UserTypes';
|
||||
import HistoryOfActions from '../History/History';
|
||||
|
||||
interface singleInstallationProps {
|
||||
current_installation?: I_Installation;
|
||||
|
@ -309,6 +310,20 @@ function Installation(props: singleInstallationProps) {
|
|||
></BatteryView>
|
||||
}
|
||||
></Route>
|
||||
|
||||
<Route
|
||||
path={routes.pvview + '*'}
|
||||
element={
|
||||
<PvView
|
||||
values={values}
|
||||
s3Credentials={s3Credentials}
|
||||
installationId={props.current_installation.id}
|
||||
productNum={props.current_installation.product}
|
||||
connected={connected}
|
||||
></PvView>
|
||||
}
|
||||
></Route>
|
||||
|
||||
<Route
|
||||
path={routes.overview}
|
||||
element={<Overview s3Credentials={s3Credentials}></Overview>}
|
||||
|
@ -342,6 +357,18 @@ function Installation(props: singleInstallationProps) {
|
|||
}
|
||||
/>
|
||||
)}
|
||||
{currentUser.userType == UserType.admin && (
|
||||
<Route
|
||||
path={routes.history}
|
||||
element={
|
||||
<HistoryOfActions
|
||||
errorLoadingS3Data={errorLoadingS3Data}
|
||||
id={props.current_installation.id}
|
||||
></HistoryOfActions>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{currentUser.userType == UserType.admin && (
|
||||
<Route
|
||||
path={routes.manage}
|
||||
|
|
|
@ -26,7 +26,9 @@ function InstallationTabs() {
|
|||
'batteryview',
|
||||
'log',
|
||||
'information',
|
||||
'configuration'
|
||||
'configuration',
|
||||
'history',
|
||||
'pvview'
|
||||
];
|
||||
|
||||
const [currentTab, setCurrentTab] = useState<string>(undefined);
|
||||
|
@ -137,6 +139,19 @@ function InstallationTabs() {
|
|||
defaultMessage="Configuration"
|
||||
/>
|
||||
)
|
||||
},
|
||||
// {
|
||||
// value: 'history',
|
||||
// label: (
|
||||
// <FormattedMessage
|
||||
// id="history"
|
||||
// defaultMessage="History Of Actions"
|
||||
// />
|
||||
// )
|
||||
// },
|
||||
{
|
||||
value: 'pvview',
|
||||
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
|
||||
}
|
||||
]
|
||||
: currentUser.userType == UserType.partner
|
||||
|
@ -158,6 +173,10 @@ function InstallationTabs() {
|
|||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'pvview',
|
||||
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
|
||||
},
|
||||
|
||||
{
|
||||
value: 'information',
|
||||
|
@ -217,6 +236,10 @@ function InstallationTabs() {
|
|||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'pvview',
|
||||
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
|
||||
},
|
||||
{
|
||||
value: 'manage',
|
||||
label: (
|
||||
|
@ -249,6 +272,15 @@ function InstallationTabs() {
|
|||
/>
|
||||
)
|
||||
}
|
||||
// {
|
||||
// value: 'history',
|
||||
// label: (
|
||||
// <FormattedMessage
|
||||
// id="history"
|
||||
// defaultMessage="History Of Actions"
|
||||
// />
|
||||
// )
|
||||
// }
|
||||
]
|
||||
: currentUser.userType == UserType.partner
|
||||
? [
|
||||
|
@ -280,6 +312,10 @@ function InstallationTabs() {
|
|||
/>
|
||||
)
|
||||
},
|
||||
{
|
||||
value: 'pvview',
|
||||
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
|
||||
},
|
||||
|
||||
{
|
||||
value: 'information',
|
||||
|
|
|
@ -35,6 +35,8 @@ export type ConfigurationValues = {
|
|||
calibrationChargeDate: Date | null;
|
||||
};
|
||||
|
||||
export interface Pv {}
|
||||
|
||||
export interface Battery {
|
||||
BatteryId: number;
|
||||
FwVersion: I_BoxDataValue;
|
||||
|
@ -163,11 +165,19 @@ export type TopologyValues = {
|
|||
additionalCalibrationChargeDate: I_BoxDataValue[];
|
||||
|
||||
batteryView: Battery[];
|
||||
|
||||
pvView: Pv[];
|
||||
};
|
||||
type TopologyPaths = { [key in keyof TopologyValues]: string[] };
|
||||
|
||||
const batteryIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
|
||||
|
||||
const PvPaths = [
|
||||
'/PvOnDc/Strings/%id%/Voltage',
|
||||
'/PvOnDc/Strings/%id%/Current',
|
||||
'/PvOnDc/Strings/%id%/Power'
|
||||
];
|
||||
|
||||
const batteryPaths = [
|
||||
'/Battery/Devices/%id%/FwVersion',
|
||||
'/Battery/Devices/%id%/Dc/Power',
|
||||
|
@ -290,6 +300,10 @@ export const topologyPaths: TopologyPaths = {
|
|||
batteryPaths.map((path) => path.replace('%id%', id.toString()))
|
||||
),
|
||||
|
||||
pvView: batteryIds.flatMap((id) =>
|
||||
PvPaths.map((path) => path.replace('%id%', id.toString()))
|
||||
),
|
||||
|
||||
minimumSoC: ['/Config/MinSoc'],
|
||||
installedDcDcPower: ['/DcDc/SystemControl/NumberOfConnectedSlaves'],
|
||||
gridSetPoint: ['/Config/GridSetPoint'],
|
||||
|
|
|
@ -119,8 +119,8 @@ function Overview(props: OverviewProps) {
|
|||
chartOverview: overviewInterface;
|
||||
}> = transformInputToDailyData(
|
||||
props.s3Credentials,
|
||||
UnixTime.fromTicks(startX),
|
||||
UnixTime.fromTicks(endX)
|
||||
UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)),
|
||||
UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2))
|
||||
);
|
||||
|
||||
let isComponentMounted = true;
|
||||
|
|
|
@ -58,6 +58,28 @@ function Installation(props: singleInstallationProps) {
|
|||
|
||||
const s3Credentials = { s3Bucket, ...S3data };
|
||||
|
||||
const fetchDataOnlyOneTime = async () => {
|
||||
var timeperiodToSearch = 70;
|
||||
|
||||
for (var i = timeperiodToSearch; i > 0; i -= 2) {
|
||||
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
|
||||
|
||||
const res = await fetchData(now, s3Credentials);
|
||||
|
||||
if (res != FetchResult.notAvailable && res != FetchResult.tryLater) {
|
||||
setConnected(true);
|
||||
setFailedToCommunicateWithInstallation(0);
|
||||
setValues(
|
||||
extractValues({
|
||||
time: now,
|
||||
value: res
|
||||
})
|
||||
);
|
||||
return now;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const fetchDataPeriodically = async () => {
|
||||
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
|
||||
|
||||
|
@ -76,7 +98,7 @@ function Installation(props: singleInstallationProps) {
|
|||
return true;
|
||||
} else {
|
||||
setFailedToCommunicateWithInstallation((prevCount) => {
|
||||
if (prevCount + 1 >= 3) {
|
||||
if (prevCount + 1 >= 20) {
|
||||
setConnected(false);
|
||||
}
|
||||
return prevCount + 1;
|
||||
|
@ -87,19 +109,6 @@ function Installation(props: singleInstallationProps) {
|
|||
}
|
||||
};
|
||||
|
||||
const fetchDataOnlyOneTime = async () => {
|
||||
let success = false;
|
||||
const max_retransmissions = 3;
|
||||
|
||||
for (let i = 0; i < max_retransmissions; i++) {
|
||||
success = await fetchDataPeriodically();
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
if (success) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
let path = location.split('/');
|
||||
|
||||
|
@ -118,7 +127,7 @@ function Installation(props: singleInstallationProps) {
|
|||
currentTab == 'live' ||
|
||||
(location.includes('batteryview') && !location.includes('mainstats'))
|
||||
) {
|
||||
fetchDataPeriodically();
|
||||
fetchDataOnlyOneTime();
|
||||
interval = setInterval(fetchDataPeriodically, 2000);
|
||||
}
|
||||
if (currentTab == 'configuration' || location.includes('mainstats')) {
|
||||
|
|
|
@ -222,6 +222,7 @@ function Topology(props: TopologyProps) {
|
|||
}}
|
||||
bottomBox={{
|
||||
title: 'AC Loads',
|
||||
|
||||
data: props.values.islandBusToLoadOnIslandBusConnection,
|
||||
connected:
|
||||
props.values.loadOnIslandBusBox[0].value.toString() !=
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { ConfigurationValues } from '../content/dashboards/Log/graph.util';
|
||||
|
||||
export interface I_S3Credentials {
|
||||
s3Region: string;
|
||||
s3Provider: string;
|
||||
|
@ -16,3 +18,10 @@ export interface ErrorMessage {
|
|||
deviceCreatedTheMessage: string;
|
||||
seen: boolean;
|
||||
}
|
||||
|
||||
export interface Action {
|
||||
configuration: ConfigurationValues;
|
||||
date: string;
|
||||
time: string;
|
||||
user: string;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue