Compare commits

..

No commits in common. "e6c32b716234f6e7f94062f882fb72589cd1c2e0" and "b0117e514879999947ed943ae2e6d6e01c44c443" have entirely different histories.

7 changed files with 150 additions and 389 deletions

View File

@ -29,6 +29,7 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs';
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import axiosConfig from '../../../Resources/axiosConfig'; import axiosConfig from '../../../Resources/axiosConfig';
import utc from 'dayjs/plugin/utc'; import utc from 'dayjs/plugin/utc';
import { Action } from '../../../interfaces/S3Types';
import { UserContext } from '../../../contexts/userContext'; import { UserContext } from '../../../contexts/userContext';
interface ConfigurationProps { interface ConfigurationProps {
@ -135,6 +136,14 @@ function Configuration(props: ConfigurationProps) {
.add(localOffset, 'minute') .add(localOffset, 'minute')
.toDate() .toDate()
}; };
const historyAction: Action = {
configuration: configurationToSend,
date: new Date().toISOString().split('T')[0], // Gets the current date in YYYY-MM-DD format
time: new Date().toISOString().split('T')[1].split('.')[0], // Gets the current time in HH:MM:SS format
user: currentUser.name
};
// console.log('will send ', dayjs(formValues.calibrationChargeDate)); // console.log('will send ', dayjs(formValues.calibrationChargeDate));
setLoading(true); setLoading(true);
@ -151,8 +160,21 @@ function Configuration(props: ConfigurationProps) {
}); });
if (res) { if (res) {
setUpdated(true); const historyRes = await axiosConfig
setLoading(false); .post(
`/UpdateActionHistory?installationId=${props.id}`,
historyAction
)
.catch((err) => {
if (err.response) {
setError(true);
setLoading(false);
}
});
if (historyRes) {
setUpdated(true);
setLoading(false);
}
} }
} }
}; };

View File

@ -122,135 +122,102 @@ function HistoryOfActions(props: HistoryProps) {
<FormattedMessage id="time" defaultMessage="Time" /> <FormattedMessage id="time" defaultMessage="Time" />
</Typography> </Typography>
</div> </div>
<div {/*<div*/}
style={{ {/* style={{*/}
flex: 1, {/* flex: 1,*/}
marginTop: '15px', {/* marginTop: '15px',*/}
display: 'flex', {/* display: 'flex',*/}
alignItems: 'center', {/* alignItems: 'center',*/}
justifyContent: 'center' {/* justifyContent: 'center'*/}
}} {/* }}*/}
> {/*>*/}
<Typography {/* <Typography*/}
variant="body1" {/* variant="body1"*/}
color="dimgrey" {/* color="dimgrey"*/}
fontWeight="bold" {/* fontWeight="bold"*/}
fontSize="1rem" {/* fontSize="1rem"*/}
gutterBottom {/* gutterBottom*/}
noWrap {/* noWrap*/}
> {/* >*/}
<FormattedMessage {/* <FormattedMessage id="seen" defaultMessage="Seen" />*/}
id="description" {/* </Typography>*/}
defaultMessage="Description" {/*</div>*/}
/>
</Typography>
</div>
</div> </div>
<Divider /> <Divider />
<div style={{ maxHeight: '400px', overflowY: 'auto' }}> <div style={{ maxHeight: '400px', overflowY: 'auto' }}>
{history.map((action, index) => { {history.map((action, index) => (
// Parse the timestamp string to a Date object <>
const date = new Date(action.timestamp); <Divider />
<div
// Extract the date part (e.g., "2023-05-31") key={index}
const datePart = date.toLocaleDateString(); style={{
height: '40px',
// Extract the time part (e.g., "12:34:56") marginBottom: '10px',
const timePart = date.toLocaleTimeString(); display: 'flex',
return ( alignItems: 'center'
<> }}
<Divider /> >
<div <div
key={index}
style={{ style={{
height: '40px', flex: 1,
marginBottom: '10px', marginTop: '15px',
display: 'flex', display: 'flex',
alignItems: 'center' alignItems: 'center',
justifyContent: 'center'
}} }}
> >
<div <Typography
style={{ variant="body1"
flex: 1, fontWeight="bold"
marginTop: '15px', color="text.primary"
display: 'flex', gutterBottom
alignItems: 'center', noWrap
justifyContent: 'center'
}}
> >
<Typography {action.user}
variant="body1" </Typography>
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
>
{action.userName}
</Typography>
</div>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
>
{datePart}
</Typography>
</div>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
>
{timePart}
</Typography>
</div>
<div
style={{
flex: 3,
display: 'flex',
marginTop: '15px',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
>
{action.description}
</Typography>
</div>
</div> </div>
</>
); <div
})} style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
>
{action.date}
</Typography>
</div>
<div
style={{
flex: 1,
marginTop: '15px',
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}
>
<Typography
variant="body1"
fontWeight="bold"
color="text.primary"
gutterBottom
noWrap
>
{action.time}
</Typography>
</div>
</div>
</>
))}
</div> </div>
</div> </div>
</Card> </Card>

View File

@ -24,7 +24,6 @@ import Information from '../Information/Information';
import BatteryView from '../BatteryView/BatteryView'; import BatteryView from '../BatteryView/BatteryView';
import { UserType } from '../../../interfaces/UserTypes'; import { UserType } from '../../../interfaces/UserTypes';
import HistoryOfActions from '../History/History'; import HistoryOfActions from '../History/History';
import PvView from '../PvView/PvView';
interface singleInstallationProps { interface singleInstallationProps {
current_installation?: I_Installation; current_installation?: I_Installation;
@ -125,7 +124,6 @@ function Installation(props: singleInstallationProps) {
useEffect(() => { useEffect(() => {
if ( if (
currentTab == 'live' || currentTab == 'live' ||
currentTab == 'pvview' ||
currentTab == 'configuration' || currentTab == 'configuration' ||
location.includes('batteryview') location.includes('batteryview')
) { ) {
@ -133,8 +131,7 @@ function Installation(props: singleInstallationProps) {
if ( if (
currentTab == 'live' || currentTab == 'live' ||
(location.includes('batteryview') && !location.includes('mainstats')) || (location.includes('batteryview') && !location.includes('mainstats'))
currentTab == 'pvview'
) { ) {
fetchDataPeriodically(); fetchDataPeriodically();
interval = setInterval(fetchDataPeriodically, 2000); interval = setInterval(fetchDataPeriodically, 2000);
@ -147,7 +144,6 @@ function Installation(props: singleInstallationProps) {
return () => { return () => {
if ( if (
currentTab == 'live' || currentTab == 'live' ||
currentTab == 'pvview' ||
(location.includes('batteryview') && !location.includes('mainstats')) (location.includes('batteryview') && !location.includes('mainstats'))
) { ) {
clearInterval(interval); clearInterval(interval);
@ -318,7 +314,13 @@ function Installation(props: singleInstallationProps) {
<Route <Route
path={routes.pvview + '*'} path={routes.pvview + '*'}
element={ element={
<PvView values={values} connected={connected}></PvView> <PvView
values={values}
s3Credentials={s3Credentials}
installationId={props.current_installation.id}
productNum={props.current_installation.product}
connected={connected}
></PvView>
} }
></Route> ></Route>

View File

@ -140,15 +140,15 @@ function InstallationTabs() {
/> />
) )
}, },
{ // {
value: 'history', // value: 'history',
label: ( // label: (
<FormattedMessage // <FormattedMessage
id="history" // id="history"
defaultMessage="History Of Actions" // defaultMessage="History Of Actions"
/> // />
) // )
}, // },
{ {
value: 'pvview', value: 'pvview',
label: <FormattedMessage id="pvview" defaultMessage="Pv View" /> label: <FormattedMessage id="pvview" defaultMessage="Pv View" />
@ -271,16 +271,16 @@ function InstallationTabs() {
defaultMessage="Configuration" defaultMessage="Configuration"
/> />
) )
},
{
value: 'history',
label: (
<FormattedMessage
id="history"
defaultMessage="History Of Actions"
/>
)
} }
// {
// value: 'history',
// label: (
// <FormattedMessage
// id="history"
// defaultMessage="History Of Actions"
// />
// )
// }
] ]
: currentUser.userType == UserType.partner : currentUser.userType == UserType.partner
? [ ? [

View File

@ -35,12 +35,7 @@ export type ConfigurationValues = {
calibrationChargeDate: Date | null; calibrationChargeDate: Date | null;
}; };
export interface Pv { export interface Pv {}
PvId: number;
Power: I_BoxDataValue;
Voltage: I_BoxDataValue;
Current: I_BoxDataValue;
}
export interface Battery { export interface Battery {
BatteryId: number; BatteryId: number;
@ -86,8 +81,6 @@ export interface Battery {
MaxDischargePower: I_BoxDataValue; MaxDischargePower: I_BoxDataValue;
} }
const PvKeys = ['PvId', 'Power', 'Voltage', 'Current'];
const BatteryKeys = [ const BatteryKeys = [
'BatteryId', 'BatteryId',
'FwVersion', 'FwVersion',
@ -179,15 +172,10 @@ type TopologyPaths = { [key in keyof TopologyValues]: string[] };
const batteryIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; const batteryIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const pvIds = [
1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22,
23, 24, 25, 26, 27, 28, 29, 30
];
const PvPaths = [ const PvPaths = [
'/PvOnDc/Strings/%id%/Power',
'/PvOnDc/Strings/%id%/Voltage', '/PvOnDc/Strings/%id%/Voltage',
'/PvOnDc/Strings/%id%/Current' '/PvOnDc/Strings/%id%/Current',
'/PvOnDc/Strings/%id%/Power'
]; ];
const batteryPaths = [ const batteryPaths = [
@ -312,7 +300,7 @@ export const topologyPaths: TopologyPaths = {
batteryPaths.map((path) => path.replace('%id%', id.toString())) batteryPaths.map((path) => path.replace('%id%', id.toString()))
), ),
pvView: pvIds.flatMap((id) => pvView: batteryIds.flatMap((id) =>
PvPaths.map((path) => path.replace('%id%', id.toString())) PvPaths.map((path) => path.replace('%id%', id.toString()))
), ),
@ -343,47 +331,15 @@ export const extractValues = (
timeSeriesData: DataPoint timeSeriesData: DataPoint
): TopologyValues | null => { ): TopologyValues | null => {
const extractedValues: TopologyValues = {} as TopologyValues; const extractedValues: TopologyValues = {} as TopologyValues;
// console.log('timeSeriesData=', timeSeriesData);
for (const topologyKey of Object.keys(topologyPaths)) { for (const topologyKey of Object.keys(topologyPaths)) {
//Each topologykey may have more than one paths (for example inverter) //Each topologykey may have more than one paths (for example inverter)
const paths = topologyPaths[topologyKey]; const paths = topologyPaths[topologyKey];
let topologyValues: { unit: string; value: string | number }[] = []; let topologyValues: { unit: string; value: string | number }[] = [];
if (topologyKey === 'pvView') { if (topologyKey === 'batteryView') {
extractedValues[topologyKey] = [];
let pv_index = 0;
let pathIndex = 0;
while (pathIndex < paths.length) {
let pv = {};
let existingKeys = 0;
//We prepare a pv object for each node. We extract the number of nodes from the '/PvOnDc/NbrOfStrings' path.
//PvKeys[0] is the pv id.
pv[PvKeys[0]] = pv_index;
//Then, search all the remaining battery keys
for (let i = 1; i < PvKeys.length; i++) {
const path = paths[pathIndex];
if (timeSeriesData.value.hasOwnProperty(path)) {
existingKeys++;
pv[PvKeys[i]] = {
unit: timeSeriesData.value[path].unit.includes('~')
? timeSeriesData.value[path].unit.replace('~', '')
: timeSeriesData.value[path].unit,
value:
typeof timeSeriesData.value[path].value === 'string'
? timeSeriesData.value[path].value
: Number(timeSeriesData.value[path].value).toFixed(1)
};
}
pathIndex++;
}
pv_index++;
if (existingKeys > 0) {
extractedValues[topologyKey].push(pv as Pv);
}
}
} else if (topologyKey === 'batteryView') {
extractedValues[topologyKey] = []; extractedValues[topologyKey] = [];
const node_ids_from_csv = timeSeriesData.value[ const node_ids_from_csv = timeSeriesData.value[
'/Config/Devices/BatteryNodes' '/Config/Devices/BatteryNodes'

View File

@ -1,187 +0,0 @@
import React, { useEffect, useState } from 'react';
import {
Container,
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography
} from '@mui/material';
import { TopologyValues } from '../Log/graph.util';
import { useLocation, useNavigate } from 'react-router-dom';
import routes from '../../../Resources/routes.json';
import CircularProgress from '@mui/material/CircularProgress';
interface PvViewProps {
values: TopologyValues;
connected: boolean;
}
function PvView(props: PvViewProps) {
if (props.values === null && props.connected == true) {
return null;
}
const currentLocation = useLocation();
const navigate = useNavigate();
const sortedPvView =
props.values != null
? [...props.values.pvView].sort((a, b) => a.PvId - b.PvId)
: [];
const [loading, setLoading] = useState(sortedPvView.length == 0);
const handleMainStatsButton = () => {
navigate(routes.mainstats);
};
const findBatteryData = (batteryId: number) => {
for (let i = 0; i < props.values.batteryView.length; i++) {
if (props.values.batteryView[i].BatteryId == batteryId) {
return props.values.batteryView[i];
}
}
};
useEffect(() => {
if (sortedPvView.length == 0) {
setLoading(true);
} else {
setLoading(false);
}
}, [sortedPvView]);
return (
<>
{!props.connected && (
<Container
maxWidth="xl"
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '70vh'
}}
>
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
<Typography
variant="body2"
style={{ color: 'black', fontWeight: 'bold' }}
mt={2}
>
Unable to communicate with the installation
</Typography>
<Typography variant="body2" style={{ color: 'black' }}>
Please wait or refresh the page
</Typography>
</Container>
)}
{loading && props.connected && (
<Container
maxWidth="xl"
sx={{
display: 'flex',
flexDirection: 'column',
justifyContent: 'center',
alignItems: 'center',
height: '70vh'
}}
>
<CircularProgress size={60} style={{ color: '#ffc04d' }} />
<Typography
variant="body2"
style={{ color: 'black', fontWeight: 'bold' }}
mt={2}
>
Battery service is not available at the moment
</Typography>
<Typography variant="body2" style={{ color: 'black' }}>
Please wait or refresh the page
</Typography>
</Container>
)}
{!loading && props.connected && (
<Container maxWidth="xl">
<TableContainer
component={Paper}
sx={{
marginTop: '20px',
marginBottom: '20px'
}}
>
<Table sx={{ minWidth: 250 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="center">Pv</TableCell>
<TableCell align="center">Power</TableCell>
<TableCell align="center">Voltage</TableCell>
<TableCell align="center">Current</TableCell>
</TableRow>
</TableHead>
<TableBody>
{sortedPvView.map((pv) => (
<TableRow
key={pv.PvId}
style={{
height: '10px'
}}
>
<TableCell
component="th"
scope="row"
align="center"
sx={{ width: '10%', fontWeight: 'bold', color: 'black' }}
>
{'String ' + pv.PvId}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
backgroundColor:
pv.Current.value == 0 ? '#FF033E' : '#32CD32',
color: pv.Power.value === '' ? 'white' : 'inherit'
}}
>
{pv.Power.value + ' ' + pv.Power.unit}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
backgroundColor:
pv.Current.value == 0 ? '#FF033E' : '#32CD32',
color: pv.Voltage.value === '' ? 'white' : 'inherit'
}}
>
{pv.Voltage.value + ' ' + pv.Voltage.unit}
</TableCell>
<TableCell
sx={{
width: '10%',
textAlign: 'center',
backgroundColor:
pv.Current.value == 0 ? '#FF033E' : '#32CD32',
color: pv.Current.value === '' ? 'white' : 'inherit'
}}
>
{pv.Current.value + ' ' + pv.Current.unit}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</Container>
)}
</>
);
}
export default PvView;

View File

@ -1,3 +1,5 @@
import { ConfigurationValues } from '../content/dashboards/Log/graph.util';
export interface I_S3Credentials { export interface I_S3Credentials {
s3Region: string; s3Region: string;
s3Provider: string; s3Provider: string;
@ -18,9 +20,8 @@ export interface ErrorMessage {
} }
export interface Action { export interface Action {
id: number; configuration: ConfigurationValues;
userName: string; date: string;
installationId: number; time: string;
timestamp: string; user: string;
description: String;
} }