Aggregated data

This commit is contained in:
Noe 2023-12-27 17:19:59 +01:00
parent 91da191874
commit efebd4221b
7 changed files with 918 additions and 329 deletions

View File

@ -22,6 +22,7 @@
"clsx": "1.1.1",
"cytoscape": "^3.26.0",
"date-fns": "^2.28.0",
"dayjs": "^1.11.10",
"history": "5.3.0",
"linq-to-typescript": "^11.0.0",
"nprogress": "0.2.0",
@ -7103,6 +7104,11 @@
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@ -23246,6 +23252,11 @@
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw=="
},
"dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
"integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="
},
"debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",

View File

@ -18,6 +18,7 @@
"clsx": "1.1.1",
"cytoscape": "^3.26.0",
"date-fns": "^2.28.0",
"dayjs": "^1.11.10",
"history": "5.3.0",
"linq-to-typescript": "^11.0.0",
"nprogress": "0.2.0",

View File

@ -5,6 +5,38 @@ import { DataRecord } from 'src/dataCache/data';
import { S3Access } from 'src/dataCache/S3/S3Access';
import { parseCsv } from '../Log/graph.util';
export const fetchDailyData = (
date: string,
s3Credentials?: I_S3Credentials
): Promise<FetchResult<DataRecord>> => {
const s3Path = `${date}.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();
//console.log(parseCsv(text));
return parseCsv(text);
} else {
return Promise.resolve(FetchResult.notAvailable);
}
})
.catch((e) => {
return Promise.resolve(FetchResult.tryLater);
});
}
};
export const fetchData = (
timestamp: UnixTime,
s3Credentials?: I_S3Credentials

View File

@ -159,20 +159,56 @@ export const getChartOptions = (
enabled: true
}
},
yaxis: {
axisBorder: {
show: false
min:
chartInfo.min >= 0
? 0
: chartInfo.max <= 0
? Math.ceil(chartInfo.min / findPower(chartInfo.min).value) *
findPower(chartInfo.min).value
: undefined,
max:
chartInfo.min >= 0
? Math.ceil(chartInfo.max / findPower(chartInfo.max).value) *
findPower(chartInfo.max).value
: chartInfo.max <= 0
? 0
: undefined,
title: {
text: chartInfo.unit,
style: {
fontSize: '12px'
},
axisTicks: {
show: false
offsetY: -160,
offsetX: 25,
rotate: 0
},
labels: {
show: false,
formatter: function (val) {
return val + '%';
formatter: function (value: number) {
return formatPowerForGraph(
value,
Math.max(Math.abs(chartInfo.max), Math.abs(chartInfo.min))
).value.toString();
}
}
}
//
// yaxis: {
// axisBorder: {
// show: false
// },
// axisTicks: {
// show: false
// },
// labels: {
// show: false,
// formatter: function (val) {
// return val + '%';
// }
// }
// }
}
: {
chart: {
@ -189,8 +225,15 @@ export const getChartOptions = (
},
dataLabels: {
enabled: true,
formatter: function (val) {
return val + '%';
formatter: function (val, opts) {
return (
formatPowerForGraph(
val,
Math.max(Math.abs(chartInfo.max), Math.abs(chartInfo.min))
).value.toFixed(2) +
' ' +
chartInfo.unit
);
},
offsetY: -20,
style: {
@ -213,19 +256,66 @@ export const getChartOptions = (
}
},
yaxis: {
axisBorder: {
show: false
min:
chartInfo.min >= 0
? 0
: chartInfo.max <= 0
? Math.ceil(chartInfo.min / findPower(chartInfo.min).value) *
findPower(chartInfo.min).value
: undefined,
max:
chartInfo.min >= 0
? Math.ceil(chartInfo.max / findPower(chartInfo.max).value) *
findPower(chartInfo.max).value
: chartInfo.max <= 0
? 0
: undefined,
title: {
text: chartInfo.unit,
style: {
fontSize: '12px'
},
axisTicks: {
show: false
offsetY: -160,
offsetX: 25,
rotate: 0
},
labels: {
show: false,
formatter: function (val) {
return val + '%';
formatter: function (value: number) {
return formatPowerForGraph(
value,
Math.max(Math.abs(chartInfo.max), Math.abs(chartInfo.min))
).value.toString();
}
}
},
tooltip: {
y: {
formatter: function (val, opts) {
return (
formatPowerForGraph(
val,
Math.max(Math.abs(chartInfo.max), Math.abs(chartInfo.min))
).value.toFixed(2) +
' ' +
chartInfo.unit
);
}
}
}
// yaxis: {
// axisBorder: {
// show: false
// },
// axisTicks: {
// show: false
// },
// labels: {
// show: false,
// formatter: function (val) {
// return val + '%';
// }
// }
// }
};
return chartOptions;

View File

@ -1,302 +1,246 @@
import { Box, Card, Container, Grid, Typography } from '@mui/material';
import ReactApexChart from 'react-apexcharts';
import React, { useEffect, useMemo, useState } from 'react';
import DataCache from 'src/dataCache/dataCache';
import { TimeRange, TimeSpan, UnixTime } from 'src/dataCache/time';
import {
BehaviorSubject,
combineLatest,
startWith,
tap,
throttleTime
} from 'rxjs';
import { RecordSeries } from 'src/dataCache/data';
import { createTimes } from '../Log/graph.util';
import React, { useEffect, useState } from 'react';
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';
import {
chartAggregatedDataInterface,
chartDataInterface,
overviewInterface,
transformInputToAggregatedData,
transformInputToDailyData
} from 'src/interfaces/Chart';
import Button from '@mui/material/Button';
import { FormattedMessage } from 'react-intl';
const prefixes = ['', 'k', 'M', 'G', 'T'];
const MAX_NUMBER = 9999999;
import CircularProgress from '@mui/material/CircularProgress';
import { TimeSpan, UnixTime } from '../../../dataCache/time';
interface OverviewProps {
s3Credentials: I_S3Credentials;
}
function Overview(props: OverviewProps) {
const numOfPointsToFetch = 100;
const [timeRange, setTimeRange] = useState(
createTimes(
UnixTime.now().rangeBefore(TimeSpan.fromDays(1)),
numOfPointsToFetch
)
);
// const numOfPointsToFetch = 100;
// const [timeRange, setTimeRange] = useState(
// createTimes(
// UnixTime.now().rangeBefore(TimeSpan.fromDays(1)),
// numOfPointsToFetch
// )
// );
//
// const cache = useMemo(() => {
// return new DataCache(
// fetchData,
// TimeSpan.fromSeconds(2),
// props.s3Credentials
// );
// }, []);
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 times$ = useMemo(() => new BehaviorSubject(timeRange), []);
const [dailyData, setDailyData] = useState(true);
const [weeklyData, setWeeklyData] = useState(false);
const [monthlyData, setMonthlyData] = useState(false);
const [loading, setLoading] = useState(true);
const [chartState, setChartState] = useState(0);
const transformToGraphData = (
input: RecordSeries
): {
// 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 [dailyDataArray, setDailyDataArray] = useState<
{
chartData: chartDataInterface;
chartOverview: overviewInterface;
} => {
const data = {};
const overviewData = {};
}[]
>([]);
const chartData: chartDataInterface = {
soc: [],
temperature: [],
dcPower: [],
gridPower: [],
pvProduction: [],
dcBusVoltage: []
};
const [chartAggregatedData, setChartAggregatedData] =
useState<chartAggregatedDataInterface>({
soc: [{ data: [] }],
pvProduction: [{ data: [] }],
dcPower: [{ data: [] }]
});
const chartOverview: overviewInterface = {
const [chartAggregatedOverview, setChartAggregatedOverview] =
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 }
};
pathsToSearch.forEach((path) => {
data[path] = [];
overviewData[path] = {
magnitude: 0,
unit: '',
min: MAX_NUMBER,
max: -MAX_NUMBER
};
});
//console.log(input);
input.forEach((item) => {
const csvContent = item.value;
pathsToSearch.forEach((path) => {
if (csvContent) {
const timestamp = item.time.ticks * 1000;
const adjustedTimestamp = new Date(timestamp);
adjustedTimestamp.setHours(adjustedTimestamp.getHours() + 1);
if (csvContent[path]) {
const value = csvContent[path];
if (value.value < overviewData[path].min) {
overviewData[path].min = value.value;
}
if (value.value > overviewData[path].max) {
overviewData[path].max = value.value;
}
data[path].push([adjustedTimestamp, value.value]);
} else {
//data[path].push([adjustedTimestamp, null]);
}
} else {
// data[path].push([
// addHours(new Date(item.time.ticks * 1000), 2),
// null
// ]);
}
});
});
pathsToSearch.forEach((path) => {
let value = Math.max(
Math.abs(overviewData[path].max),
Math.abs(overviewData[path].min)
);
let magnitude = 0;
if (value < 0) {
value = -value;
}
while (value >= 1000) {
value /= 1000;
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] }];
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
};
};
useEffect(() => {
const left = cache.gotData.pipe(
startWith(UnixTime.fromTicks(0)),
throttleTime(200, undefined, { leading: true, trailing: true })
);
const right = times$.pipe(
tap((times) => {
//console.log(times);
})
);
const combined = combineLatest([left, right]);
const subscription = combined.subscribe(([_, times]) => {
const timeSeries = cache.getSeries(times);
const result: {
const resultPromise: Promise<{
chartData: chartDataInterface;
chartOverview: overviewInterface;
} = transformToGraphData(timeSeries);
}> = transformInputToDailyData(
props.s3Credentials,
UnixTime.now().rangeBefore(TimeSpan.fromDays(1)).start,
UnixTime.now()
);
setChartData(result.chartData);
setChartOverview(result.chartOverview);
resultPromise
.then((result) => {
// setChartData(result.chartData);
// setChartOverview(result.chartOverview);
setDailyDataArray((prevData) =>
prevData.concat({
chartData: result.chartData,
chartOverview: result.chartOverview
})
);
setLoading(false);
})
.catch((error) => {
console.error('Error:', error);
});
return () => subscription.unsubscribe();
// const left = cache.gotData.pipe(
// startWith(UnixTime.fromTicks(0)),
// throttleTime(200, undefined, { leading: true, trailing: true })
// );
//
// const right = times$.pipe(
// tap((times) => {
// //console.log(times);
// })
// );
//
// const combined = combineLatest([left, right]);
//
// const subscription = combined.subscribe(([_, times]) => {
// const timeSeries = cache.getSeries(times);
// const result: {
// chartData: chartDataInterface;
// chartOverview: overviewInterface;
// } = transformInputToDailyData(timeSeries);
//
// setChartData(result.chartData);
// setChartOverview(result.chartOverview);
// });
// return () => subscription.unsubscribe();
}, []);
const handleBeforeZoom = (chartContext, { xaxis }) => {
const startX = parseInt(xaxis.min) / 1000;
const endX = parseInt(xaxis.max) / 1000;
const times = createTimes(
TimeRange.fromTimes(UnixTime.fromTicks(startX), UnixTime.fromTicks(endX)),
numOfPointsToFetch
//console.log(UnixTime.fromTicks(startX));
//console.log(endX);
setLoading(true);
const resultPromise: Promise<{
chartData: chartDataInterface;
chartOverview: overviewInterface;
}> = transformInputToDailyData(
props.s3Credentials,
UnixTime.fromTicks(startX),
UnixTime.fromTicks(endX)
);
cache.getSeries(times);
times$.next(times);
};
resultPromise
.then((result) => {
//setChartData(result.chartData);
//setChartOverview(result.chartOverview);
const handleDoubleClick = () => {
const times = createTimes(
UnixTime.now().rangeBefore(TimeSpan.fromDays(1)),
numOfPointsToFetch
setDailyDataArray((prevData) =>
prevData.concat({
chartData: result.chartData,
chartOverview: result.chartOverview
})
);
cache.getSeries(times);
times$.next(times);
setLoading(false);
setChartState(dailyDataArray.length);
})
.catch((error) => {
console.error('Error:', error);
});
// const times = createTimes(
// TimeRange.fromTimes(UnixTime.fromTicks(startX), UnixTime.fromTicks(endX)),
// numOfPointsToFetch
// );
//
// cache.getSeries(times);
// times$.next(times);
};
const handle24HourData = () => {
setDailyData(true);
setWeeklyData(false);
setMonthlyData(false);
const times = createTimes(
UnixTime.now().rangeBefore(TimeSpan.fromDays(1)),
numOfPointsToFetch
);
cache.getSeries(times);
times$.next(times);
// const times = createTimes(
// UnixTime.now().rangeBefore(TimeSpan.fromDays(1)),
// numOfPointsToFetch
// );
// cache.getSeries(times);
// times$.next(times);
// setLoading(true);
// const resultPromise: Promise<{
// chartData: chartDataInterface;
// chartOverview: overviewInterface;
// }> = transformInputToDailyData(
// props.s3Credentials,
// UnixTime.now().rangeBefore(TimeSpan.fromDays(1)).start,
// UnixTime.now()
// );
//
// resultPromise
// .then((result) => {
// //setChartData(result.chartData);
// //setChartOverview(result.chartOverview);
// setDailyDataArray((prevData) =>
// prevData.concat({
// chartData: result.chartData,
// chartOverview: result.chartOverview
// })
// );
// setLoading(false);
// setChartState(dailyDataArray.length);
// })
// .catch((error) => {
// console.error('Error:', error);
// });
setChartState(0);
};
const handleWeekData = () => {
setDailyData(false);
setWeeklyData(true);
setMonthlyData(false);
//fetchData(12312,props.s3Credentials);
const resultPromise: Promise<{
chartAggregatedData: chartAggregatedDataInterface;
chartOverview: overviewInterface;
}> = transformInputToAggregatedData(props.s3Credentials);
resultPromise
.then((result) => {
setChartAggregatedData(result.chartAggregatedData);
setChartAggregatedOverview(result.chartOverview);
})
.catch((error) => {
console.error('Error:', error);
});
// const times = createTimes(
// UnixTime.now().rangeBefore(TimeSpan.fromWeeks(1)),
// numOfPointsToFetch
@ -305,6 +249,18 @@ function Overview(props: OverviewProps) {
// times$.next(times);
};
const handleGoBack = () => {
if (chartState > 0) {
setChartState(chartState - 1);
}
};
const handleGoForward = () => {
if (chartState + 1 < dailyDataArray.length) {
setChartState(chartState + 1);
}
};
const handleMonthData = () => {
setDailyData(false);
setWeeklyData(false);
@ -317,18 +273,32 @@ function Overview(props: OverviewProps) {
// times$.next(times);
};
const series = [
{
name: 'Inflation',
data: [2.3, 3.1, 4.0, 10.1, 4.0, 3.6, 3.2, 2.3, 1.4, 0.8, 0.5, 0.2]
}
];
const renderGraphs = () => {
if (loading) {
// Display a loading indicator while waiting for the data
return (
<Container
maxWidth="xl"
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '100vh'
}}
>
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
<Typography variant="body2" style={{ color: 'black' }} mt={2}>
Fetching data...
</Typography>
</Container>
);
}
return (
<Container maxWidth="xl">
<Grid container>
<Grid item xs={12} md={12}>
<Grid item xs={6} md={6}>
<Button
variant="contained"
onClick={handle24HourData}
@ -368,6 +338,44 @@ function Overview(props: OverviewProps) {
<FormattedMessage id="lastmonth" defaultMessage="Last Month" />
</Button>
</Grid>
<Grid
container
justifyContent="flex-end"
alignItems="center"
item
xs={6}
md={6}
>
<Button
variant="contained"
disabled={!(chartState > 0)}
onClick={handleGoBack}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="goback" defaultMessage="Go Back" />
</Button>
<Button
variant="contained"
disabled={!(chartState < dailyDataArray.length - 1)}
onClick={handleGoForward}
sx={{
marginTop: '20px',
marginLeft: '10px',
backgroundColor: '#ffc04d',
color: '#000000',
'&:hover': { bgcolor: '#f7b34d' }
}}
>
<FormattedMessage id="goback" defaultMessage="Go Forward" />
</Button>
</Grid>
<Grid item xs={12} md={12}>
<Grid
@ -413,14 +421,17 @@ function Overview(props: OverviewProps) {
{dailyData && (
<ReactApexChart
options={{
...getChartOptions(chartOverview.soc, 'daily'),
...getChartOptions(
dailyDataArray[chartState].chartOverview.soc,
'daily'
),
chart: {
events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={chartData.soc}
series={dailyDataArray[chartState].chartData.soc}
type="area"
height={350}
/>
@ -429,9 +440,12 @@ function Overview(props: OverviewProps) {
{weeklyData && (
<ReactApexChart
options={{
...getChartOptions(chartOverview.soc, 'weekly')
...getChartOptions(
chartAggregatedOverview.soc,
'weekly'
)
}}
series={series}
series={chartAggregatedData.soc}
type="bar"
height={350}
/>
@ -440,9 +454,12 @@ function Overview(props: OverviewProps) {
{monthlyData && (
<ReactApexChart
options={{
...getChartOptions(chartOverview.soc, 'monthly')
...getChartOptions(
chartAggregatedOverview.soc,
'monthly'
)
}}
series={series}
series={chartAggregatedData.soc}
type="bar"
height={350}
/>
@ -481,21 +498,22 @@ function Overview(props: OverviewProps) {
}}
></Box>
</Box>
<div onDoubleClick={handleDoubleClick}>
<ReactApexChart
options={{
...getChartOptions(chartOverview.temperature, 'daily'),
...getChartOptions(
dailyDataArray[chartState].chartOverview.temperature,
'daily'
),
chart: {
events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={chartData.temperature}
series={dailyDataArray[chartState].chartData.temperature}
type="area"
height={350}
/>
</div>
</Card>
</Grid>
</Grid>
@ -539,21 +557,53 @@ function Overview(props: OverviewProps) {
}}
></Box>
</Box>
<div onDoubleClick={handleDoubleClick}>
{dailyData && (
<ReactApexChart
options={{
...getChartOptions(chartOverview.pvProduction, 'daily'),
...getChartOptions(
dailyDataArray[chartState].chartOverview.pvProduction,
'daily'
),
chart: {
events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={chartData.pvProduction}
series={dailyDataArray[chartState].chartData.pvProduction}
type="area"
height={350}
/>
</div>
)}
{weeklyData && (
<ReactApexChart
options={{
...getChartOptions(
chartAggregatedOverview.pvProduction,
'weekly'
)
}}
series={chartAggregatedData.pvProduction}
type="bar"
height={350}
/>
)}
{monthlyData && (
<ReactApexChart
options={{
...getChartOptions(
chartAggregatedOverview.pvProduction,
'monthly'
)
}}
series={chartAggregatedData.pvProduction}
type="bar"
height={350}
/>
)}
</Card>
</Grid>
<Grid item md={6} xs={12}>
@ -588,21 +638,22 @@ function Overview(props: OverviewProps) {
}}
></Box>
</Box>
<div onDoubleClick={handleDoubleClick}>
<ReactApexChart
options={{
...getChartOptions(chartOverview.gridPower, 'daily'),
...getChartOptions(
dailyDataArray[chartState].chartOverview.gridPower,
'daily'
),
chart: {
events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={chartData.gridPower}
series={dailyDataArray[chartState].chartData.gridPower}
type="area"
height={350}
/>
</div>
</Card>
</Grid>
</Grid>
@ -645,21 +696,53 @@ function Overview(props: OverviewProps) {
}}
></Box>
</Box>
<div onDoubleClick={handleDoubleClick}>
{dailyData && (
<ReactApexChart
options={{
...getChartOptions(chartOverview.dcPower, 'daily'),
...getChartOptions(
dailyDataArray[chartState].chartOverview.dcPower,
'daily'
),
chart: {
events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={chartData.dcPower}
series={dailyDataArray[chartState].chartData.dcPower}
type="area"
height={350}
/>
</div>
)}
{weeklyData && (
<ReactApexChart
options={{
...getChartOptions(
chartAggregatedOverview.dcPower,
'weekly'
)
}}
series={chartAggregatedData.dcPower}
type="bar"
height={350}
/>
)}
{monthlyData && (
<ReactApexChart
options={{
...getChartOptions(
chartAggregatedOverview.dcPower,
'monthly'
)
}}
series={chartAggregatedData.dcPower}
type="bar"
height={350}
/>
)}
</Card>
</Grid>
<Grid item md={6} xs={12}>
@ -694,21 +777,22 @@ function Overview(props: OverviewProps) {
}}
></Box>
</Box>
<div onDoubleClick={handleDoubleClick}>
<ReactApexChart
options={{
...getChartOptions(chartOverview.dcBusVoltage, 'daily'),
...getChartOptions(
dailyDataArray[chartState].chartOverview.dcBusVoltage,
'daily'
),
chart: {
events: {
beforeZoom: handleBeforeZoom
}
}
}}
series={chartData.dcBusVoltage}
series={dailyDataArray[chartState].chartData.dcBusVoltage}
type="area"
height={350}
/>
</div>
</Card>
</Grid>
</Grid>

View File

@ -4,9 +4,9 @@ import { isDefined } from './utils/maybe';
import { I_CsvEntry } from 'src/content/dashboards/Log/graph.util';
export type DataRecord = Record<string, I_CsvEntry>;
export type DataPoint = Timestamped<Maybe<DataRecord>>;
export type RecordSeries = Array<DataPoint>;
export type DataRecordSeries = Array<DataRecord>;
export type PointSeries = Array<Timestamped<Maybe<I_CsvEntry>>>;
export type DataSeries = Array<Maybe<I_CsvEntry>>;

View File

@ -1,4 +1,12 @@
import { ApexOptions } from 'apexcharts';
import dayjs from 'dayjs';
import {
fetchDailyData,
fetchData
} from '../content/dashboards/Installations/fetchData';
import { FetchResult } from '../dataCache/dataCache';
import { I_S3Credentials } from './S3Types';
import { UnixTime } from '../dataCache/time';
export interface overviewInterface {
soc: chartInfoInterface;
@ -16,6 +24,12 @@ export interface chartInfoInterface {
max: number;
}
export interface chartAggregatedDataInterface {
soc: [{ data: number[] }];
pvProduction: [{ data: number[] }];
dcPower: [{ data: number[] }];
}
export interface chartDataInterface {
soc: ApexOptions['series'];
temperature: ApexOptions['series'];
@ -24,3 +38,360 @@ export interface chartDataInterface {
pvProduction: ApexOptions['series'];
dcBusVoltage: ApexOptions['series'];
}
export const transformInputToDailyData = async (
s3Credentials: I_S3Credentials,
startTimestamp: UnixTime,
endTimestamp: UnixTime
): Promise<{
chartData: chartDataInterface;
chartOverview: overviewInterface;
}> => {
const data = {};
const overviewData = {};
const prefixes = ['', 'k', 'M', 'G', 'T'];
const MAX_NUMBER = 9999999;
const pathsToSearch = [
'/Battery/Soc',
'/Battery/Temperature',
'/Battery/Dc/Power',
'/GridMeter/Ac/Power/Active',
'/PvOnDc/Dc/Power',
'/DcDc/Dc/Link/Voltage'
];
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: -MAX_NUMBER
};
});
// const currentTimestamp = UnixTime.now();
// const twentyFourHoursAgoRange = currentTimestamp.rangeBefore(
// TimeSpan.fromDays(1)
// );
let startTimestampToNum = Number(startTimestamp);
if (startTimestampToNum % 2 != 0) {
startTimestampToNum += 1;
}
let startUnixTime = UnixTime.fromTicks(startTimestampToNum);
let diff = endTimestamp.ticks - startUnixTime.ticks;
//console.log('Current Unix Timestamp:', currentTimestamp);
//console.log('Unix Timestamp 24 hours ago (even):', startTimestamp);
while (startUnixTime < endTimestamp) {
// console.log('Current day:', currentDay.format('YYYY-MM-DD'));
let result = await Promise.resolve(fetchData(startUnixTime, s3Credentials));
if (
result === FetchResult.notAvailable ||
result === FetchResult.tryLater
) {
// Handle not available or try later case
} else {
//console.log('Received data:', result);
// eslint-disable-next-line @typescript-eslint/no-loop-func
pathsToSearch.forEach((path) => {
const timestamp = startUnixTime.ticks * 1000;
const adjustedTimestamp = new Date(timestamp);
adjustedTimestamp.setHours(adjustedTimestamp.getHours() + 1);
if (result[path]) {
const value = result[path];
if (value.value < overviewData[path].min) {
overviewData[path].min = value.value;
}
if (value.value > overviewData[path].max) {
overviewData[path].max = value.value;
}
data[path].push([adjustedTimestamp, value.value]);
} else {
//data[path].push([adjustedTimestamp, null]);
}
});
}
startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + diff / 100);
if (startUnixTime.ticks % 2 != 0) {
startUnixTime = UnixTime.fromTicks(startUnixTime.ticks + 1);
}
console.log('Try next timestamp: ', startUnixTime);
}
//console.log(input);
// input.forEach((item) => {
// const csvContent = item.value;
// pathsToSearch.forEach((path) => {
// if (csvContent) {
// const timestamp = item.time.ticks * 1000;
//
// const adjustedTimestamp = new Date(timestamp);
// adjustedTimestamp.setHours(adjustedTimestamp.getHours() + 1);
//
// if (csvContent[path]) {
// const value = csvContent[path];
//
// if (value.value < overviewData[path].min) {
// overviewData[path].min = value.value;
// }
//
// if (value.value > overviewData[path].max) {
// overviewData[path].max = value.value;
// }
//
// data[path].push([adjustedTimestamp, value.value]);
// } else {
// //data[path].push([adjustedTimestamp, null]);
// }
// } else {
// // data[path].push([
// // addHours(new Date(item.time.ticks * 1000), 2),
// // null
// // ]);
// }
// });
// });
pathsToSearch.forEach((path) => {
let value = Math.max(
Math.abs(overviewData[path].max),
Math.abs(overviewData[path].min)
);
let magnitude = 0;
if (value < 0) {
value = -value;
}
while (value >= 1000) {
value /= 1000;
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] }];
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
};
};
export const transformInputToAggregatedData = async (
s3Credentials: I_S3Credentials
): Promise<{
chartAggregatedData: chartAggregatedDataInterface;
chartOverview: overviewInterface;
}> => {
const data = {};
const overviewData = {};
const prefixes = ['', 'k', 'M', 'G', 'T'];
const MAX_NUMBER = 9999999;
let currentDate = dayjs().add(1, 'day');
let currentDay = currentDate.subtract(1, 'week');
const pathsToSearch = [
'/AvgSoc',
'/AvgPvPower',
'/BatteryPowerAverage',
'/GridMeter/Ac/Power/Active',
'/PvOnDc/Dc/Power',
'/DcDc/Dc/Link/Voltage'
];
const chartAggregatedData: chartAggregatedDataInterface = {
soc: [{ data: [] }],
pvProduction: [{ data: [] }],
dcPower: [{ data: [] }]
};
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: -MAX_NUMBER
};
});
while (currentDay.isBefore(currentDate)) {
// console.log('Current day:', currentDay.format('YYYY-MM-DD'));
let result = await Promise.resolve(
fetchDailyData(currentDay.format('YYYY-MM-DD'), s3Credentials)
);
if (
result === FetchResult.notAvailable ||
result === FetchResult.tryLater
) {
// Handle not available or try later case
} else {
console.log('Received data:', result);
pathsToSearch.forEach((path) => {
if (result[path]) {
if (result[path].value < overviewData[path].min) {
overviewData[path].min = result[path].value;
}
if (result[path].value > overviewData[path].max) {
overviewData[path].max = result[path].value;
}
data[path].push(result[path].value as number);
}
});
}
// Add one day to move to the next day
currentDay = currentDay.add(1, 'day');
}
pathsToSearch.forEach((path) => {
let value = Math.max(
Math.abs(overviewData[path].max),
Math.abs(overviewData[path].min)
);
let magnitude = 0;
if (value < 0) {
value = -value;
}
while (value >= 1000) {
value /= 1000;
magnitude++;
}
overviewData[path].magnitude = prefixes[magnitude];
});
let path = '/AvgSoc';
chartAggregatedData.soc[0].data = data[path];
chartOverview.soc = {
unit: '(%)',
magnitude: overviewData[path].magnitude,
min: 0,
max: 100
};
path = '/AvgPvPower';
chartAggregatedData.pvProduction[0].data = data[path];
chartOverview.pvProduction = {
magnitude: overviewData[path].magnitude,
unit: '(' + overviewData[path].magnitude + 'W' + ')',
min: overviewData[path].min,
max: overviewData[path].max
};
path = '/BatteryPowerAverage';
chartAggregatedData.dcPower[0].data = data[path];
chartOverview.dcPower = {
magnitude: overviewData[path].magnitude,
unit: '(' + overviewData[path].magnitude + 'W' + ')',
min: overviewData[path].min,
max: overviewData[path].max
};
return {
chartAggregatedData: chartAggregatedData,
chartOverview: chartOverview
};
};