updated frontend

This commit is contained in:
Noe 2023-10-09 15:03:27 +02:00
parent 88b79dd398
commit d071f81d27
19 changed files with 1472 additions and 0 deletions

View File

@ -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 */
}

View File

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

View File

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

View File

@ -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"
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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'];
}