Merge remote-tracking branch 'origin/main'

This commit is contained in:
Yinyin Liu 2024-06-11 14:31:40 +02:00
commit b0117e5148
21 changed files with 480 additions and 415 deletions

View File

@ -75,14 +75,14 @@ public static class SessionMethods
await Task.Run(() => await Task.Run(() =>
{ {
Process process = new Process(); var process = new Process();
process.StartInfo.FileName = "/bin/bash"; process.StartInfo.FileName = "/bin/bash";
process.StartInfo.Arguments = $"{scriptPath} {vpnIp} {batteryNode} {version}"; process.StartInfo.Arguments = $"{scriptPath} {vpnIp} {batteryNode} {version}";
process.StartInfo.UseShellExecute = false; process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardOutput = true;
process.Start(); process.Start();
string output = process.StandardOutput.ReadToEnd(); var output = process.StandardOutput.ReadToEnd();
process.WaitForExit(); process.WaitForExit();
Console.WriteLine(output); Console.WriteLine(output);
}); });
@ -143,7 +143,7 @@ public static class SessionMethods
//Salimax installation //Salimax installation
if (installation.Product==0) if (installation.Product == 0)
{ {
return user is not null return user is not null
&& user.UserType != 0 && user.UserType != 0
@ -156,7 +156,7 @@ public static class SessionMethods
} }
if (installation.Product==1) if (installation.Product == 1)
{ {
return user is not null return user is not null
&& user.UserType != 0 && user.UserType != 0
@ -175,7 +175,7 @@ public static class SessionMethods
var original = Db.GetInstallationById(installation?.Id); var original = Db.GetInstallationById(installation?.Id);
//Salimax installation //Salimax installation
if (installation.Product==0) if (installation.Product == 0)
{ {
return user is not null return user is not null

View File

@ -227,8 +227,8 @@ public static class Aggregator
Console.WriteLine($"Max SOC: {aggregatedData.MaxSoc}"); Console.WriteLine($"Max SOC: {aggregatedData.MaxSoc}");
Console.WriteLine($"Min SOC: {aggregatedData.MinSoc}"); Console.WriteLine($"Min SOC: {aggregatedData.MinSoc}");
Console.WriteLine($"ChargingBatteryPower: {aggregatedData.DischargingBatteryPower}"); Console.WriteLine($"DischargingBatteryBattery: {aggregatedData.DischargingBatteryPower}");
Console.WriteLine($"DischargingBatteryBattery: {aggregatedData.ChargingBatteryPower}"); Console.WriteLine($"ChargingBatteryPower: {aggregatedData.ChargingBatteryPower}");
Console.WriteLine($"SumGridExportPower: {aggregatedData.GridExportPower}"); Console.WriteLine($"SumGridExportPower: {aggregatedData.GridExportPower}");
Console.WriteLine($"SumGridImportPower: {aggregatedData.GridImportPower}"); Console.WriteLine($"SumGridImportPower: {aggregatedData.GridImportPower}");

View File

@ -679,34 +679,35 @@ internal static class Program
// This is temporary for Wittman, but now it's for all Instalattion // 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()); 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 // Compress CSV data to a byte array
byte[] compressedBytes; // byte[] compressedBytes;
using (var memoryStream = new MemoryStream()) // using (var memoryStream = new MemoryStream())
{ // {
//Create a zip directory and put the compressed file inside // //Create a zip directory and put the compressed file inside
using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) // 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.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive
using (var entryStream = entry.Open()) // using (var entryStream = entry.Open())
using (var writer = new StreamWriter(entryStream)) // using (var writer = new StreamWriter(entryStream))
{ // {
writer.Write(csv); // writer.Write(csv);
} // }
} // }
//
compressedBytes = memoryStream.ToArray(); // compressedBytes = memoryStream.ToArray();
} // }
//
// Encode the compressed byte array as a Base64 string // // Encode the compressed byte array as a Base64 string
string base64String = Convert.ToBase64String(compressedBytes); // string base64String = Convert.ToBase64String(compressedBytes);
//
// Create StringContent from Base64 string // // Create StringContent from Base64 string
var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64"); // var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64");
//
// Upload the compressed data (ZIP archive) to S3 // // Upload the compressed data (ZIP archive) to S3
var response = await request.PutAsync(stringContent); // var response = await request.PutAsync(stringContent);
//
if (response.StatusCode != 200) if (response.StatusCode != 200)
{ {
Console.WriteLine("ERROR: PUT"); Console.WriteLine("ERROR: PUT");

View File

@ -70,7 +70,8 @@ public record S3Config
// CanonicalizedResource; // 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('/')}"; var payload = $"{method}\n{md5Hash}\n{contentType}\n{date}\n/{bucket.Trim('/')}/{s3Path.Trim('/')}";
using var hmacSha1 = new HMACSHA1(UTF8.GetBytes(s3Secret)); using var hmacSha1 = new HMACSHA1(UTF8.GetBytes(s3Secret));

View File

@ -77,12 +77,13 @@ public class AmptDevices
Current = busCurrent Current = busCurrent
}; };
// flatten the 2 strings of each SO into one array // flatten the output strings of each SO into one array
var strings = soStati.SelectMany(GetStrings).ToArray(nStrings); var strings = soStati.SelectMany(GetDc).ToArray(nStrings);
return new AmptStatus return new AmptStatus
{ {
Dc = dc, Dc = dc,
NbrOfStrings = nStringOptimizers,
Strings = strings, Strings = strings,
DcWh = dailyOutputEnergy 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) private static IEnumerable<ModbusDevice<StringOptimizerRegisters>> StringOptimizers(ModbusClient modbusClient)
{ {

View File

@ -6,6 +6,7 @@ namespace InnovEnergy.Lib.Devices.AMPT;
public class AmptStatus : IMppt public class AmptStatus : IMppt
{ {
public required DcBus Dc { get; init; } public required DcBus Dc { get; init; }
public required UInt16 NbrOfStrings { get; init; }
public required IReadOnlyList<DcBus> Strings { get; init; } public required IReadOnlyList<DcBus> Strings { get; init; }
public required Double DcWh { get; init; } //Daily integrated string output energy in Wh public required Double DcWh { get; init; } //Daily integrated string output energy in Wh
} }

View File

@ -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.Clients;
using InnovEnergy.Lib.Protocols.Modbus.Slaves; using InnovEnergy.Lib.Protocols.Modbus.Slaves;
using InnovEnergy.Lib.Utils; 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}");
}
}
}
} }

View File

@ -2,7 +2,7 @@ using System.Net.Sockets;
using InnovEnergy.Lib.Protocols.Modbus.Protocol; using InnovEnergy.Lib.Protocols.Modbus.Protocol;
using InnovEnergy.Lib.Utils.Net; using InnovEnergy.Lib.Utils.Net;
/*namespace InnovEnergy.Lib.Protocols.Modbus.Channels; namespace InnovEnergy.Lib.Protocols.Modbus.Channels;
public class TcpChannel : ConnectionChannel<TcpClient> public class TcpChannel : ConnectionChannel<TcpClient>
{ {
@ -82,104 +82,4 @@ public class TcpChannel : ConnectionChannel<TcpClient>
var array = data.ToArray(); var array = data.ToArray();
tcpClient.GetStream().Write(array, 0, array.Length); 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();
}
}
} }

View File

@ -12,7 +12,7 @@ namespace InnovEnergy.Lib.Protocols.Modbus.Clients;
using UInt16s = IReadOnlyCollection<UInt16>; using UInt16s = IReadOnlyCollection<UInt16>;
using Booleans = IReadOnlyCollection<Boolean>; using Booleans = IReadOnlyCollection<Boolean>;
/*public class ModbusTcpClient : ModbusClient public class ModbusTcpClient : ModbusClient
{ {
public const UInt16 DefaultPort = 502; public const UInt16 DefaultPort = 502;
private UInt16 _Id; private UInt16 _Id;
@ -184,171 +184,4 @@ using Booleans = IReadOnlyCollection<Boolean>;
return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian); 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);
}
} }

View File

@ -11,10 +11,12 @@
"overview": "overview", "overview": "overview",
"manage": "manage", "manage": "manage",
"batteryview": "batteryview", "batteryview": "batteryview",
"pvview": "pvview",
"log": "log", "log": "log",
"live": "live", "live": "live",
"information": "information", "information": "information",
"configuration": "configuration", "configuration": "configuration",
"history": "history",
"mainstats": "mainstats", "mainstats": "mainstats",
"detailed_view": "detailed_view/" "detailed_view": "detailed_view/"
} }

View File

@ -246,8 +246,8 @@ function MainStats(props: MainStatsProps) {
chartOverview: BatteryOverviewInterface; chartOverview: BatteryOverviewInterface;
}> = transformInputToBatteryViewData( }> = transformInputToBatteryViewData(
props.s3Credentials, props.s3Credentials,
UnixTime.fromTicks(startX), UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)),
UnixTime.fromTicks(endX) UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2))
); );
resultPromise resultPromise

View File

@ -15,7 +15,7 @@ import {
Typography, Typography,
useTheme useTheme
} from '@mui/material'; } from '@mui/material';
import React, { useState } from 'react'; import React, { useContext, useState } from 'react';
import { FormattedMessage } from 'react-intl'; import { FormattedMessage } from 'react-intl';
import Button from '@mui/material/Button'; import Button from '@mui/material/Button';
import { Close as CloseIcon } from '@mui/icons-material'; 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 dayjs from 'dayjs';
import axiosConfig from '../../../Resources/axiosConfig'; import axiosConfig from '../../../Resources/axiosConfig';
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc';
import { Action } from '../../../interfaces/S3Types';
import { UserContext } from '../../../contexts/userContext';
interface ConfigurationProps { interface ConfigurationProps {
values: TopologyValues; values: TopologyValues;
@ -82,6 +84,8 @@ function Configuration(props: ConfigurationProps) {
const [updated, setUpdated] = useState(false); const [updated, setUpdated] = useState(false);
const [dateSelectionError, setDateSelectionError] = useState(''); const [dateSelectionError, setDateSelectionError] = useState('');
const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false); const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false);
const context = useContext(UserContext);
const { currentUser, setUser } = context;
const [formValues, setFormValues] = useState<ConfigurationValues>({ const [formValues, setFormValues] = useState<ConfigurationValues>({
minimumSoC: props.values.minimumSoC[0].value, minimumSoC: props.values.minimumSoC[0].value,
@ -133,6 +137,13 @@ function Configuration(props: ConfigurationProps) {
.toDate() .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)); // console.log('will send ', dayjs(formValues.calibrationChargeDate));
setLoading(true); setLoading(true);
@ -149,10 +160,23 @@ function Configuration(props: ConfigurationProps) {
}); });
if (res) { 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); setUpdated(true);
setLoading(false); setLoading(false);
} }
} }
}
}; };
const handleOkOnErrorDateModal = () => { const handleOkOnErrorDateModal = () => {

View File

@ -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;

View File

@ -23,6 +23,7 @@ import routes from '../../../Resources/routes.json';
import Information from '../Information/Information'; import Information from '../Information/Information';
import BatteryView from '../BatteryView/BatteryView'; import BatteryView from '../BatteryView/BatteryView';
import { UserType } from '../../../interfaces/UserTypes'; import { UserType } from '../../../interfaces/UserTypes';
import HistoryOfActions from '../History/History';
interface singleInstallationProps { interface singleInstallationProps {
current_installation?: I_Installation; current_installation?: I_Installation;
@ -309,6 +310,20 @@ function Installation(props: singleInstallationProps) {
></BatteryView> ></BatteryView>
} }
></Route> ></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 <Route
path={routes.overview} path={routes.overview}
element={<Overview s3Credentials={s3Credentials}></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 && ( {currentUser.userType == UserType.admin && (
<Route <Route
path={routes.manage} path={routes.manage}

View File

@ -26,7 +26,9 @@ function InstallationTabs() {
'batteryview', 'batteryview',
'log', 'log',
'information', 'information',
'configuration' 'configuration',
'history',
'pvview'
]; ];
const [currentTab, setCurrentTab] = useState<string>(undefined); const [currentTab, setCurrentTab] = useState<string>(undefined);
@ -137,6 +139,19 @@ function InstallationTabs() {
defaultMessage="Configuration" 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 : currentUser.userType == UserType.partner
@ -158,6 +173,10 @@ function InstallationTabs() {
/> />
) )
}, },
{
value: 'pvview',
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
},
{ {
value: 'information', value: 'information',
@ -217,6 +236,10 @@ function InstallationTabs() {
/> />
) )
}, },
{
value: 'pvview',
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
},
{ {
value: 'manage', value: 'manage',
label: ( label: (
@ -249,6 +272,15 @@ function InstallationTabs() {
/> />
) )
} }
// {
// value: 'history',
// label: (
// <FormattedMessage
// id="history"
// defaultMessage="History Of Actions"
// />
// )
// }
] ]
: currentUser.userType == UserType.partner : currentUser.userType == UserType.partner
? [ ? [
@ -280,6 +312,10 @@ function InstallationTabs() {
/> />
) )
}, },
{
value: 'pvview',
label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
},
{ {
value: 'information', value: 'information',

View File

@ -35,6 +35,8 @@ export type ConfigurationValues = {
calibrationChargeDate: Date | null; calibrationChargeDate: Date | null;
}; };
export interface Pv {}
export interface Battery { export interface Battery {
BatteryId: number; BatteryId: number;
FwVersion: I_BoxDataValue; FwVersion: I_BoxDataValue;
@ -163,11 +165,19 @@ export type TopologyValues = {
additionalCalibrationChargeDate: I_BoxDataValue[]; additionalCalibrationChargeDate: I_BoxDataValue[];
batteryView: Battery[]; batteryView: Battery[];
pvView: Pv[];
}; };
type TopologyPaths = { [key in keyof TopologyValues]: string[] }; type TopologyPaths = { [key in keyof TopologyValues]: string[] };
const batteryIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 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 = [ const batteryPaths = [
'/Battery/Devices/%id%/FwVersion', '/Battery/Devices/%id%/FwVersion',
'/Battery/Devices/%id%/Dc/Power', '/Battery/Devices/%id%/Dc/Power',
@ -290,6 +300,10 @@ export const topologyPaths: TopologyPaths = {
batteryPaths.map((path) => path.replace('%id%', id.toString())) batteryPaths.map((path) => path.replace('%id%', id.toString()))
), ),
pvView: batteryIds.flatMap((id) =>
PvPaths.map((path) => path.replace('%id%', id.toString()))
),
minimumSoC: ['/Config/MinSoc'], minimumSoC: ['/Config/MinSoc'],
installedDcDcPower: ['/DcDc/SystemControl/NumberOfConnectedSlaves'], installedDcDcPower: ['/DcDc/SystemControl/NumberOfConnectedSlaves'],
gridSetPoint: ['/Config/GridSetPoint'], gridSetPoint: ['/Config/GridSetPoint'],

View File

@ -119,8 +119,8 @@ function Overview(props: OverviewProps) {
chartOverview: overviewInterface; chartOverview: overviewInterface;
}> = transformInputToDailyData( }> = transformInputToDailyData(
props.s3Credentials, props.s3Credentials,
UnixTime.fromTicks(startX), UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)),
UnixTime.fromTicks(endX) UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2))
); );
let isComponentMounted = true; let isComponentMounted = true;

View File

@ -58,6 +58,28 @@ function Installation(props: singleInstallationProps) {
const s3Credentials = { s3Bucket, ...S3data }; 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 fetchDataPeriodically = async () => {
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20)); const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
@ -76,7 +98,7 @@ function Installation(props: singleInstallationProps) {
return true; return true;
} else { } else {
setFailedToCommunicateWithInstallation((prevCount) => { setFailedToCommunicateWithInstallation((prevCount) => {
if (prevCount + 1 >= 3) { if (prevCount + 1 >= 20) {
setConnected(false); setConnected(false);
} }
return prevCount + 1; 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(() => { useEffect(() => {
let path = location.split('/'); let path = location.split('/');
@ -118,7 +127,7 @@ function Installation(props: singleInstallationProps) {
currentTab == 'live' || currentTab == 'live' ||
(location.includes('batteryview') && !location.includes('mainstats')) (location.includes('batteryview') && !location.includes('mainstats'))
) { ) {
fetchDataPeriodically(); fetchDataOnlyOneTime();
interval = setInterval(fetchDataPeriodically, 2000); interval = setInterval(fetchDataPeriodically, 2000);
} }
if (currentTab == 'configuration' || location.includes('mainstats')) { if (currentTab == 'configuration' || location.includes('mainstats')) {

View File

@ -222,6 +222,7 @@ function Topology(props: TopologyProps) {
}} }}
bottomBox={{ bottomBox={{
title: 'AC Loads', title: 'AC Loads',
data: props.values.islandBusToLoadOnIslandBusConnection, data: props.values.islandBusToLoadOnIslandBusConnection,
connected: connected:
props.values.loadOnIslandBusBox[0].value.toString() != props.values.loadOnIslandBusBox[0].value.toString() !=

View File

@ -1,3 +1,5 @@
import { ConfigurationValues } from '../content/dashboards/Log/graph.util';
export interface I_S3Credentials { export interface I_S3Credentials {
s3Region: string; s3Region: string;
s3Provider: string; s3Provider: string;
@ -16,3 +18,10 @@ export interface ErrorMessage {
deviceCreatedTheMessage: string; deviceCreatedTheMessage: string;
seen: boolean; seen: boolean;
} }
export interface Action {
configuration: ConfigurationValues;
date: string;
time: string;
user: string;
}