Merge remote-tracking branch 'origin/marios'
This commit is contained in:
commit
52f3bf6e3f
|
@ -0,0 +1,10 @@
|
||||||
|
/* Disable text selection for the entire page */
|
||||||
|
body {
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Style the selected text */
|
||||||
|
::selection {
|
||||||
|
background-color: transparent; /* Set the background color to transparent */
|
||||||
|
color: #000; /* Set the text color for selected text */
|
||||||
|
}
|
|
@ -0,0 +1,49 @@
|
||||||
|
export function formatPowerForGraph(value, max): { value: number } {
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
let negative = false;
|
||||||
|
if (max > 1000) {
|
||||||
|
value = parseFloat(value);
|
||||||
|
|
||||||
|
if (value < 0) {
|
||||||
|
value = -value;
|
||||||
|
negative = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = value / 1000;
|
||||||
|
|
||||||
|
while (value >= 1000) {
|
||||||
|
value /= 1000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: negative === false ? value : -value
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function findPower(value) {
|
||||||
|
value = parseFloat(value);
|
||||||
|
|
||||||
|
if (value === 0) {
|
||||||
|
return { value: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the sign of the input value
|
||||||
|
const sign = Math.sign(value);
|
||||||
|
|
||||||
|
// Take the absolute value for calculations
|
||||||
|
value = Math.abs(value);
|
||||||
|
|
||||||
|
// Calculate the power of 10 that's greater or equal to the absolute value
|
||||||
|
let exponent = Math.floor(Math.log10(value));
|
||||||
|
|
||||||
|
// Compute the nearest power of 10
|
||||||
|
const nearestPowerOf10 = Math.pow(10, exponent);
|
||||||
|
|
||||||
|
// Restore the sign to the result
|
||||||
|
const result = nearestPowerOf10 * sign;
|
||||||
|
|
||||||
|
return { value: result };
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
Binary file not shown.
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
|
@ -0,0 +1,11 @@
|
||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
|
||||||
|
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
</g>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 438 B |
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"installation": "installation/",
|
||||||
|
"liveView": "liveView/",
|
||||||
|
"users": "/users/",
|
||||||
|
"log": "log/",
|
||||||
|
"installations": "/installations/",
|
||||||
|
"groups": "/groups/",
|
||||||
|
"group": "group/",
|
||||||
|
"folder": "folder/",
|
||||||
|
"manageAccess": "manageAccess/",
|
||||||
|
"user": "user/",
|
||||||
|
"tree": "tree",
|
||||||
|
"list": "list"
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { TopologyValues } from '../Log/graph.util';
|
||||||
|
import { Box, CardContent, Container, Grid, TextField } from '@mui/material';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface ConfigurationProps {
|
||||||
|
values: TopologyValues;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Configuration(props: ConfigurationProps) {
|
||||||
|
if (props.values === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container maxWidth="xl">
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="stretch"
|
||||||
|
spacing={3}
|
||||||
|
>
|
||||||
|
<Grid item xs={12} md={12}>
|
||||||
|
<CardContent>
|
||||||
|
<Box
|
||||||
|
component="form"
|
||||||
|
sx={{
|
||||||
|
'& .MuiTextField-root': { m: 1, width: '50ch' }
|
||||||
|
}}
|
||||||
|
noValidate
|
||||||
|
autoComplete="off"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label="Minimum SoC"
|
||||||
|
value={props.values.minimumSoC.values[0].value + ' %'}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label="Calibration Charge forced"
|
||||||
|
value={props.values.calibrationChargeForced.values[0].value}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label="Grid Set Point"
|
||||||
|
value={
|
||||||
|
(
|
||||||
|
(props.values.gridSetPoint.values[0].value as number) /
|
||||||
|
1000
|
||||||
|
).toString() + ' kW'
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label="Installed Power DC1010"
|
||||||
|
value={
|
||||||
|
(
|
||||||
|
(props.values.installedDcDcPower.values[0]
|
||||||
|
.value as number) * 10
|
||||||
|
).toString() + ' kW'
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label="Maximum Discharge Power"
|
||||||
|
value={
|
||||||
|
(
|
||||||
|
(props.values.maximumDischargePower.values[0]
|
||||||
|
.value as number) * 48
|
||||||
|
).toString() + ' W'
|
||||||
|
}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<TextField
|
||||||
|
label="Number of Batteries"
|
||||||
|
value={props.values.battery.values.length - 4}
|
||||||
|
fullWidth
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Configuration;
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { UnixTime } from 'src/dataCache/time';
|
||||||
|
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';
|
||||||
|
|
||||||
|
export const fetchData = (
|
||||||
|
timestamp: UnixTime,
|
||||||
|
s3Credentials?: I_S3Credentials
|
||||||
|
): Promise<FetchResult<DataRecord>> => {
|
||||||
|
const s3Path = `${timestamp.ticks}.csv`;
|
||||||
|
if (s3Credentials && s3Credentials.s3Bucket) {
|
||||||
|
const s3Access = new S3Access(
|
||||||
|
s3Credentials.s3Bucket,
|
||||||
|
s3Credentials.s3Region,
|
||||||
|
s3Credentials.s3Provider,
|
||||||
|
s3Credentials.s3Key,
|
||||||
|
s3Credentials.s3Secret
|
||||||
|
);
|
||||||
|
return s3Access
|
||||||
|
.get(s3Path)
|
||||||
|
.then(async (r) => {
|
||||||
|
if (r.status === 404) {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
} else if (r.status === 200) {
|
||||||
|
const text = await r.text();
|
||||||
|
return parseCsv(text);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
return Promise.resolve(FetchResult.tryLater);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Box, Grid, useTheme } from '@mui/material';
|
||||||
|
import InstallationsContextProvider from 'src/contexts/InstallationsContextProvider';
|
||||||
|
import InstallationSearch from './InstallationSearch';
|
||||||
|
|
||||||
|
function FlatView() {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InstallationsContextProvider>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Box p={4}>
|
||||||
|
<InstallationSearch />
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</InstallationsContextProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default FlatView;
|
|
@ -0,0 +1,37 @@
|
||||||
|
import { UnixTime } from '../../../dataCache/time';
|
||||||
|
import { I_S3Credentials } from '../../../interfaces/S3Types';
|
||||||
|
import { FetchResult } from '../../../dataCache/dataCache';
|
||||||
|
import { DataRecord } from '../../../dataCache/data';
|
||||||
|
import { S3Access } from '../../../dataCache/S3/S3Access';
|
||||||
|
import { parseCsv } from './graph.util';
|
||||||
|
|
||||||
|
export const fetchData = (
|
||||||
|
timestamp: UnixTime,
|
||||||
|
s3Credentials?: I_S3Credentials
|
||||||
|
): Promise<FetchResult<DataRecord>> => {
|
||||||
|
const s3Path = `${timestamp.ticks}.csv`;
|
||||||
|
if (s3Credentials && s3Credentials.s3Bucket) {
|
||||||
|
const s3Access = new S3Access(
|
||||||
|
s3Credentials.s3Bucket,
|
||||||
|
s3Credentials.s3Region,
|
||||||
|
s3Credentials.s3Provider,
|
||||||
|
s3Credentials.s3Key,
|
||||||
|
s3Credentials.s3Secret
|
||||||
|
);
|
||||||
|
return s3Access
|
||||||
|
.get(s3Path)
|
||||||
|
.then(async (r) => {
|
||||||
|
if (r.status === 404) {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
} else if (r.status === 200) {
|
||||||
|
const text = await r.text();
|
||||||
|
return parseCsv(text);
|
||||||
|
} else {
|
||||||
|
return Promise.resolve(FetchResult.notAvailable);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
return Promise.resolve(FetchResult.tryLater);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
|
@ -0,0 +1,97 @@
|
||||||
|
// chartOptions.ts
|
||||||
|
|
||||||
|
import { ApexOptions } from 'apexcharts';
|
||||||
|
import { chartInfoInterface } from 'src/interfaces/Chart';
|
||||||
|
import { findPower, formatPowerForGraph } from '../../../Resources/formatPower';
|
||||||
|
|
||||||
|
export const getChartOptions = (chartInfo: chartInfoInterface): ApexOptions => {
|
||||||
|
const chartOptions: ApexOptions = {
|
||||||
|
chart: {
|
||||||
|
id: 'area-datetime',
|
||||||
|
type: 'area',
|
||||||
|
height: 350,
|
||||||
|
zoom: {
|
||||||
|
autoScaleYaxis: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dataLabels: {
|
||||||
|
enabled: false
|
||||||
|
},
|
||||||
|
fill: {
|
||||||
|
type: 'gradient',
|
||||||
|
gradient: {
|
||||||
|
shadeIntensity: 1,
|
||||||
|
opacityFrom: 0.7,
|
||||||
|
opacityTo: 0.9,
|
||||||
|
stops: [0, 100]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
//colors: ['#FF5733', '#3498db'],
|
||||||
|
colors: ['#3498db', '#2ecc71'],
|
||||||
|
//colors: ['#1abc9c', '#e91e63'],
|
||||||
|
xaxis: {
|
||||||
|
type: 'datetime',
|
||||||
|
labels: {
|
||||||
|
datetimeFormatter: {
|
||||||
|
year: 'yyyy',
|
||||||
|
month: "MMM 'yy",
|
||||||
|
day: 'dd MMM',
|
||||||
|
hour: 'HH:mm'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
stroke: {
|
||||||
|
curve: 'smooth',
|
||||||
|
width: 2
|
||||||
|
},
|
||||||
|
yaxis: {
|
||||||
|
min: chartInfo.min > 0 ? 0 : undefined,
|
||||||
|
max:
|
||||||
|
chartInfo.min > 0
|
||||||
|
? Math.round(chartInfo.max / findPower(chartInfo.max).value) *
|
||||||
|
findPower(chartInfo.max).value
|
||||||
|
: undefined,
|
||||||
|
title: {
|
||||||
|
text: chartInfo.unit,
|
||||||
|
style: {
|
||||||
|
fontSize: '12px'
|
||||||
|
},
|
||||||
|
offsetY: -160,
|
||||||
|
offsetX: 25,
|
||||||
|
rotate: 0
|
||||||
|
},
|
||||||
|
labels: {
|
||||||
|
formatter:
|
||||||
|
chartInfo.min > 0
|
||||||
|
? function (value: number) {
|
||||||
|
return formatPowerForGraph(
|
||||||
|
value,
|
||||||
|
chartInfo.max
|
||||||
|
).value.toString();
|
||||||
|
}
|
||||||
|
: function (value: number) {
|
||||||
|
return formatPowerForGraph(
|
||||||
|
value,
|
||||||
|
chartInfo.max
|
||||||
|
).value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
x: {
|
||||||
|
format: 'dd MMM HH:mm'
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
formatter: function (val, opts) {
|
||||||
|
return (
|
||||||
|
formatPowerForGraph(val, chartInfo.max).value.toFixed(2) +
|
||||||
|
' ' +
|
||||||
|
chartInfo.unit
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return chartOptions;
|
||||||
|
};
|
|
@ -0,0 +1,509 @@
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
Container,
|
||||||
|
Grid,
|
||||||
|
Typography,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material';
|
||||||
|
import ReactApexChart from 'react-apexcharts';
|
||||||
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
|
import DataCache from 'src/dataCache/dataCache';
|
||||||
|
import { TimeSpan, UnixTime } from 'src/dataCache/time';
|
||||||
|
import { BehaviorSubject, startWith, throttleTime, withLatestFrom } from 'rxjs';
|
||||||
|
import { RecordSeries } from 'src/dataCache/data';
|
||||||
|
import { createTimes } from '../Log/graph.util';
|
||||||
|
import { I_S3Credentials } from 'src/interfaces/S3Types';
|
||||||
|
import { getChartOptions } from './chartOptions';
|
||||||
|
import { chartDataInterface, overviewInterface } from 'src/interfaces/Chart';
|
||||||
|
import { fetchData } from 'src/content/dashboards/Installations/fetchData';
|
||||||
|
|
||||||
|
const prefixes = ['', 'k', 'M', 'G', 'T'];
|
||||||
|
const MAX_NUMBER = 9999999;
|
||||||
|
|
||||||
|
interface OverviewProps {
|
||||||
|
s3Credentials: I_S3Credentials;
|
||||||
|
}
|
||||||
|
|
||||||
|
function Overview(props: OverviewProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const timeRange = createTimes(
|
||||||
|
UnixTime.now().rangeBefore(TimeSpan.fromDays(1)),
|
||||||
|
200
|
||||||
|
);
|
||||||
|
|
||||||
|
const [chartData, setChartData] = useState<chartDataInterface>({
|
||||||
|
soc: [],
|
||||||
|
temperature: [],
|
||||||
|
dcPower: [],
|
||||||
|
gridPower: [],
|
||||||
|
pvProduction: [],
|
||||||
|
dcBusVoltage: []
|
||||||
|
});
|
||||||
|
|
||||||
|
const [chartOverview, setChartOverview] = useState<overviewInterface>({
|
||||||
|
soc: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
temperature: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
dcPower: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
gridPower: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
pvProduction: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
dcBusVoltage: { magnitude: 0, unit: '', min: 0, max: 0 }
|
||||||
|
});
|
||||||
|
|
||||||
|
const pathsToSearch = [
|
||||||
|
'/Battery/Soc',
|
||||||
|
'/Battery/Temperature',
|
||||||
|
'/Battery/Dc/Power',
|
||||||
|
'/GridMeter/Ac/Power/Active',
|
||||||
|
'/PvOnDc/Dc/Power',
|
||||||
|
'/DcDc/Dc/Link/Voltage'
|
||||||
|
];
|
||||||
|
|
||||||
|
const cache = useMemo(() => {
|
||||||
|
return new DataCache(
|
||||||
|
fetchData,
|
||||||
|
TimeSpan.fromSeconds(2),
|
||||||
|
props.s3Credentials
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const times$ = useMemo(() => new BehaviorSubject(timeRange), []);
|
||||||
|
|
||||||
|
const transformToGraphData = (
|
||||||
|
input: RecordSeries
|
||||||
|
): {
|
||||||
|
chartData: chartDataInterface;
|
||||||
|
chartOverview: overviewInterface;
|
||||||
|
} => {
|
||||||
|
const data = {};
|
||||||
|
const overviewData = {};
|
||||||
|
|
||||||
|
const chartData: chartDataInterface = {
|
||||||
|
soc: [],
|
||||||
|
temperature: [],
|
||||||
|
dcPower: [],
|
||||||
|
gridPower: [],
|
||||||
|
pvProduction: [],
|
||||||
|
dcBusVoltage: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const chartOverview: overviewInterface = {
|
||||||
|
soc: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
temperature: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
dcPower: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
gridPower: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
pvProduction: { magnitude: 0, unit: '', min: 0, max: 0 },
|
||||||
|
dcBusVoltage: { magnitude: 0, unit: '', min: 0, max: 0 }
|
||||||
|
};
|
||||||
|
|
||||||
|
pathsToSearch.forEach((path) => {
|
||||||
|
data[path] = [];
|
||||||
|
overviewData[path] = {
|
||||||
|
magnitude: 0,
|
||||||
|
unit: '',
|
||||||
|
min: MAX_NUMBER,
|
||||||
|
max: 0
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
input.forEach((item) => {
|
||||||
|
const csvContent = item.value;
|
||||||
|
|
||||||
|
pathsToSearch.forEach((path) => {
|
||||||
|
if (csvContent && csvContent[path]) {
|
||||||
|
const timestamp = item.time.ticks * 1000;
|
||||||
|
const value = csvContent[path];
|
||||||
|
// const result: { magnitude: number; value: number } = formatPower(
|
||||||
|
// value.value
|
||||||
|
// );
|
||||||
|
if (value.value < overviewData[path].min) {
|
||||||
|
overviewData[path].min = value.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.value > overviewData[path].max) {
|
||||||
|
overviewData[path].max = value.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if (result.magnitude > result.magnitude) {
|
||||||
|
// overviewData[path].magnitude = result.magnitude;
|
||||||
|
// }
|
||||||
|
data[path].push([timestamp, value.value]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
pathsToSearch.forEach((path) => {
|
||||||
|
let value = overviewData[path].max;
|
||||||
|
let negative = false;
|
||||||
|
let magnitude = 0;
|
||||||
|
|
||||||
|
if (value < 0) {
|
||||||
|
value = -value;
|
||||||
|
negative = true;
|
||||||
|
}
|
||||||
|
while (value >= 1000) {
|
||||||
|
value /= 1000;
|
||||||
|
magnitude++;
|
||||||
|
}
|
||||||
|
console.log(path, magnitude);
|
||||||
|
|
||||||
|
overviewData[path].magnitude = prefixes[magnitude];
|
||||||
|
});
|
||||||
|
|
||||||
|
let path = '/Battery/Soc';
|
||||||
|
chartData.soc = [{ name: 'State of Charge', data: data[path] }];
|
||||||
|
|
||||||
|
chartOverview.soc = {
|
||||||
|
unit: '(%)',
|
||||||
|
magnitude: overviewData[path].magnitude,
|
||||||
|
min: 0,
|
||||||
|
max: 100
|
||||||
|
};
|
||||||
|
|
||||||
|
path = '/Battery/Temperature';
|
||||||
|
chartData.temperature = [
|
||||||
|
{ name: 'Battery Temperature:', data: data[path] }
|
||||||
|
];
|
||||||
|
|
||||||
|
chartOverview.temperature = {
|
||||||
|
unit: '(°C)',
|
||||||
|
magnitude: overviewData[path].magnitude,
|
||||||
|
min: overviewData[path].min,
|
||||||
|
max: overviewData[path].max
|
||||||
|
};
|
||||||
|
|
||||||
|
path = '/Battery/Dc/Power';
|
||||||
|
chartData.dcPower = [{ name: 'Battery Power', data: data[path] }];
|
||||||
|
|
||||||
|
//console.log(overviewData[path]);
|
||||||
|
//console.log(data[path]);
|
||||||
|
chartOverview.dcPower = {
|
||||||
|
magnitude: overviewData[path].magnitude,
|
||||||
|
unit: '(' + overviewData[path].magnitude + 'W' + ')',
|
||||||
|
min: overviewData[path].min,
|
||||||
|
max: overviewData[path].max
|
||||||
|
};
|
||||||
|
|
||||||
|
path = '/GridMeter/Ac/Power/Active';
|
||||||
|
chartData.gridPower = [{ name: 'Grid Power', data: data[path] }];
|
||||||
|
|
||||||
|
chartOverview.gridPower = {
|
||||||
|
magnitude: overviewData[path].magnitude,
|
||||||
|
unit: '(' + overviewData[path].magnitude + 'W' + ')',
|
||||||
|
min: overviewData[path].min,
|
||||||
|
max: overviewData[path].max
|
||||||
|
};
|
||||||
|
|
||||||
|
path = '/PvOnDc/Dc/Power';
|
||||||
|
chartData.pvProduction = [{ name: 'Pv Production', data: data[path] }];
|
||||||
|
|
||||||
|
chartOverview.pvProduction = {
|
||||||
|
magnitude: overviewData[path].magnitude,
|
||||||
|
unit: '(' + overviewData[path].magnitude + 'W' + ')',
|
||||||
|
min: overviewData[path].min,
|
||||||
|
max: overviewData[path].max
|
||||||
|
};
|
||||||
|
|
||||||
|
path = '/DcDc/Dc/Link/Voltage';
|
||||||
|
chartData.dcBusVoltage = [{ name: 'DC Bus Voltage', data: data[path] }];
|
||||||
|
|
||||||
|
chartOverview.dcBusVoltage = {
|
||||||
|
magnitude: overviewData[path].magnitude,
|
||||||
|
unit: '(' + overviewData[path].magnitude + 'V' + ')',
|
||||||
|
min: overviewData[path].min,
|
||||||
|
max: overviewData[path].max
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
chartData: chartData,
|
||||||
|
chartOverview: chartOverview
|
||||||
|
};
|
||||||
|
|
||||||
|
//return chartData;
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const subscription = cache.gotData
|
||||||
|
.pipe(
|
||||||
|
startWith(0),
|
||||||
|
throttleTime(200, undefined, { leading: true, trailing: true }),
|
||||||
|
withLatestFrom(times$)
|
||||||
|
)
|
||||||
|
.subscribe(([_, times]) => {
|
||||||
|
const timeSeries = cache.getSeries(times);
|
||||||
|
|
||||||
|
console.log(timeSeries);
|
||||||
|
|
||||||
|
const result: {
|
||||||
|
chartData: chartDataInterface;
|
||||||
|
chartOverview: overviewInterface;
|
||||||
|
} = transformToGraphData(timeSeries);
|
||||||
|
|
||||||
|
setChartData(result.chartData);
|
||||||
|
setChartOverview(result.chartOverview);
|
||||||
|
});
|
||||||
|
return () => subscription.unsubscribe();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderGraphs = () => {
|
||||||
|
return (
|
||||||
|
<Container maxWidth="xl">
|
||||||
|
<Grid container>
|
||||||
|
<Grid item xs={12} md={12}>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="stretch"
|
||||||
|
spacing={3}
|
||||||
|
>
|
||||||
|
<Grid item md={6} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '30px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
Battery SOC (State Of Charge)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
<ReactApexChart
|
||||||
|
options={getChartOptions(chartOverview.soc)}
|
||||||
|
series={chartData.soc}
|
||||||
|
type="area"
|
||||||
|
height={350}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item md={6} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '30px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
Battery Temperature
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
<ReactApexChart
|
||||||
|
options={getChartOptions(chartOverview.temperature)}
|
||||||
|
series={chartData.temperature}
|
||||||
|
type="line"
|
||||||
|
height={350}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="stretch"
|
||||||
|
spacing={3}
|
||||||
|
>
|
||||||
|
<Grid item md={6} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '30px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
Battery Power
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
<ReactApexChart
|
||||||
|
options={getChartOptions(chartOverview.dcPower)}
|
||||||
|
series={chartData.dcPower}
|
||||||
|
type="area"
|
||||||
|
height={350}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item md={6} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '30px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
Grid Power
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
<ReactApexChart
|
||||||
|
options={getChartOptions(chartOverview.gridPower)}
|
||||||
|
series={chartData.gridPower}
|
||||||
|
type="area"
|
||||||
|
height={350}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
direction="row"
|
||||||
|
justifyContent="center"
|
||||||
|
alignItems="stretch"
|
||||||
|
spacing={3}
|
||||||
|
>
|
||||||
|
<Grid item md={6} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '30px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
PV Production
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
<ReactApexChart
|
||||||
|
options={getChartOptions(chartOverview.pvProduction)}
|
||||||
|
series={chartData.pvProduction}
|
||||||
|
type="area"
|
||||||
|
height={350}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
<Grid item md={6} xs={12}>
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
overflow: 'visible',
|
||||||
|
marginTop: '30px',
|
||||||
|
marginBottom: '30px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
marginLeft: '20px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box display="flex" alignItems="center">
|
||||||
|
<Box>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
DC Bus Voltage
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
pt: 3
|
||||||
|
}}
|
||||||
|
></Box>
|
||||||
|
</Box>
|
||||||
|
<ReactApexChart
|
||||||
|
options={getChartOptions(chartOverview.dcBusVoltage)}
|
||||||
|
series={chartData.dcBusVoltage}
|
||||||
|
type="line"
|
||||||
|
height={350}
|
||||||
|
/>
|
||||||
|
</Card>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return <>{renderGraphs()}</>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Overview;
|
|
@ -0,0 +1,73 @@
|
||||||
|
|
||||||
|
.line {
|
||||||
|
overflow: hidden;
|
||||||
|
border-top: none;
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.horizontalLine {
|
||||||
|
position: absolute;
|
||||||
|
overflow: hidden;
|
||||||
|
border-left: none;
|
||||||
|
margin-left: 173px;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dotRight {
|
||||||
|
position: absolute;
|
||||||
|
margin-left: 35px;
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #f7b34d;
|
||||||
|
animation: rightflow 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dotLeft {
|
||||||
|
position: absolute;
|
||||||
|
margin-left: 35px;
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #f7b34d;
|
||||||
|
animation: leftflow 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verticalDotDown {
|
||||||
|
position: absolute;
|
||||||
|
margin-top: 35px;
|
||||||
|
width: 3px;
|
||||||
|
height: 3px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #f7b34d;
|
||||||
|
animation: verticalDownFlow 2s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@keyframes rightflow {
|
||||||
|
0% {
|
||||||
|
left: -35px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: 110%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes leftflow {
|
||||||
|
0% {
|
||||||
|
left: 110px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
left: -35px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes verticalDownFlow {
|
||||||
|
0% {
|
||||||
|
top: -35px;
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
top: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,237 @@
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
Divider,
|
||||||
|
Typography,
|
||||||
|
useTheme
|
||||||
|
} from '@mui/material';
|
||||||
|
import { AvatarWrapper } from 'src/layouts/TabsContainerWrapper';
|
||||||
|
import BatteryCharging60Icon from '@mui/icons-material/BatteryCharging60';
|
||||||
|
import OutletIcon from '@mui/icons-material/Outlet';
|
||||||
|
import SolarPowerIcon from '@mui/icons-material/SolarPower';
|
||||||
|
import PowerInputIcon from '@mui/icons-material/PowerInput';
|
||||||
|
import BoltIcon from '@mui/icons-material/Bolt';
|
||||||
|
import { BoxData } from '../Log/graph.util';
|
||||||
|
import inverterImage from 'src/Resources/images/inverter.png';
|
||||||
|
import acCurrentImage from 'src/Resources/images/ac-current.png';
|
||||||
|
import converterImage from 'src/Resources/images/converter.png';
|
||||||
|
|
||||||
|
export interface TopologyBoxProps {
|
||||||
|
title: string;
|
||||||
|
data?: BoxData;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInt = (value: number) => {
|
||||||
|
return value % 1 === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatPower(value) {
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return 'Invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefixes = ['', 'k', 'M', 'G', 'T'];
|
||||||
|
let magnitude = 0;
|
||||||
|
let negative = false;
|
||||||
|
|
||||||
|
if (value < 0) {
|
||||||
|
value = -value;
|
||||||
|
negative = true;
|
||||||
|
}
|
||||||
|
while (value >= 1000) {
|
||||||
|
value /= 1000;
|
||||||
|
magnitude++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roundedValue = value.toFixed(2);
|
||||||
|
|
||||||
|
return negative === false
|
||||||
|
? `${roundedValue} ${prefixes[magnitude]}`
|
||||||
|
: `-${roundedValue} ${prefixes[magnitude]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TopologyBox(props: TopologyBoxProps) {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
sx={{
|
||||||
|
visibility:
|
||||||
|
props.data && props.data.values[0].value != 0 ? 'visible' : 'hidden',
|
||||||
|
width: '104px',
|
||||||
|
height:
|
||||||
|
props.title === 'Battery'
|
||||||
|
? '165px'
|
||||||
|
: props.title === 'AC Loads' ||
|
||||||
|
props.title === 'DC Loads' ||
|
||||||
|
props.title === 'Pv Inverter' ||
|
||||||
|
props.title === 'Pv DcDc'
|
||||||
|
? '100px'
|
||||||
|
: '150px',
|
||||||
|
backgroundColor:
|
||||||
|
props.title === 'Grid Bus' ||
|
||||||
|
props.title === 'Island Bus' ||
|
||||||
|
props.title === 'DC Link'
|
||||||
|
? '#f7b34d'
|
||||||
|
: 'none',
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<CardContent>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'flex-start',
|
||||||
|
textAlign: 'center'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.title === 'Battery' && (
|
||||||
|
<Typography variant="h5" noWrap>
|
||||||
|
{props.title} (x{props.data.values.length - 4})
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
{props.title != 'Battery' && (
|
||||||
|
<Typography variant="h5" noWrap>
|
||||||
|
{props.title}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{props.title === 'AC-DC' && (
|
||||||
|
<img
|
||||||
|
src={inverterImage}
|
||||||
|
style={{
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
color: 'orange'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{props.title === 'DC Link' && (
|
||||||
|
<PowerInputIcon
|
||||||
|
style={{
|
||||||
|
fontSize: 40,
|
||||||
|
color: 'black',
|
||||||
|
backgroundColor: '#f7b34d',
|
||||||
|
padding: '5px',
|
||||||
|
borderTopLeftRadius: '4px',
|
||||||
|
borderTopRightRadius: '4px'
|
||||||
|
}}
|
||||||
|
></PowerInputIcon>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{props.title === 'DC-DC' && (
|
||||||
|
<img
|
||||||
|
src={converterImage}
|
||||||
|
style={{
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
color: 'orange'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(props.title === 'Grid Bus' || props.title === 'Island Bus') && (
|
||||||
|
<img
|
||||||
|
src={acCurrentImage}
|
||||||
|
style={{
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
backgroundColor: '#f7b34d'
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{props.title != 'AC-DC' &&
|
||||||
|
props.title != 'DC Link' &&
|
||||||
|
props.title != 'DC-DC' && (
|
||||||
|
<AvatarWrapper
|
||||||
|
sx={{
|
||||||
|
display:
|
||||||
|
props.title === 'Grid Bus' || props.title === 'Island Bus'
|
||||||
|
? 'none'
|
||||||
|
: 'flex',
|
||||||
|
marginTop: '4px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(props.title === 'Pv Inverter' ||
|
||||||
|
props.title === 'Pv DcDc') && (
|
||||||
|
<SolarPowerIcon
|
||||||
|
style={{
|
||||||
|
fontSize: 30,
|
||||||
|
color: 'orange',
|
||||||
|
padding: '5px',
|
||||||
|
borderTopLeftRadius: '4px',
|
||||||
|
borderTopRightRadius: '4px'
|
||||||
|
}}
|
||||||
|
></SolarPowerIcon>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{props.title === 'Battery' && (
|
||||||
|
<BatteryCharging60Icon
|
||||||
|
style={{
|
||||||
|
fontSize: 40,
|
||||||
|
color: 'green',
|
||||||
|
padding: '5px',
|
||||||
|
borderTopLeftRadius: '4px',
|
||||||
|
borderTopRightRadius: '4px'
|
||||||
|
}}
|
||||||
|
></BatteryCharging60Icon>
|
||||||
|
)}
|
||||||
|
{(props.title === 'AC Loads' || props.title === 'DC Loads') && (
|
||||||
|
<OutletIcon
|
||||||
|
style={{
|
||||||
|
fontSize: 30,
|
||||||
|
color: 'orange',
|
||||||
|
padding: '5px',
|
||||||
|
borderTopLeftRadius: '4px',
|
||||||
|
borderTopRightRadius: '4px'
|
||||||
|
}}
|
||||||
|
></OutletIcon>
|
||||||
|
)}
|
||||||
|
{props.title === 'Grid' && (
|
||||||
|
<BoltIcon
|
||||||
|
style={{
|
||||||
|
fontSize: 30,
|
||||||
|
color: 'orange',
|
||||||
|
padding: '5px',
|
||||||
|
borderTopLeftRadius: '4px',
|
||||||
|
borderTopRightRadius: '4px'
|
||||||
|
}}
|
||||||
|
></BoltIcon>
|
||||||
|
)}
|
||||||
|
</AvatarWrapper>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{props.data && <Divider sx={{ width: '150%', marginTop: '0px' }} />}
|
||||||
|
{props.data && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
pt: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.data.values.map((boxData, index) => {
|
||||||
|
return (
|
||||||
|
<Typography key={index}>
|
||||||
|
{formatPower(boxData.value)}
|
||||||
|
{boxData.unit}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopologyBox;
|
|
@ -0,0 +1,45 @@
|
||||||
|
import React from 'react';
|
||||||
|
import TopologyBox, { TopologyBoxProps } from './topologyBox';
|
||||||
|
import { Box } from '@mui/material';
|
||||||
|
import './dotsAnimation.css';
|
||||||
|
import TopologyFlow, { TopologyFlowProps } from './topologyFlow';
|
||||||
|
|
||||||
|
interface TopologyColumnProps {
|
||||||
|
topBox?: TopologyBoxProps;
|
||||||
|
centerBox?: TopologyBoxProps;
|
||||||
|
bottomBox?: TopologyBoxProps;
|
||||||
|
topConnection?: TopologyFlowProps;
|
||||||
|
centerConnection?: TopologyFlowProps;
|
||||||
|
bottomConnection?: TopologyFlowProps;
|
||||||
|
isLast: boolean;
|
||||||
|
isFirst: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TopologyColumn(props: TopologyColumnProps) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
marginLeft: props.isFirst ? '0px' : '68px'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<TopologyBox {...props.topBox}></TopologyBox>
|
||||||
|
|
||||||
|
{props.topBox && props.centerBox && (
|
||||||
|
<TopologyFlow {...props.topConnection} />
|
||||||
|
)}
|
||||||
|
<TopologyBox {...props.centerBox}></TopologyBox>
|
||||||
|
{!props.isLast && <TopologyFlow {...props.centerConnection} />}
|
||||||
|
|
||||||
|
{props.centerBox && props.bottomBox && (
|
||||||
|
<TopologyFlow {...props.bottomConnection} />
|
||||||
|
)}
|
||||||
|
<TopologyBox {...props.bottomBox}></TopologyBox>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopologyColumn;
|
|
@ -0,0 +1,190 @@
|
||||||
|
import React, { useEffect, useState } from 'react';
|
||||||
|
import './dotsAnimation.css';
|
||||||
|
import { BoxData } from '../Log/graph.util';
|
||||||
|
import { Box, Typography } from '@mui/material';
|
||||||
|
|
||||||
|
export interface TopologyFlowProps {
|
||||||
|
orientation: string;
|
||||||
|
position?: string;
|
||||||
|
data?: BoxData;
|
||||||
|
amount?: number;
|
||||||
|
showValues: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomStyle() {
|
||||||
|
const animationDelay = `${Math.random() * 2}s`; // Random delay between 0 and 2 seconds
|
||||||
|
const top = `${Math.random() * 100}%`; // Random top position within the container
|
||||||
|
return { animationDelay, top };
|
||||||
|
}
|
||||||
|
|
||||||
|
function getRandomStyleVertical() {
|
||||||
|
const animationDelay = `${Math.random() * 2}s`; // Random delay between 0 and 2 seconds
|
||||||
|
const left = `${Math.random() * 100}%`; // Random top position within the container
|
||||||
|
return { animationDelay, left };
|
||||||
|
}
|
||||||
|
|
||||||
|
const isInt = (value: number) => {
|
||||||
|
return value % 1 === 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
function formatPower(value) {
|
||||||
|
if (isNaN(value)) {
|
||||||
|
return 'Invalid';
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefixes = ['', 'k', 'M', 'G', 'T'];
|
||||||
|
let magnitude = 0;
|
||||||
|
let negative = false;
|
||||||
|
|
||||||
|
if (value < 0) {
|
||||||
|
value = -value;
|
||||||
|
negative = true;
|
||||||
|
}
|
||||||
|
while (value >= 1000) {
|
||||||
|
value /= 1000;
|
||||||
|
magnitude++;
|
||||||
|
}
|
||||||
|
|
||||||
|
const roundedValue = value.toFixed(2);
|
||||||
|
|
||||||
|
return negative === false
|
||||||
|
? `${roundedValue} ${prefixes[magnitude]}`
|
||||||
|
: `-${roundedValue} ${prefixes[magnitude]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function TopologyFlow(props: TopologyFlowProps) {
|
||||||
|
const [dotStyles, setDotStyle] = useState<
|
||||||
|
{ animationDelay: string; top: string }[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const [dotStylesVertical, setDotStyleVertical] = useState<
|
||||||
|
{ animationDelay: string; left: string }[]
|
||||||
|
>([]);
|
||||||
|
|
||||||
|
const numOfDots = 400;
|
||||||
|
const minNumberOfDots = 100;
|
||||||
|
const minHeight = 2;
|
||||||
|
const maxHeight = 70;
|
||||||
|
const minWidth = 2;
|
||||||
|
const maxWidth = 68;
|
||||||
|
|
||||||
|
const desiredNumOfDots =
|
||||||
|
numOfDots * props.amount < minNumberOfDots
|
||||||
|
? minNumberOfDots
|
||||||
|
: numOfDots * props.amount > numOfDots
|
||||||
|
? numOfDots
|
||||||
|
: numOfDots * props.amount;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDotStyle(
|
||||||
|
Array.from({ length: desiredNumOfDots }, () => getRandomStyle())
|
||||||
|
);
|
||||||
|
|
||||||
|
setDotStyleVertical(
|
||||||
|
Array.from({ length: desiredNumOfDots }, () => getRandomStyleVertical())
|
||||||
|
);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{props.data && props.orientation === 'horizontal' && (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: 'absolute',
|
||||||
|
marginLeft: '170px',
|
||||||
|
marginTop: '2px',
|
||||||
|
backgroundColor: 'transparent',
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
zIndex: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.showValues && props.data.values[0].value != 0 && (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
color: 'black',
|
||||||
|
bgcolor: 'background.paper',
|
||||||
|
borderRadius: 5,
|
||||||
|
fontSize: '0.75rem',
|
||||||
|
zIndex: 1
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{formatPower(props.data.values[0].value)}
|
||||||
|
{props.data.values[0].unit}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={props.orientation === 'vertical' ? 'line' : 'horizontalLine'}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
position:
|
||||||
|
props.orientation === 'horizontal' ? 'absolute' : 'relative',
|
||||||
|
|
||||||
|
height:
|
||||||
|
props.orientation === 'horizontal'
|
||||||
|
? `${Math.min(
|
||||||
|
Math.max(props.amount * maxHeight, minHeight),
|
||||||
|
maxHeight
|
||||||
|
)}px`
|
||||||
|
: `${maxHeight}px`,
|
||||||
|
|
||||||
|
width:
|
||||||
|
props.orientation === 'vertical'
|
||||||
|
? `${Math.min(
|
||||||
|
Math.max(props.amount * maxWidth, minWidth),
|
||||||
|
maxWidth
|
||||||
|
)}px`
|
||||||
|
: `${maxWidth}px`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{props.orientation === 'horizontal' &&
|
||||||
|
props.data &&
|
||||||
|
props.data.values[0].value != 0 && (
|
||||||
|
<>
|
||||||
|
<div className="container">
|
||||||
|
{dotStyles.map((style, index) => (
|
||||||
|
<div
|
||||||
|
className={
|
||||||
|
(props.data.values[0].value as number) >= 0
|
||||||
|
? 'dotRight'
|
||||||
|
: 'dotLeft'
|
||||||
|
}
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
animationDelay: style.animationDelay,
|
||||||
|
top: style.top
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{props.orientation === 'vertical' &&
|
||||||
|
props.data &&
|
||||||
|
props.data.values[0].value != 0 && (
|
||||||
|
<div className="dot-container">
|
||||||
|
{dotStylesVertical.map((style, index) => (
|
||||||
|
<div
|
||||||
|
className="verticalDotDown"
|
||||||
|
key={index}
|
||||||
|
style={{
|
||||||
|
animationDelay: style.animationDelay,
|
||||||
|
left: style.left
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TopologyFlow;
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Box, Grid, useTheme } from '@mui/material';
|
||||||
|
import InstallationTree from './InstallationTree';
|
||||||
|
import InstallationsContextProvider from 'src/contexts/InstallationsContextProvider';
|
||||||
|
|
||||||
|
function TreeView() {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InstallationsContextProvider>
|
||||||
|
<Grid item xs={12}>
|
||||||
|
<Box p={4}>
|
||||||
|
<InstallationTree />
|
||||||
|
</Box>
|
||||||
|
</Grid>
|
||||||
|
</InstallationsContextProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TreeView;
|
|
@ -0,0 +1,26 @@
|
||||||
|
import { ApexOptions } from 'apexcharts';
|
||||||
|
|
||||||
|
export interface overviewInterface {
|
||||||
|
soc: chartInfoInterface;
|
||||||
|
temperature: chartInfoInterface;
|
||||||
|
dcPower: chartInfoInterface;
|
||||||
|
gridPower: chartInfoInterface;
|
||||||
|
pvProduction: chartInfoInterface;
|
||||||
|
dcBusVoltage: chartInfoInterface;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface chartInfoInterface {
|
||||||
|
magnitude: number;
|
||||||
|
unit: string;
|
||||||
|
min: number;
|
||||||
|
max: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface chartDataInterface {
|
||||||
|
soc: ApexOptions['series'];
|
||||||
|
temperature: ApexOptions['series'];
|
||||||
|
dcPower: ApexOptions['series'];
|
||||||
|
gridPower: ApexOptions['series'];
|
||||||
|
pvProduction: ApexOptions['series'];
|
||||||
|
dcBusVoltage: ApexOptions['series'];
|
||||||
|
}
|
Loading…
Reference in New Issue