Updated backend to provide support for the overview tab

Updated frontend to parse chunks
This commit is contained in:
Noe 2024-06-26 17:05:27 +02:00
parent e3e9817f2b
commit abe69193e2
22 changed files with 690 additions and 286 deletions

View File

@ -137,6 +137,72 @@ public class Controller : ControllerBase
.ToList();
}
[HttpGet(nameof(GetCsvTimestampsForInstallation))]
public ActionResult<IEnumerable<CsvTimestamp>> GetCsvTimestampsForInstallation(Int64 id, Int32 start, Int32 end, Token authToken)
{
var user = Db.GetSession(authToken)?.User;
if (user == null)
return Unauthorized();
var installation = Db.GetInstallationById(id);
if (installation is null || !user.HasAccessTo(installation))
return Unauthorized();
var sampleSize = 100;
var allTimestamps = new List<CsvTimestamp>();
if (start != 0 && end != 0)
{
allTimestamps = Db.CsvTimestamps
.Where(csvTimestamp => csvTimestamp.InstallationId == id && csvTimestamp.Timestamp > start && csvTimestamp.Timestamp < end)
.OrderBy(csvTimestamp => csvTimestamp.Timestamp).ToList();
}
else
{
allTimestamps = Db.CsvTimestamps
.Where(csvTimestamp => csvTimestamp.InstallationId == id)
.OrderBy(csvTimestamp => csvTimestamp.Timestamp).ToList();
}
int totalRecords = allTimestamps.Count;
if (totalRecords <= sampleSize)
{
// If the total records are less than or equal to the sample size, return all records
Console.WriteLine("Start timestamp = "+start +" end timestamp = "+end);
Console.WriteLine("SampledTimestamps = " + allTimestamps.Count);
return allTimestamps;
}
int interval = totalRecords / sampleSize;
var sampledTimestamps = new List<CsvTimestamp>();
for (int i = 0; i < totalRecords; i += interval)
{
sampledTimestamps.Add(allTimestamps[i]);
}
// If we haven't picked enough records (due to rounding), add the latest record to ensure completeness
if (sampledTimestamps.Count < sampleSize)
{
sampledTimestamps.Add(allTimestamps.Last());
}
Console.WriteLine("Start timestamp = "+start +" end timestamp = "+end);
Console.WriteLine("TotalRecords = "+totalRecords + " interval = "+ interval);
Console.WriteLine("SampledTimestamps = " + sampledTimestamps.Count);
return sampledTimestamps;
// return Db.CsvTimestamps
// .Where(csvTimestamp => csvTimestamp.InstallationId == id)
// .OrderByDescending(csvTimestamp => csvTimestamp.Timestamp)
// .ToList();
}
[HttpGet(nameof(GetUserById))]
public ActionResult<User> GetUserById(Int64 id, Token authToken)
{
@ -555,6 +621,7 @@ public class Controller : ControllerBase
{
var session = Db.GetSession(authToken);
var installationToUpdate = Db.GetInstallationById(installationId);
if (installationToUpdate != null)
{

View File

@ -0,0 +1,11 @@
using SQLite;
namespace InnovEnergy.App.Backend.DataTypes;
public class CsvTimestamp{
[PrimaryKey, AutoIncrement]
public Int64 Id { get; set; }
public Int64 InstallationId { get; set; }
public Int32 Timestamp { get; set; }
}

View File

@ -32,6 +32,11 @@ public static partial class Db
return Insert(warning);
}
public static Boolean Create(CsvTimestamp csvTimestamp)
{
return Insert(csvTimestamp);
}
public static Boolean Create(Folder folder)
{
return Insert(folder);
@ -144,4 +149,35 @@ public static partial class Db
Create(newWarning);
}
}
public static void AddCsvTimestamp(CsvTimestamp newCsvTimestamp,int installationId)
{
var maxCSvPerInstallation = 2 * 60 * 24;
//Find the total number of warnings for this installation
var totalCsvNames = CsvTimestamps.Count(csvTimestamp => csvTimestamp.InstallationId == installationId);
//If there are 100 warnings, remove the one with the oldest timestamp
if (totalCsvNames == maxCSvPerInstallation)
{
var oldestCSvTimestamp =
CsvTimestamps.Where(csvTimestamp => csvTimestamp.InstallationId == installationId)
.OrderBy(csvTimestamp => csvTimestamp.Timestamp)
.FirstOrDefault();
//Remove the old error
Delete(oldestCSvTimestamp);
//Add the new error
Create(newCsvTimestamp);
}
else
{
Console.WriteLine("---------------Added the new Csv Timestamp to the database-----------------");
Create(newCsvTimestamp);
}
}
}

View File

@ -37,6 +37,7 @@ public static partial class Db
fileConnection.CreateTable<Error>();
fileConnection.CreateTable<Warning>();
fileConnection.CreateTable<UserAction>();
fileConnection.CreateTable<CsvTimestamp>();
return fileConnection;
//return CopyDbToMemory(fileConnection);
@ -57,6 +58,7 @@ public static partial class Db
memoryConnection.CreateTable<Error>();
memoryConnection.CreateTable<Warning>();
fileConnection.CreateTable<UserAction>();
fileConnection.CreateTable<CsvTimestamp>();
//Copy all the existing tables from the disk to main memory
fileConnection.Table<Session>().ForEach(memoryConnection.Insert);
@ -68,7 +70,8 @@ public static partial class Db
fileConnection.Table<OrderNumber2Installation>().ForEach(memoryConnection.Insert);
fileConnection.Table<Error>().ForEach(memoryConnection.Insert);
fileConnection.Table<Warning>().ForEach(memoryConnection.Insert);
fileConnection.Table<UserAction>().ForEach(memoryConnection.Insert);
fileConnection.Table<UserAction>().ForEach(memoryConnection.Insert);
fileConnection.Table<CsvTimestamp>().ForEach(memoryConnection.Insert);
return memoryConnection;
}
@ -89,6 +92,7 @@ public static partial class Db
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 TableQuery<CsvTimestamp> CsvTimestamps => Connection.Table<CsvTimestamp>();
public static void Init()
{
@ -111,6 +115,7 @@ public static partial class Db
Connection.CreateTable<Error>();
Connection.CreateTable<Warning>();
Connection.CreateTable<UserAction>();
Connection.CreateTable<CsvTimestamp>();
});
//UpdateKeys();

View File

@ -77,6 +77,20 @@ public static partial class Db
return Warnings.Delete(error => error.Id == warningToDelete.Id) >0;
}
}
public static Boolean Delete(CsvTimestamp csvTimestampToDelete)
{
var deleteSuccess = RunTransaction(DeleteCsvTimestampToDelete);
if (deleteSuccess)
BackupDatabase();
return deleteSuccess;
Boolean DeleteCsvTimestampToDelete()
{
return CsvTimestamps.Delete(csvTimestamp => csvTimestamp.Id == csvTimestampToDelete.Id) >0;
}
}
public static Boolean Delete(Installation installation)
{

View File

@ -68,7 +68,18 @@ public static class RabbitMqManager
//Every 15 iterations(30 seconds), the installation sends a heartbit message to the queue
if (receivedStatusMessage.Type == MessageType.Heartbit)
{
Console.WriteLine("This is a heartbit message from installation: " + installationId);
Console.WriteLine("This is a heartbit message from installation: " + installationId + " Name of the file is "+ receivedStatusMessage.Timestamp);
if (receivedStatusMessage.Timestamp != 0)
{
CsvTimestamp newCsvTimestamp = new CsvTimestamp
{
InstallationId = installationId,
Timestamp = receivedStatusMessage.Timestamp
};
Db.AddCsvTimestamp(newCsvTimestamp, installationId);
}
}
else
{
@ -182,52 +193,5 @@ public static class RabbitMqManager
};
Channel.BasicConsume(queue: "statusQueue", autoAck: true, consumer: consumer);
}
public static void InformInstallationsToSubscribeToRabbitMq()
{
var installationIps = Db.Installations.Select(inst => inst.VpnIp).ToList();
Console.WriteLine("Count is "+installationIps.Count);
var maxRetransmissions = 2;
UdpClient udpClient = new UdpClient();
udpClient.Client.ReceiveTimeout = 2000;
int port = 9000;
//Send a message to each installation and tell it to subscribe to the queue
using (udpClient)
{
for (int i = 0; i < installationIps.Count; i++)
{
if(installationIps[i]==""){continue;}
Console.WriteLine("-----------------------------------------------------------");
Console.WriteLine("Trying to reach installation with IP: " + installationIps[i]);
//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(message);
udpClient.Send(data, data.Length, installationIps[i], port);
Console.WriteLine($"Sent UDP message to {installationIps[i]}:{port}: {message}");
IPEndPoint remoteEndPoint = new IPEndPoint(IPAddress.Parse(installationIps[i]), port);
try
{
byte[] replyData = udpClient.Receive(ref remoteEndPoint);
string replyMessage = Encoding.UTF8.GetString(replyData);
Console.WriteLine("Received " + replyMessage + " from installation " + installationIps[i]);
break;
}
catch (SocketException ex)
{
if (ex.SocketErrorCode == SocketError.TimedOut){Console.WriteLine("Timed out waiting for a response. Retry...");}
else{Console.WriteLine("Error: " + ex.Message);}
}
}
}
}
Console.WriteLine("Start RabbitMQ Consumer");
}
}

View File

@ -9,6 +9,7 @@ public class StatusMessage
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

View File

@ -16,9 +16,9 @@ dotnet publish \
-r linux-x64
echo -e "\n============================ Deploy ============================\n"
#ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" "10.2.4.113" "10.2.4.29")
#ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.29" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" "10.2.4.113" "10.2.4.211")
#ip_addresses=("10.2.4.154" "10.2.4.29")
ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35" "10.2.4.154" "10.2.4.29")
ip_addresses=("10.2.3.115" "10.2.3.104" "10.2.4.29" "10.2.4.33" "10.2.4.32" "10.2.4.36" "10.2.4.35")

View File

@ -10,6 +10,7 @@ public class StatusMessage
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

View File

@ -1,4 +1,4 @@
#undef Amax
#define Amax
#undef GridLimit
using System.Diagnostics;
@ -58,9 +58,10 @@ internal static class Program
private static Boolean _subscribedToQueue = false;
private static Boolean _subscribeToQueueForTheFirstTime = false;
private static SalimaxAlarmState _prevSalimaxState = SalimaxAlarmState.Green;
private static Int32 _heartBitInterval = 0;
//private static Int32 _heartBitInterval = 0;
private const UInt16 NbrOfFileToConcatenate = 15;
private static UInt16 _counterOfFile = 0;
private static SalimaxAlarmState _salimaxAlarmState = SalimaxAlarmState.Green;
static Program()
{
@ -185,7 +186,6 @@ internal static class Program
LoadOnAcGrid = gridBusLoad,
LoadOnAcIsland = loadOnAcIsland,
LoadOnDc = dcLoad,
StateMachine = StateMachine.Default,
EssControl = EssControl.Default,
Log = new SystemLog { SalimaxAlarmState = SalimaxAlarmState.Green, Message = null, SalimaxAlarms = null, SalimaxWarnings = null}, //TODO: Put real stuff
@ -269,7 +269,7 @@ internal static class Program
var subscribedNow = false;
//Every 15 iterations(30 seconds), the installation sends a heartbit message to the queue
_heartBitInterval++;
//_heartBitInterval++;
//When the controller boots, it tries to subscribe to the queue
if (_subscribeToQueueForTheFirstTime == false)
@ -287,16 +287,16 @@ internal static class Program
if (s3Bucket != null)
RabbitMqManager.InformMiddleware(currentSalimaxState);
}
else if (_subscribedToQueue && _heartBitInterval >= 30)
{
//Send a heartbit to the backend
Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------");
_heartBitInterval = 0;
currentSalimaxState.Type = MessageType.Heartbit;
if (s3Bucket != null)
RabbitMqManager.InformMiddleware(currentSalimaxState);
}
// else if (_subscribedToQueue && _heartBitInterval >= 30)
// {
// //Send a heartbit to the backend
// Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------");
// _heartBitInterval = 0;
// currentSalimaxState.Type = MessageType.Heartbit;
//
// if (s3Bucket != null)
// RabbitMqManager.InformMiddleware(currentSalimaxState);
// }
//If there is an available message from the RabbitMQ Broker, apply the configuration file
Configuration? config = SetConfigurationFile();
@ -441,13 +441,13 @@ internal static class Program
});
}
var salimaxAlarmsState = warningList.Any()
_salimaxAlarmState = warningList.Any()
? SalimaxAlarmState.Orange
: SalimaxAlarmState.Green; // this will be replaced by LedState
salimaxAlarmsState = alarmList.Any()
_salimaxAlarmState = alarmList.Any()
? SalimaxAlarmState.Red
: salimaxAlarmsState; // this will be replaced by LedState
: _salimaxAlarmState; // this will be replaced by LedState
int.TryParse(s3Bucket?.Split("-")[0], out var installationId);
@ -455,7 +455,7 @@ internal static class Program
{
InstallationId = installationId,
Product = 0,
Status = salimaxAlarmsState,
Status = _salimaxAlarmState,
Type = MessageType.AlarmOrWarning,
Alarms = alarmList,
Warnings = warningList
@ -683,6 +683,7 @@ internal static class Program
private static async Task<Boolean> UploadCsv(StatusRecord status, DateTime timeStamp)
{
var csv = status.ToCsv().LogInfo();
await RestApiSavingfile(csv);
@ -730,6 +731,26 @@ internal static class Program
Console.WriteLine(error);
return false;
}
Console.WriteLine("----------------------------------------Sending Heartbit----------------------------------------");
var s3Bucket = Config.Load().S3?.Bucket;
int.TryParse(s3Bucket?.Split("-")[0], out var installationId);
int.TryParse(timeStamp.ToUnixTime().ToString(), out var nameOfCsvFile);
var returnedStatus = new StatusMessage
{
InstallationId = installationId,
Product = 0,
Status = _salimaxAlarmState,
Type = MessageType.Heartbit,
Timestamp = nameOfCsvFile
};
if (s3Bucket != null)
RabbitMqManager.InformMiddleware(returnedStatus);
}
_counterOfFile++;

View File

@ -170,7 +170,10 @@ function BatteryView(props: BatteryViewProps) {
<Route
path={routes.mainstats + '*'}
element={
<MainStats s3Credentials={props.s3Credentials}></MainStats>
<MainStats
s3Credentials={props.s3Credentials}
id={props.installationId}
></MainStats>
}
/>
{props.values.batteryView.map((battery) => (

View File

@ -28,6 +28,7 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
interface MainStatsProps {
s3Credentials: I_S3Credentials;
id: number;
}
function MainStats(props: MainStatsProps) {
@ -92,11 +93,7 @@ function MainStats(props: MainStatsProps) {
const resultPromise: Promise<{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
}> = transformInputToBatteryViewData(
props.s3Credentials,
UnixTime.now().rangeBefore(TimeSpan.fromDays(1)).start,
UnixTime.now()
);
}> = transformInputToBatteryViewData(props.s3Credentials, props.id);
resultPromise
.then((result) => {
@ -186,6 +183,7 @@ function MainStats(props: MainStatsProps) {
chartOverview: BatteryOverviewInterface;
}> = transformInputToBatteryViewData(
props.s3Credentials,
props.id,
UnixTime.fromTicks(startDate.unix()),
UnixTime.fromTicks(endDate.unix())
);
@ -246,6 +244,7 @@ function MainStats(props: MainStatsProps) {
chartOverview: BatteryOverviewInterface;
}> = transformInputToBatteryViewData(
props.s3Credentials,
props.id,
UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)),
UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2))
);

View File

@ -136,7 +136,7 @@ function HistoryOfActions(props: HistoryProps) {
label="Select Action Date"
name="timestamp"
value={actionDate}
onChange={(newDate) => handleDateChange(newDate.toDate())}
onChange={(newDate) => handleDateChange(newDate)}
sx={{
width: 450,
marginTop: 2

View File

@ -4,9 +4,13 @@ import {
CardContent,
CircularProgress,
Container,
FormControl,
Grid,
IconButton,
InputLabel,
MenuItem,
Modal,
Select,
TextField,
Typography,
useTheme
@ -42,6 +46,8 @@ function InformationSalidomo(props: InformationSalidomoProps) {
useState(false);
const navigate = useNavigate();
const DeviceTypes = ['Cerbo', 'Venus'];
const installationContext = useContext(InstallationsContext);
const {
updateInstallation,
@ -259,6 +265,61 @@ function InformationSalidomo(props: InformationSalidomoProps) {
/>
</div>
<div>
<TextField
label={
<FormattedMessage id="vrmLink" defaultMessage="vrmLink" />
}
name="vrmLink"
value={formValues.vrmLink}
onChange={handleChange}
variant="outlined"
fullWidth
/>
</div>
<div>
<FormControl
fullWidth
sx={{
marginLeft: 1,
marginTop: 1,
marginBottom: 1,
width: 390
}}
>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="DeviceType"
defaultMessage="Device Type"
/>
</InputLabel>
<Select
name="device"
value={
DeviceTypes.indexOf(
DeviceTypes[formValues.device - 1]
) + 1
}
onChange={handleChange}
>
{DeviceTypes.map((type) => (
<MenuItem
key={type}
value={DeviceTypes.indexOf(type) + 1}
>
{type}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div>
<TextField
label={
@ -275,19 +336,6 @@ function InformationSalidomo(props: InformationSalidomoProps) {
/>
</div>
{/*<div>*/}
{/* <TextField*/}
{/* label={*/}
{/* <FormattedMessage id="vrmLink" defaultMessage="vrmLink" />*/}
{/* }*/}
{/* name="vrmLink"*/}
{/* value={formValues.vrmLink}*/}
{/* onChange={handleChange}*/}
{/* variant="outlined"*/}
{/* fullWidth*/}
{/* />*/}
{/*</div>*/}
<div>
<TextField
label="S3 Bucket Name"

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { Card, CircularProgress, Grid, Typography } from '@mui/material';
import { I_Installation } from 'src/interfaces/InstallationTypes';
import { UserContext } from 'src/contexts/userContext';
@ -35,16 +35,13 @@ function Installation(props: singleInstallationProps) {
const context = useContext(UserContext);
const { currentUser } = context;
const location = useLocation().pathname;
const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false);
const [errorLoadingS3Data, setErrorLoadingS3Data] = useState(false);
const webSocketsContext = useContext(WebSocketContext);
const { getStatus } = webSocketsContext;
const [currentTab, setCurrentTab] = useState<string>(undefined);
const [values, setValues] = useState<TopologyValues | null>(null);
const status = getStatus(props.current_installation.id);
const [
failedToCommunicateWithInstallation,
setFailedToCommunicateWithInstallation
] = useState(0);
const [connected, setConnected] = useState(true);
if (props.current_installation == undefined) {
@ -68,51 +65,122 @@ function Installation(props: singleInstallationProps) {
const s3Credentials = { s3Bucket, ...S3data };
const fetchDataPeriodically = async () => {
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
function timeout(delay: number) {
return new Promise((res) => setTimeout(res, delay));
}
try {
const res = await fetchData(now, s3Credentials);
const continueFetching = useRef(false);
if (res != FetchResult.notAvailable && res != FetchResult.tryLater) {
setFailedToCommunicateWithInstallation(0);
setConnected(true);
setValues(
extractValues({
time: now,
value: res
})
);
return true;
} else {
setFailedToCommunicateWithInstallation((prevCount) => {
if (prevCount + 1 >= 3) {
setConnected(false);
}
return prevCount + 1;
});
const fetchDataForOneTime = async () => {
var timeperiodToSearch = 80;
let res;
let timestampToFetch;
for (var i = timeperiodToSearch; i > 0; i -= 2) {
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
try {
res = await fetchData(timestampToFetch, s3Credentials);
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
break;
}
} catch (err) {
console.error('Error fetching data:', err);
return false;
}
} catch (err) {
}
if (i <= 0) {
setConnected(false);
return false;
}
setConnected(true);
const timestamp = Object.keys(res)[Object.keys(res).length - 1];
setValues(
extractValues({
time: UnixTime.fromTicks(parseInt(timestamp, 10)),
value: res[timestamp]
})
);
return true;
};
const fetchDataOnlyOneTime = async () => {
let success = false;
const max_retransmissions = 3;
const fetchDataPeriodically = async () => {
var timeperiodToSearch = 80;
let res;
let timestampToFetch;
for (let i = 0; i < max_retransmissions; i++) {
success = await fetchDataPeriodically();
await new Promise((resolve) => setTimeout(resolve, 1000));
if (success) {
break;
for (var i = timeperiodToSearch; i > 0; i -= 2) {
if (!continueFetching.current) {
return false;
}
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
try {
res = await fetchData(timestampToFetch, s3Credentials);
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
break;
}
} catch (err) {
console.error('Error fetching data:', err);
return false;
}
}
if (i <= 0) {
setConnected(false);
return false;
}
setConnected(true);
while (continueFetching.current) {
for (const timestamp of Object.keys(res)) {
if (!continueFetching.current) {
setFetchFunctionCalled(false);
return false;
}
console.log(`Timestamp: ${timestamp}`);
console.log(res[timestamp]);
// Set values asynchronously with delay
setValues(
extractValues({
time: UnixTime.fromTicks(parseInt(timestamp, 10)),
value: res[timestamp]
})
);
// Wait for 2 seconds before processing next timestamp
await timeout(2000);
}
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(30));
console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch);
for (i = 0; i < 10; i++) {
if (!continueFetching.current) {
return false;
}
try {
console.log('Trying to fetch timestamp ' + timestampToFetch);
res = await fetchData(timestampToFetch, s3Credentials);
if (
res !== FetchResult.notAvailable &&
res !== FetchResult.tryLater
) {
break;
}
} catch (err) {
console.error('Error fetching data:', err);
return false;
}
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(1));
}
}
};
useEffect(() => {
let path = location.split('/');
setCurrentTab(path[path.length - 1]);
}, [location]);
@ -129,30 +197,28 @@ function Installation(props: singleInstallationProps) {
currentTab == 'configuration' ||
location.includes('batteryview')
) {
let interval;
if (
currentTab == 'live' ||
(location.includes('batteryview') && !location.includes('mainstats')) ||
currentTab == 'pvview'
) {
fetchDataPeriodically();
interval = setInterval(fetchDataPeriodically, 2000);
if (!continueFetching.current) {
continueFetching.current = true;
if (!fetchFunctionCalled) {
setFetchFunctionCalled(true);
fetchDataPeriodically();
}
}
}
if (currentTab == 'configuration' || location.includes('mainstats')) {
fetchDataOnlyOneTime();
if (currentTab == 'configuration') {
fetchDataForOneTime();
}
// Cleanup function to cancel interval
return () => {
if (
currentTab == 'live' ||
currentTab == 'pvview' ||
(location.includes('batteryview') && !location.includes('mainstats'))
) {
clearInterval(interval);
}
continueFetching.current = false;
};
} else {
continueFetching.current = false;
}
}, [currentTab, location]);
@ -324,7 +390,12 @@ function Installation(props: singleInstallationProps) {
<Route
path={routes.overview}
element={<Overview s3Credentials={s3Credentials}></Overview>}
element={
<Overview
s3Credentials={s3Credentials}
id={props.current_installation.id}
></Overview>
}
/>
<Route

View File

@ -3,10 +3,10 @@ import { I_S3Credentials } from 'src/interfaces/S3Types';
import { FetchResult } from 'src/dataCache/dataCache';
import { DataRecord } from 'src/dataCache/data';
import { S3Access } from 'src/dataCache/S3/S3Access';
import { parseCsv } from '../Log/graph.util';
import { parseChunk, parseCsv } from '../Log/graph.util';
import JSZip from 'jszip';
export const fetchDailyData = (
export const fetchAggregatedData = (
date: string,
s3Credentials?: I_S3Credentials
): Promise<FetchResult<DataRecord>> => {
@ -25,10 +25,7 @@ export const fetchDailyData = (
if (r.status === 404) {
return Promise.resolve(FetchResult.notAvailable);
} else if (r.status === 200) {
// const text = await r.text();
const csvtext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text
//const response = await fetch(url); // Fetch the resource from the server
const contentEncoding = r.headers.get('content-type');
if (contentEncoding != 'application/base64; charset=utf-8') {
@ -43,11 +40,7 @@ export const fetchDailyData = (
const zip = await JSZip.loadAsync(byteArray);
// Assuming the CSV file is named "data.csv" inside the ZIP archive
const csvContent = await zip.file('data.csv').async('text');
return parseCsv(csvContent);
//console.log(parseCsv(text));
//return parseCsv(text);
} else {
return Promise.resolve(FetchResult.notAvailable);
}
@ -61,7 +54,7 @@ export const fetchDailyData = (
export const fetchData = (
timestamp: UnixTime,
s3Credentials?: I_S3Credentials
): Promise<FetchResult<DataRecord>> => {
): Promise<FetchResult<Record<string, DataRecord>>> => {
const s3Path = `${timestamp.ticks}.csv`;
if (s3Credentials && s3Credentials.s3Bucket) {
const s3Access = new S3Access(
@ -79,12 +72,10 @@ export const fetchData = (
return Promise.resolve(FetchResult.notAvailable);
} else if (r.status === 200) {
const csvtext = await r.text(); // Assuming the server returns the Base64 encoded ZIP file as text
//const response = await fetch(url); // Fetch the resource from the server
const contentEncoding = r.headers.get('content-type');
if (contentEncoding != 'application/base64; charset=utf-8') {
return parseCsv(csvtext);
return parseChunk(csvtext);
}
const byteArray = Uint8Array.from(atob(csvtext), (c) =>
@ -96,7 +87,7 @@ export const fetchData = (
// Assuming the CSV file is named "data.csv" inside the ZIP archive
const csvContent = await zip.file('data.csv').async('text');
return parseCsv(csvContent);
return parseChunk(csvContent);
} else {
return Promise.resolve(FetchResult.notAvailable);
}

View File

@ -23,6 +23,35 @@ export const parseCsv = (text: string): DataRecord => {
.reduce((acc, current) => ({ ...acc, ...current }), {} as DataRecord);
};
export const parseChunk = (text: string): Record<string, DataRecord> => {
const lines = text.split(/\r?\n/).filter((line) => line.length > 0);
let result: Record<string, DataRecord> = {};
let currentTimestamp = null;
lines.forEach((line) => {
const fields = line.split(';');
if (fields[0] === 'Timestamp') {
currentTimestamp = fields[1];
result[currentTimestamp] = {};
} else if (currentTimestamp) {
let key = fields[0];
let value = fields[1];
let unit = fields[2];
if (isNaN(Number(value)) || value === '') {
result[currentTimestamp][key] = { value: value, unit: unit };
} else {
result[currentTimestamp][key] = {
value: parseFloat(value),
unit: unit
};
}
}
});
return result;
};
export interface I_BoxDataValue {
unit: string;
value: string | number;

View File

@ -13,15 +13,16 @@ import {
import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl';
import CircularProgress from '@mui/material/CircularProgress';
import { TimeSpan, UnixTime } from '../../../dataCache/time';
import { DateTimePicker, LocalizationProvider } from '@mui/x-date-pickers';
import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs';
import { UserContext } from '../../../contexts/userContext';
import { UserType } from '../../../interfaces/UserTypes';
import { TimeSpan, UnixTime } from '../../../dataCache/time';
interface OverviewProps {
s3Credentials: I_S3Credentials;
id: number;
}
const computeLast7Days = (): string[] => {
@ -43,7 +44,6 @@ const computeLast7Days = (): string[] => {
function Overview(props: OverviewProps) {
const context = useContext(UserContext);
const { currentUser } = context;
const [dailyData, setDailyData] = useState(true);
const [aggregatedData, setAggregatedData] = useState(false);
const [loading, setLoading] = useState(true);
@ -85,11 +85,7 @@ function Overview(props: OverviewProps) {
const resultPromise: Promise<{
chartData: chartDataInterface;
chartOverview: overviewInterface;
}> = transformInputToDailyData(
props.s3Credentials,
UnixTime.now().rangeBefore(TimeSpan.fromDays(1)).start,
UnixTime.now()
);
}> = transformInputToDailyData(props.s3Credentials, props.id);
resultPromise
.then((result) => {
@ -119,6 +115,7 @@ function Overview(props: OverviewProps) {
chartOverview: overviewInterface;
}> = transformInputToDailyData(
props.s3Credentials,
props.id,
UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)),
UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2))
);
@ -174,7 +171,6 @@ function Overview(props: OverviewProps) {
chartOverview: overviewInterface;
}> = transformInputToAggregatedData(
props.s3Credentials,
dayjs().subtract(1, 'week'),
dayjs()
);
@ -246,6 +242,7 @@ function Overview(props: OverviewProps) {
chartOverview: overviewInterface;
}> = transformInputToDailyData(
props.s3Credentials,
props.id,
UnixTime.fromTicks(startDate.unix()),
UnixTime.fromTicks(endDate.unix())
);

View File

@ -1,4 +1,4 @@
import React, { useContext, useEffect, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { Card, CircularProgress, Grid, Typography } from '@mui/material';
import { I_Installation } from 'src/interfaces/InstallationTypes';
import { UserContext } from 'src/contexts/userContext';
@ -56,93 +56,110 @@ function Installation(props: singleInstallationProps) {
'-' +
'c0436b6a-d276-4cd8-9c44-1eae86cf5d0e';
const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false);
const s3Credentials = { s3Bucket, ...S3data };
const fetchDataOnlyOneTime = async () => {
var timeperiodToSearch = 70;
function timeout(delay: number) {
return new Promise((res) => setTimeout(res, delay));
}
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 continueFetching = useRef(false);
const fetchDataPeriodically = async () => {
const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20));
var timeperiodToSearch = 80;
let res;
let timestampToFetch;
try {
const res = await fetchData(now, s3Credentials);
if (res != FetchResult.notAvailable && res != FetchResult.tryLater) {
setConnected(true);
setFailedToCommunicateWithInstallation(0);
setValues(
extractValues({
time: now,
value: res
})
);
return true;
} else {
setFailedToCommunicateWithInstallation((prevCount) => {
if (prevCount + 1 >= 20) {
setConnected(false);
}
return prevCount + 1;
});
for (var i = timeperiodToSearch; i > 0; i -= 2) {
if (!continueFetching.current) {
return false;
}
} catch (err) {
timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i));
try {
res = await fetchData(timestampToFetch, s3Credentials);
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
break;
}
} catch (err) {
console.error('Error fetching data:', err);
return false;
}
}
if (i <= 0) {
setConnected(false);
return false;
}
};
setConnected(true);
while (continueFetching.current) {
for (const timestamp of Object.keys(res)) {
if (!continueFetching.current) {
setFetchFunctionCalled(false);
return false;
}
console.log(`Timestamp: ${timestamp}`);
console.log(res[timestamp]);
// Set values asynchronously with delay
setValues(
extractValues({
time: UnixTime.fromTicks(parseInt(timestamp, 10)),
value: res[timestamp]
})
);
// Wait for 2 seconds before processing next timestamp
await timeout(2000);
}
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(30));
console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch);
for (i = 0; i < 10; i++) {
if (!continueFetching.current) {
return false;
}
try {
console.log('Trying to fetch timestamp ' + timestampToFetch);
res = await fetchData(timestampToFetch, s3Credentials);
if (
res !== FetchResult.notAvailable &&
res !== FetchResult.tryLater
) {
break;
}
} catch (err) {
console.error('Error fetching data:', err);
return false;
}
timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(1));
}
}
};
useEffect(() => {
let path = location.split('/');
setCurrentTab(path[path.length - 1]);
}, [location]);
useEffect(() => {
if (
currentTab == 'live' ||
currentTab == 'configuration' ||
location.includes('batteryview')
) {
let interval;
if (
currentTab == 'live' ||
(location.includes('batteryview') && !location.includes('mainstats'))
) {
fetchDataOnlyOneTime();
interval = setInterval(fetchDataPeriodically, 2000);
}
if (currentTab == 'configuration' || location.includes('mainstats')) {
fetchDataOnlyOneTime();
}
// Cleanup function to cancel interval and update isMounted when unmounted
return () => {
if (
currentTab == 'live' ||
(location.includes('batteryview') && !location.includes('mainstats'))
) {
clearInterval(interval);
if (location.includes('batteryview')) {
if (location.includes('batteryview') && !location.includes('mainstats')) {
if (!continueFetching.current) {
continueFetching.current = true;
if (!fetchFunctionCalled) {
setFetchFunctionCalled(true);
fetchDataPeriodically();
}
}
}
return () => {
continueFetching.current = false;
};
} else {
continueFetching.current = false;
}
}, [currentTab, location]);

View File

@ -3,8 +3,12 @@ import {
Alert,
Box,
CircularProgress,
FormControl,
IconButton,
InputLabel,
MenuItem,
Modal,
Select,
TextField,
useTheme
} from '@mui/material';
@ -37,6 +41,8 @@ function SalidomonstallationForm(props: SalidomoInstallationFormProps) {
'vpnIp',
'vrmLink'
];
const DeviceTypes = ['Cerbo', 'Venus'];
const installationContext = useContext(InstallationsContext);
const { createInstallation, loading, setLoading, error, setError } =
installationContext;
@ -128,6 +134,7 @@ function SalidomonstallationForm(props: SalidomoInstallationFormProps) {
value={formValues.region}
onChange={handleChange}
required
error={formValues.region === ''}
/>
</div>
<div>
@ -180,6 +187,36 @@ function SalidomonstallationForm(props: SalidomoInstallationFormProps) {
/>
</div>
<div>
<FormControl
fullWidth
sx={{ marginTop: 1, marginBottom: 1, width: 390 }}
>
<InputLabel
sx={{
fontSize: 14,
backgroundColor: 'white'
}}
>
<FormattedMessage
id="DeviceType"
defaultMessage="Device Type"
/>
</InputLabel>
<Select
name="device"
value={DeviceTypes[formValues.device]}
onChange={handleChange}
>
{DeviceTypes.map((type) => (
<MenuItem key={type} value={DeviceTypes.indexOf(type) + 1}>
{type}
</MenuItem>
))}
</Select>
</FormControl>
</div>
<div>
<TextField
label={

View File

@ -1,11 +1,14 @@
import dayjs from 'dayjs';
import {
fetchDailyData,
fetchAggregatedData,
fetchData
} from '../content/dashboards/Installations/fetchData';
import { FetchResult } from '../dataCache/dataCache';
import { I_S3Credentials } from './S3Types';
import { UnixTime } from '../dataCache/time';
import { CsvTimestamp, I_S3Credentials } from './S3Types';
import { TimeSpan, UnixTime } from '../dataCache/time';
import { DataRecord } from '../dataCache/data';
import axiosConfig from '../Resources/axiosConfig';
import { AxiosError, AxiosResponse } from 'axios';
export interface chartInfoInterface {
magnitude: number;
@ -74,8 +77,9 @@ export interface BatteryOverviewInterface {
export const transformInputToBatteryViewData = async (
s3Credentials: I_S3Credentials,
startTimestamp: UnixTime,
endTimestamp: UnixTime
id: number,
start_time?: UnixTime,
end_time?: UnixTime
): Promise<{
chartData: BatteryDataInterface;
chartOverview: BatteryOverviewInterface;
@ -124,42 +128,69 @@ export const transformInputToBatteryViewData = async (
};
let initialiation = true;
//console.log(start_time);
//console.log(end_time);
let timestampArray: CsvTimestamp[] = [];
let adjustedTimestampArray = [];
let startTimestampToNum = Number(startTimestamp);
if (startTimestampToNum % 2 != 0) {
startTimestampToNum += 1;
}
let startUnixTime = UnixTime.fromTicks(startTimestampToNum);
let diff = endTimestamp.ticks - startUnixTime.ticks;
const timestampPromises = [];
while (startUnixTime < endTimestamp) {
timestampPromises.push(fetchData(startUnixTime, s3Credentials));
if (start_time && end_time) {
await axiosConfig
.get(
`/GetCsvTimestampsForInstallation?id=${id}&start=${start_time.ticks}&end=${end_time.ticks}`
)
.then((res: AxiosResponse<CsvTimestamp[]>) => {
timestampArray = res.data;
})
.catch((err: AxiosError) => {
if (err.response && err.response.status == 401) {
//removeToken();
//navigate(routes.login);
}
});
} else {
await axiosConfig
.get(`/GetCsvTimestampsForInstallation?id=${id}&start=${0}&end=${0}`)
.then((res: AxiosResponse<CsvTimestamp[]>) => {
timestampArray = res.data;
})
.catch((err: AxiosError) => {
if (err.response && err.response.status == 401) {
//removeToken();
//navigate(routes.login);
}
});
}
startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + diff / 100);
if (startUnixTime.ticks % 2 !== 0) {
startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + 1);
}
const adjustedTimestamp = new Date(startUnixTime.ticks * 1000);
for (var i = 0; i < timestampArray.length; i++) {
timestampPromises.push(
fetchDataForOneTime(
UnixTime.fromTicks(timestampArray[i].timestamp),
s3Credentials
)
);
const adjustedTimestamp = new Date(timestampArray[i].timestamp * 1000);
//Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset
adjustedTimestamp.setHours(
adjustedTimestamp.getHours() - adjustedTimestamp.getTimezoneOffset() / 60
);
adjustedTimestampArray.push(adjustedTimestamp);
}
//Wait until fetching all the data
const results = await Promise.all(timestampPromises);
const results: Promise<FetchResult<Record<string, DataRecord>>>[] =
await Promise.all(timestampPromises);
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (
result === FetchResult.notAvailable ||
result === FetchResult.tryLater
) {
if (results[i] == null) {
// Handle not available or try later case
} else {
const timestamp = Object.keys(results[i])[
Object.keys(results[i]).length - 1
];
const result = results[i][timestamp];
const battery_nodes = result['/Config/Devices/BatteryNodes'].value
.toString()
.split(',');
@ -252,14 +283,42 @@ export const transformInputToBatteryViewData = async (
};
};
const fetchDataForOneTime = async (
startUnixTime: UnixTime,
s3Credentials: I_S3Credentials
): Promise<FetchResult<Record<string, DataRecord>>> => {
var timeperiodToSearch = 2;
let res;
let timestampToFetch;
for (var i = 0; i < timeperiodToSearch; i++) {
timestampToFetch = startUnixTime.later(TimeSpan.fromSeconds(i));
try {
res = await fetchData(timestampToFetch, s3Credentials);
if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) {
//console.log('Successfully fetched ' + timestampToFetch);
return res;
}
} catch (err) {
console.error('Error fetching data:', err);
}
}
return null;
};
export const transformInputToDailyData = async (
s3Credentials: I_S3Credentials,
startTimestamp: UnixTime,
endTimestamp: UnixTime
id: number,
start_time?: UnixTime,
end_time?: UnixTime
): Promise<{
chartData: chartDataInterface;
chartOverview: overviewInterface;
}> => {
//const navigate = useNavigate();
//const tokencontext = useContext(TokenContext);
//const { removeToken } = tokencontext;
const prefixes = ['', 'k', 'M', 'G', 'T'];
const MAX_NUMBER = 9999999;
const pathsToSearch = [
@ -309,45 +368,71 @@ export const transformInputToDailyData = async (
};
});
//console.log(start_time);
//console.log(end_time);
let timestampArray: CsvTimestamp[] = [];
let adjustedTimestampArray = [];
let startTimestampToNum = Number(startTimestamp);
if (startTimestampToNum % 2 != 0) {
startTimestampToNum += 1;
}
let startUnixTime = UnixTime.fromTicks(startTimestampToNum);
let diff = endTimestamp.ticks - startUnixTime.ticks;
const timestampPromises = [];
while (startUnixTime < endTimestamp) {
timestampPromises.push(fetchData(startUnixTime, s3Credentials));
if (start_time && end_time) {
await axiosConfig
.get(
`/GetCsvTimestampsForInstallation?id=${id}&start=${start_time.ticks}&end=${end_time.ticks}`
)
.then((res: AxiosResponse<CsvTimestamp[]>) => {
timestampArray = res.data;
})
.catch((err: AxiosError) => {
if (err.response && err.response.status == 401) {
//removeToken();
//navigate(routes.login);
}
});
} else {
await axiosConfig
.get(`/GetCsvTimestampsForInstallation?id=${id}&start=${0}&end=${0}`)
.then((res: AxiosResponse<CsvTimestamp[]>) => {
timestampArray = res.data;
})
.catch((err: AxiosError) => {
if (err.response && err.response.status == 401) {
//removeToken();
//navigate(routes.login);
}
});
}
startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + diff / 100);
if (startUnixTime.ticks % 2 !== 0) {
startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + 1);
}
const adjustedTimestamp = new Date(startUnixTime.ticks * 1000);
//while (startUnixTime < endTimestamp) {
for (var i = 0; i < timestampArray.length; i++) {
timestampPromises.push(
fetchDataForOneTime(
UnixTime.fromTicks(timestampArray[i].timestamp),
s3Credentials
)
);
const adjustedTimestamp = new Date(timestampArray[i].timestamp * 1000);
//Timezone offset is negative, so we convert the timestamp to the current zone by subtracting the corresponding offset
adjustedTimestamp.setHours(
adjustedTimestamp.getHours() - adjustedTimestamp.getTimezoneOffset() / 60
);
adjustedTimestampArray.push(adjustedTimestamp);
}
const results = await Promise.all(timestampPromises);
const results: Promise<FetchResult<Record<string, DataRecord>>>[] =
await Promise.all(timestampPromises);
for (let i = 0; i < results.length; i++) {
const result = results[i];
if (
result === FetchResult.notAvailable ||
result === FetchResult.tryLater
) {
if (results[i] == null) {
// Handle not available or try later case
} else {
// eslint-disable-next-line @typescript-eslint/no-loop-func
const timestamp = Object.keys(results[i])[
Object.keys(results[i]).length - 1
];
const result = results[i][timestamp];
let category_index = 0;
// eslint-disable-next-line @typescript-eslint/no-loop-func
pathsToSearch.forEach((path) => {
if (result[path]) {
const value = result[path];
@ -370,6 +455,7 @@ export const transformInputToDailyData = async (
});
}
}
categories.forEach((category) => {
let value = Math.max(
Math.abs(chartOverview[category].max),
@ -500,7 +586,7 @@ export const transformInputToAggregatedData = async (
while (currentDay.isBefore(end_date)) {
timestampPromises.push(
fetchDailyData(currentDay.format('YYYY-MM-DD'), s3Credentials)
fetchAggregatedData(currentDay.format('YYYY-MM-DD'), s3Credentials)
);
currentDay = currentDay.add(1, 'day');
}

View File

@ -17,6 +17,12 @@ export interface ErrorMessage {
seen: boolean;
}
export interface CsvTimestamp {
id: number;
installationId: number;
timestamp: number;
}
export interface Action {
id: number;
userName: string;