From 0d8978c6b0595e2bcbe6d3c739aea66297bf8b00 Mon Sep 17 00:00:00 2001 From: Noe Date: Mon, 1 Sep 2025 12:59:01 +0200 Subject: [PATCH 1/4] Update frontend. Added Features: SodioHome installations, User Access --- typescript/frontend-marios2/deploy.sh | 4 +- typescript/frontend-marios2/deploy_stage.sh | 2 +- typescript/frontend-marios2/public/Logo.svg | 1 + typescript/frontend-marios2/public/index.html | 4 +- typescript/frontend-marios2/src/App.tsx | 2 +- .../frontend-marios2/src/Resources/Logo.svg | 1 + .../src/Resources/axiosConfig.tsx | 4 +- .../src/components/Footer/index.tsx | 6 +- .../src/components/ForgotPassword.tsx | 23 +- .../src/components/ResetPassword.tsx | 19 +- .../src/components/SetNewPassword.tsx | 19 +- .../frontend-marios2/src/components/login.tsx | 22 +- .../dashboards/BatteryView/BatteryView.tsx | 8 +- .../BatteryView/BatteryViewSodioHome.tsx | 333 ++++++++++ .../DetailedBatteryViewSodistore.tsx | 44 +- .../Configuration/Configuration.tsx | 609 +++++++++++++----- .../dashboards/Information/Information.tsx | 33 +- .../Information/InformationSodioHome.tsx | 393 ----------- .../Installations/FlatInstallationView.tsx | 1 - .../Installations/installationForm.tsx | 5 +- .../src/content/dashboards/Log/graph.util.tsx | 123 +++- .../dashboards/ManageAccess/UserAccess.tsx | 499 ++++++++++++++ .../src/content/dashboards/PvView/PvView.tsx | 178 +++-- .../FlatInstallationView.tsx | 2 +- .../SodiohomeInstallations/Installation.tsx | 173 +++-- .../SodiohomeInstallationForm.tsx | 286 -------- .../SodiohomeInstallations/index.tsx | 97 +-- .../content/dashboards/Topology/Topology.tsx | 17 +- .../content/dashboards/Tree/Information.tsx | 35 +- .../dashboards/Users/FlatUsersView.tsx | 17 +- .../src/content/dashboards/Users/User.tsx | 13 +- .../src/content/dashboards/Users/userForm.tsx | 39 +- .../src/contexts/AccessContextProvider.tsx | 29 + .../contexts/InstallationsContextProvider.tsx | 9 +- .../frontend-marios2/src/interfaces/Chart.tsx | 40 +- .../Sidebar/SidebarMenu/index.tsx | 2 +- .../layouts/SidebarLayout/Sidebar/index.tsx | 29 +- .../src/layouts/SidebarLayout/index.tsx | 2 +- .../src/theme/schemes/PureLightTheme.ts | 2 +- 39 files changed, 1906 insertions(+), 1219 deletions(-) create mode 100644 typescript/frontend-marios2/public/Logo.svg create mode 100644 typescript/frontend-marios2/src/Resources/Logo.svg create mode 100644 typescript/frontend-marios2/src/content/dashboards/BatteryView/BatteryViewSodioHome.tsx delete mode 100644 typescript/frontend-marios2/src/content/dashboards/Information/InformationSodioHome.tsx create mode 100644 typescript/frontend-marios2/src/content/dashboards/ManageAccess/UserAccess.tsx delete mode 100644 typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/SodiohomeInstallationForm.tsx diff --git a/typescript/frontend-marios2/deploy.sh b/typescript/frontend-marios2/deploy.sh index 0c030cdff..427dc8ce2 100755 --- a/typescript/frontend-marios2/deploy.sh +++ b/typescript/frontend-marios2/deploy.sh @@ -1,4 +1,4 @@ -npm run build && rsync -rv .* ubuntu@194.182.190.208:~/frontend/ && ssh ubuntu@194.182.190.208 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.innov.energy/html/' && ssh ubuntu@194.182.190.208 'sudo npm install -g serve' +npm run build && rsync -rv .* ubuntu@194.182.190.208:~/frontend/ && ssh ubuntu@194.182.190.208 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.inesco.energy/html/' && ssh ubuntu@194.182.190.208 'sudo npm install -g serve' -#npm run build && rsync -rv .* ubuntu@91.92.154.141:~/frontend/ && ssh ubuntu@91.92.154.141 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.innov.energy/html/' && ssh ubuntu@91.92.154.141 'sudo npm install -g serve' +#npm run build && rsync -rv .* ubuntu@91.92.154.141:~/frontend/ && ssh ubuntu@91.92.154.141 'sudo cp -rf ~/frontend/build/* /var/www/html/monitor.inesco.energy/html/' && ssh ubuntu@91.92.154.141 'sudo npm install -g serve' diff --git a/typescript/frontend-marios2/deploy_stage.sh b/typescript/frontend-marios2/deploy_stage.sh index d894cc979..5dfea58ad 100755 --- a/typescript/frontend-marios2/deploy_stage.sh +++ b/typescript/frontend-marios2/deploy_stage.sh @@ -1 +1 @@ -npm run build && rsync -rv .* ubuntu@91.92.154.141:~/frontend/ && ssh ubuntu@91.92.154.141 'sudo cp -rf ~/frontend/build/* /var/www/html/stage.innov.energy/html/' && ssh ubuntu@91.92.154.141 'sudo npm install -g serve' +npm run build && rsync -rv .* ubuntu@91.92.154.141:~/frontend/ && ssh ubuntu@91.92.154.141 'sudo cp -rf ~/frontend/build/* /var/www/html/stage.inesco.energy/html/' && ssh ubuntu@91.92.154.141 'sudo npm install -g serve' diff --git a/typescript/frontend-marios2/public/Logo.svg b/typescript/frontend-marios2/public/Logo.svg new file mode 100644 index 000000000..08220ee5f --- /dev/null +++ b/typescript/frontend-marios2/public/Logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/typescript/frontend-marios2/public/index.html b/typescript/frontend-marios2/public/index.html index ee76d7b15..92bfa63d2 100644 --- a/typescript/frontend-marios2/public/index.html +++ b/typescript/frontend-marios2/public/index.html @@ -2,7 +2,7 @@ - + - InnovEnergy + Inesco Energy diff --git a/typescript/frontend-marios2/src/App.tsx b/typescript/frontend-marios2/src/App.tsx index 50c862604..d90d7a868 100644 --- a/typescript/frontend-marios2/src/App.tsx +++ b/typescript/frontend-marios2/src/App.tsx @@ -186,7 +186,7 @@ function App() { path={routes.sodiohome_installations + '*'} element={ - + } /> diff --git a/typescript/frontend-marios2/src/Resources/Logo.svg b/typescript/frontend-marios2/src/Resources/Logo.svg new file mode 100644 index 000000000..08220ee5f --- /dev/null +++ b/typescript/frontend-marios2/src/Resources/Logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/typescript/frontend-marios2/src/Resources/axiosConfig.tsx b/typescript/frontend-marios2/src/Resources/axiosConfig.tsx index 0d4606588..97dec2ca8 100644 --- a/typescript/frontend-marios2/src/Resources/axiosConfig.tsx +++ b/typescript/frontend-marios2/src/Resources/axiosConfig.tsx @@ -1,12 +1,12 @@ import axios from 'axios'; export const axiosConfigWithoutToken = axios.create({ - baseURL: 'https://monitor.innov.energy/api' + baseURL: 'https://monitor.inesco.energy/api' //baseURL: 'http://127.0.0.1:7087/api' }); const axiosConfig = axios.create({ - baseURL: 'https://monitor.innov.energy/api' + baseURL: 'https://monitor.inesco.energy/api' //baseURL: 'http://127.0.0.1:7087/api' }); diff --git a/typescript/frontend-marios2/src/components/Footer/index.tsx b/typescript/frontend-marios2/src/components/Footer/index.tsx index 239c9a31e..981ece646 100644 --- a/typescript/frontend-marios2/src/components/Footer/index.tsx +++ b/typescript/frontend-marios2/src/components/Footer/index.tsx @@ -18,7 +18,7 @@ function Footer() { > - © 2024 - InnovEnergy AG + © 2025 - Inesco Energy AG Crafted by{' '} - InnovEnergy AG + Inesco Energy AG diff --git a/typescript/frontend-marios2/src/components/ForgotPassword.tsx b/typescript/frontend-marios2/src/components/ForgotPassword.tsx index c44a4325c..c53ffe8e5 100644 --- a/typescript/frontend-marios2/src/components/ForgotPassword.tsx +++ b/typescript/frontend-marios2/src/components/ForgotPassword.tsx @@ -10,7 +10,8 @@ import { Typography, useTheme } from '@mui/material'; -import innovenergyLogo from 'src/Resources/innoveng_logo_on_orange.png'; +import inescologo from 'src/Resources/Logo.svg'; + import { UserContext } from 'src/contexts/userContext'; import { TokenContext } from 'src/contexts/tokenContext'; import Avatar from '@mui/material/Avatar'; @@ -76,8 +77,8 @@ function ForgotPassword() { - - innovenergy logo + + inesco logo @@ -100,7 +101,7 @@ function ForgotPassword() { transform: 'translate(-50%, -50%)' }} > - + @@ -134,15 +135,15 @@ function ForgotPassword() { }} /> - {loading && } + {loading && } + + + + + + {/**/} + {/* */} + {/* */} + {/* }*/} + {/* />*/} + {/* {product === 0*/} + {/* ? Object.entries(props.values.Battery.Devices).map(*/} + {/* ([BatteryId, battery]) => (*/} + {/* */} + {/* }*/} + {/* />*/} + {/* )*/} + {/* )*/} + {/* : Object.entries(props.values.Battery.Devices).map(*/} + {/* ([BatteryId, battery]) => (*/} + {/* */} + {/* }*/} + {/* />*/} + {/* )*/} + {/* )}*/} + {/* */} + {/**/} + + + + + + Battery + Power + Battery Voltage + Current + SoC + SoH + Daily Charge Energy + Daily Discharge Energy + + + + {sortedBatteryView.map(({ BatteryId, battery }) => ( + + + + {'Battery Cluster ' + BatteryId} + + + + {battery.Power + ' ' + 'W'} + + + {battery.Voltage + ' ' + 'V'} + + + {battery.Current + ' A'} + + + {battery.Soc + ' %'} + + + {battery.Soh + ' %'} + + + {battery.DailyChargeEnergy + ' Wh'} + + + {battery.DailyDischargeEnergy + ' Wh'} + + + ))} + +
+
+
+ )} + + ); +} + +export default BatteryViewSodioHome; diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/DetailedBatteryViewSodistore.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/DetailedBatteryViewSodistore.tsx index bcbd57c8a..b27545262 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/DetailedBatteryViewSodistore.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/DetailedBatteryViewSodistore.tsx @@ -169,7 +169,7 @@ function DetailedBatteryViewSodistore( align="left" sx={{ fontWeight: 'bold' }} > - Total Battery Voltage + Bus Voltage + + + + Battery Voltage + + + {props.batteryData.BatteryDeligreenDataRecord + .TotalBatteryVoltage + ' V'} + + + - - - - Port Voltage - - - diff --git a/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx index 7fe378aa9..229c07552 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx @@ -11,10 +11,13 @@ import { InputLabel, Modal, Select, + Tab, + Tabs, TextField, Typography, useTheme } from '@mui/material'; + import React, { useContext, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import Button from '@mui/material/Button'; @@ -29,6 +32,7 @@ import { UserContext } from '../../../contexts/userContext'; import { DateTimePicker } from '@mui/x-date-pickers/DateTimePicker'; import { TimePicker } from '@mui/lab'; +import { ProductIdContext } from '../../../contexts/ProductIdContextProvider'; interface ConfigurationProps { values: JSONRecordData; @@ -40,6 +44,9 @@ function Configuration(props: ConfigurationProps) { return null; } + //console.log(props.values.Config); + const [activeTab, setActiveTab] = useState<'charge' | 'discharge'>('charge'); + const CalibrationChargeOptions = [ 'Repetitive Calibration', 'Additional Calibration', @@ -84,11 +91,12 @@ function Configuration(props: ConfigurationProps) { const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false); const context = useContext(UserContext); const { currentUser, setUser } = context; + const { product, setProduct } = useContext(ProductIdContext); const [formValues, setFormValues] = useState({ minimumSoC: props.values.Config.MinSoc, gridSetPoint: (props.values.Config.GridSetPoint as number) / 1000, - CalibrationChargeState: CalibrationChargeOptionsController.indexOf( + calibrationChargeState: CalibrationChargeOptionsController.indexOf( props.values.Config.ForceCalibrationChargeState.toString() ), calibrationChargeDate: @@ -100,9 +108,27 @@ function Configuration(props: ConfigurationProps) { .toDate() : dayjs(props.values.Config.DayAndTimeForAdditionalCalibration) // .add(localOffset, 'minute') - .toDate() + .toDate(), + + ...(product === 3 && { + calibrationDischargeState: CalibrationChargeOptionsController.indexOf( + props.values.Config.ForceCalibrationDischargeState.toString() + ), + calibrationDischargeDate: + CalibrationChargeOptionsController.indexOf( + props.values.Config.ForceCalibrationDischargeState.toString() + ) == 0 + ? dayjs( + props.values.Config.DownDayAndTimeForRepetitiveCalibration + ).toDate() + : dayjs( + props.values.Config.DownDayAndTimeForAdditionalCalibration + ).toDate() + }) }); + // console.log(formValues); + const handleSubmit = async (e) => { if ( CalibrationChargeOptionsController.indexOf( @@ -116,7 +142,7 @@ function Configuration(props: ConfigurationProps) { setErrorDateModalOpen(true); return; } else if ( - formValues.CalibrationChargeState === 1 && + formValues.calibrationChargeState === 1 && dayjs(formValues.calibrationChargeDate).isBefore(dayjs()) ) { //console.log('asked for', dayjs(formValues.calibrationChargeDate)); @@ -128,13 +154,17 @@ function Configuration(props: ConfigurationProps) { const configurationToSend: ConfigurationValues = { minimumSoC: formValues.minimumSoC, gridSetPoint: formValues.gridSetPoint, - CalibrationChargeState: formValues.CalibrationChargeState, + calibrationChargeState: formValues.calibrationChargeState, calibrationChargeDate: dayjs .utc(formValues.calibrationChargeDate) .add(localOffset, 'minute') + .toDate(), + calibrationDischargeState: formValues.calibrationDischargeState, + calibrationDischargeDate: dayjs + .utc(formValues.calibrationDischargeDate) + .add(localOffset, 'minute') .toDate() }; - // console.log('will send ', dayjs(formValues.calibrationChargeDate)); setLoading(true); const res = await axiosConfig @@ -169,6 +199,15 @@ function Configuration(props: ConfigurationProps) { }); }; + const handleConfirmDischarge = (newDate) => { + //console.log('non adapted day is ', newDate); + //console.log('adapted day is ', dayjs.utc(newDate).toDate()); + setFormValues({ + ...formValues, + ['calibrationDischargeDate']: dayjs(newDate).toDate() + }); + }; + const handleSelectedCalibrationChargeDay = (event) => { const selectedDay = daysInWeek.indexOf(event.target.value); const currentDate = dayjs(); @@ -184,10 +223,25 @@ function Configuration(props: ConfigurationProps) { }); }; + const handleSelectedCalibrationDisChargeDay = (event) => { + const selectedDay = daysInWeek.indexOf(event.target.value); + const currentDate = dayjs(); + let difference = selectedDay - currentDate.day(); + if (difference < 0) { + difference += 7; + } + + const adjustedDate = currentDate.add(difference, 'day'); + setFormValues({ + ...formValues, + ['calibrationDischargeDate']: adjustedDate.toDate() + }); + }; + const handleSelectedCalibrationChargeChange = (event) => { setFormValues({ ...formValues, - ['CalibrationChargeState']: CalibrationChargeOptions.indexOf( + ['calibrationChargeState']: CalibrationChargeOptions.indexOf( event.target.value ), ['calibrationChargeDate']: @@ -201,6 +255,23 @@ function Configuration(props: ConfigurationProps) { }); }; + const handleSelectedCalibrationDisChargeChange = (event) => { + setFormValues({ + ...formValues, + ['calibrationDischargeState']: CalibrationChargeOptions.indexOf( + event.target.value + ), + ['calibrationDischargeDate']: + CalibrationChargeOptions.indexOf(event.target.value) == 0 + ? dayjs(props.values.Config.DownDayAndTimeForRepetitiveCalibration) + // .add(localOffset, 'minute') + .toDate() + : dayjs(props.values.Config.DownDayAndTimeForAdditionalCalibration) + // .add(localOffset, 'minute') + .toDate() + }); + }; + const handleChange = (e) => { const { name, value } = e.target; @@ -284,7 +355,84 @@ function Configuration(props: ConfigurationProps) { )} + + + setActiveTab(newValue)} + textColor="inherit" + TabIndicatorProps={{ style: { display: 'none' } }} // hide default underline + sx={{ + bgcolor: '#f5f5f5', + borderRadius: 2, + p: 0.5, + boxShadow: 1, + width: 500, + height: 47 + }} + > + ({ + flex: 2, + fontWeight: 'bold', + borderRadius: 2, + textTransform: 'none', + color: + activeTab === 'charge' + ? 'white' + : theme.palette.text.primary, + bgcolor: + activeTab === 'charge' + ? theme.palette.primary.main + : 'transparent', + + '&:hover': { + bgcolor: + activeTab === 'charge' + ? theme.palette.primary.dark + : '#eee' + } + })} + /> + {product === 3 && ( + ({ + flex: 2, + fontWeight: 'bold', + borderRadius: 2, + textTransform: 'none', + color: + activeTab === 'discharge' + ? 'white' + : theme.palette.text.primary, + bgcolor: + activeTab === 'discharge' + ? theme.palette.primary.main + : 'transparent', + '&:hover': { + bgcolor: + activeTab === 'discharge' + ? theme.palette.primary.dark + : '#eee' + } + })} + /> + )} + + + -
- - } - name="minimumSoC" - value={formValues.minimumSoC} - onChange={handleChange} - helperText={ - errors.minimumSoC ? ( - - Value should be between 0-100% - - ) : ( - '' - ) - } - fullWidth - /> -
- -
- - - - - - -
- {formValues.CalibrationChargeState == 1 && ( -
- - ( - - )} - /> - - {/**/} - -
- )} - - {formValues.CalibrationChargeState == 0 && ( + {activeTab === 'charge' && ( <>
- + } + name="minimumSoC" + value={formValues.minimumSoC} + onChange={handleChange} + helperText={ + errors.minimumSoC ? ( + + Value should be between 0-100% + + ) : ( + '' + ) + } fullWidth - sx={{ marginLeft: 1, width: 390, marginTop: 2 }} - > + /> +
+ +
+
-
- - handleConfirm(dayjs(newTime))} - renderInput={(params) => ( - + + ( + + )} + /> + +
+ )} + + {formValues.calibrationChargeState == 0 && ( + <> +
+ + + + + + +
+ +
+ + + handleConfirm(dayjs(newTime)) + } + renderInput={(params) => ( + + )} /> - )} - /> - + +
+ + )} + +
+ + } + name="gridSetPoint" + value={formValues.gridSetPoint} + onChange={handleChange} + helperText={ + errors.gridSetPoint ? ( + + Please provide a valid number + + ) : ( + '' + ) + } + fullWidth + />
+
+ + } + value={ + (props.values.DcDc.SystemControl + .NumberOfConnectedSlaves as number) * 10 + } + fullWidth + /> +
+ + {props.values.Config.MaxBatteryDischargingCurrent && ( +
+ + } + value={ + (props.values.Config + .MaxBatteryDischargingCurrent as number) * + 48 * + (props.values.DcDc.SystemControl + .NumberOfConnectedSlaves as number) + } + fullWidth + /> +
+ )} )} -
- - } - name="gridSetPoint" - value={formValues.gridSetPoint} - onChange={handleChange} - helperText={ - errors.gridSetPoint ? ( - - Please provide a valid number - - ) : ( - '' - ) - } - fullWidth - /> -
-
- - } - value={ - (props.values.DcDc.SystemControl - .NumberOfConnectedSlaves as number) * 10 - } - fullWidth - /> -
+ {product == 3 && activeTab === 'discharge' && ( + <> +
+ + + + + + +
- {props.values.Config.MaxBatteryDischargingCurrent && ( -
- - } - value={ - (props.values.Config - .MaxBatteryDischargingCurrent as number) * - 48 * - (props.values.DcDc.SystemControl - .NumberOfConnectedSlaves as number) - } - fullWidth - /> -
+ {formValues.calibrationDischargeState == 1 && ( +
+ + ( + + )} + /> + +
+ )} + + {formValues.calibrationDischargeState == 0 && ( + <> +
+ + + + + + +
+ +
+ + + handleConfirmDischarge(dayjs(newTime)) + } + renderInput={(params) => ( + + )} + /> + +
+ + )} + )} + {/*
*/} {/*
+
+ + } + name="information" + value={formValues.information} + onChange={handleChange} + variant="outlined" + fullWidth + multiline + minRows={6} // 👈 Makes it visually bigger + maxRows={12} // 👈 Optional max height before scroll + inputProps={{ + style: { + fontFamily: 'monospace' // optional: makes tabs/formatting more clear + } + }} + /> +
+ {currentUser.userType == UserType.admin && ( <>
@@ -282,8 +308,11 @@ function Information(props: InformationProps) { label="S3 Bucket Name" name="s3bucketname" value={ - formValues.s3BucketId + - '-3e5b3069-214a-43ee-8d85-57d72000c19d' + product === 0 || product == 3 + ? formValues.s3BucketId + + '-3e5b3069-214a-43ee-8d85-57d72000c19d' + : formValues.s3BucketId + + '-e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa' } variant="outlined" fullWidth diff --git a/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodioHome.tsx b/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodioHome.tsx deleted file mode 100644 index 99d1dff87..000000000 --- a/typescript/frontend-marios2/src/content/dashboards/Information/InformationSodioHome.tsx +++ /dev/null @@ -1,393 +0,0 @@ -import { - Alert, - Box, - CardContent, - CircularProgress, - Container, - Grid, - IconButton, - Modal, - TextField, - Typography, - useTheme -} from '@mui/material'; -import { FormattedMessage } from 'react-intl'; -import Button from '@mui/material/Button'; -import { Close as CloseIcon } from '@mui/icons-material'; -import React, { useContext, useState } from 'react'; -import { I_Installation } from '../../../interfaces/InstallationTypes'; -import { InstallationsContext } from '../../../contexts/InstallationsContextProvider'; -import { UserContext } from '../../../contexts/userContext'; -import routes from '../../../Resources/routes.json'; -import { useNavigate } from 'react-router-dom'; -import { UserType } from '../../../interfaces/UserTypes'; - -interface InformationSodioHomeProps { - values: I_Installation; - type?: string; -} - -function InformationSodioHome(props: InformationSodioHomeProps) { - if (props.values === null) { - return null; - } - - const context = useContext(UserContext); - const { currentUser } = context; - const theme = useTheme(); - const [formValues, setFormValues] = useState(props.values); - const requiredFields = ['name', 'region', 'location', 'country']; - const [openModalDeleteInstallation, setOpenModalDeleteInstallation] = - useState(false); - const navigate = useNavigate(); - - const DeviceTypes = ['Cerbo', 'Venus']; - - const installationContext = useContext(InstallationsContext); - const { - updateInstallation, - deleteInstallation, - loading, - setLoading, - error, - setError, - updated, - setUpdated - } = installationContext; - - const handleChange = (e) => { - const { name, value } = e.target; - setFormValues({ - ...formValues, - [name]: value - }); - }; - const handleSubmit = () => { - setLoading(true); - setError(false); - updateInstallation(formValues, props.type); - }; - - const handleDelete = () => { - setLoading(true); - setError(false); - setOpenModalDeleteInstallation(true); - }; - - const deleteInstallationModalHandle = () => { - setOpenModalDeleteInstallation(false); - deleteInstallation(formValues, props.type); - setLoading(false); - - navigate(routes.salidomo_installations + routes.list, { - replace: true - }); - }; - - const deleteInstallationModalHandleCancel = () => { - setOpenModalDeleteInstallation(false); - setLoading(false); - }; - - const areRequiredFieldsFilled = () => { - for (const field of requiredFields) { - if (!formValues[field]) { - return false; - } - } - return true; - }; - - return ( - <> - {openModalDeleteInstallation && ( - - - - Do you want to delete this installation? - - -
- - -
-
-
- )} - - - - - - -
- - } - name="name" - value={formValues.name} - onChange={handleChange} - variant="outlined" - fullWidth - /> -
-
- - } - name="region" - value={formValues.region} - onChange={handleChange} - variant="outlined" - fullWidth - required - error={formValues.region === ''} - /> -
-
- - } - name="location" - value={formValues.location} - onChange={handleChange} - variant="outlined" - fullWidth - required - error={formValues.location === ''} - /> -
-
- - } - name="country" - value={formValues.country} - onChange={handleChange} - variant="outlined" - fullWidth - required - error={formValues.country === ''} - /> -
- -
- - } - name="information" - value={formValues.information} - onChange={handleChange} - variant="outlined" - fullWidth - /> -
- - {currentUser.userType == UserType.admin && ( - <> -
- -
- -
- -
- - )} - -
- - - {currentUser.userType == UserType.admin && ( - - )} - - {loading && ( - - )} - {error && ( - - - setError(false)} - sx={{ marginLeft: '4px' }} - > - - - - )} - {updated && ( - - - - setUpdated(false)} // Set error state to false on click - sx={{ marginLeft: '4px' }} - > - - - - )} -
-
-
-
-
-
- - ); -} - -export default InformationSodioHome; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx index 8ad8158d4..d20df49d6 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/FlatInstallationView.tsx @@ -135,7 +135,6 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { }, []); const handleSelectOneInstallation = (installationID: number): void => { - console.log('when selecting installation', product); if (selectedInstallation != installationID) { setSelectedInstallation(installationID); setSelectedInstallation(-1); diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/installationForm.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/installationForm.tsx index a1e79c7f7..99dfb9760 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/installationForm.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/installationForm.tsx @@ -19,11 +19,14 @@ interface installationFormProps { cancel: () => void; submit: () => void; parentid: number; + productToInsert: number; } function installationForm(props: installationFormProps) { const theme = useTheme(); const [open, setOpen] = useState(true); + + console.log('productToInsert IS ', props.productToInsert); const [formValues, setFormValues] = useState>({ installationName: '', name: '', @@ -59,7 +62,7 @@ function installationForm(props: installationFormProps) { const handleSubmit = async (e) => { setLoading(true); formValues.parentId = props.parentid; - formValues.product = 0; + formValues.product = props.productToInsert; const responseData = await createInstallation(formValues); props.submit(); }; diff --git a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx index 9b192b300..acf98244f 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx @@ -216,6 +216,20 @@ export interface Line { Power: Power; } +export interface SodioHomeBattery { + AccumulatedChargeEnergy: number; + AccumulatedDischargeEnergy: number; + Current: number; + DailyChargeEnergy: number; + DailyDischargeEnergy: number; + MaxAllowableChargePower: number; + MaxAllowableDischargePower: number; + Power: number; + Soc: number; + Soh: number; + Voltage: number; +} + // The interface for the Battery structure, with dynamic keys (Device IDs) export interface JSONRecordData { Battery: { @@ -282,9 +296,12 @@ export interface JSONRecordData { CurtailP: number; DayAndTimeForAdditionalCalibration: string; DayAndTimeForRepetitiveCalibration: string; + DownDayAndTimeForAdditionalCalibration: string; + DownDayAndTimeForRepetitiveCalibration: string; DisplayIndividualBatteries: string; MaxBatteryDischargingCurrent: number; ForceCalibrationChargeState: string; + ForceCalibrationDischargeState: string; GridSetPoint: number; HoldSocZone: number; MinSoc: number; @@ -392,17 +409,101 @@ export interface JSONRecordData { LoadOnDc: { Power: number }; PvOnDc: { - DcWh: number; - NbrOfStrings: number; - Dc: { - Voltage: number; - Current: number; - Power: number; - }; - Strings: { - [PvId: string]: PvString; + [deviceId: string]: { + DcWh: number; + Dc: { + Voltage: number; + Current: number; + Power: number; + NbrOfStrings: number; + }; + Strings: { + [PvId: string]: PvString; + }; }; }; + + AcDcGrowatt: { + AcChargeEnable: number; + ActivePowerPercentDerating: number; + ActualChargeDischargePowerControlValue: number; + AlarmMainCode: number; + AlarmSubCode: number; + BatteriesRecords: { + AverageSoc: number; + AverageSoh: number; + Batteries: { [deviceId: string]: SodioHomeBattery }; + LowestSoc: number; + Power: number; + TotalChargeEnergy: number; + TotalDischargeEnergy: number; + TotalMaxCharge: number; + TotalMaxDischarge: number; + }; + + BatteryChargeCutoffVoltage: number; + BatteryClusterIndex: number; + BatteryDischargeCutoffVoltage: number; + BatteryMaxChargeCurrent: number; + BatteryMaxChargePower: number; + BatteryMaxDischargePower: number; + BatteryMaxdischargeCurrent: number; + BatteryOperatingMode: string; + BatteryType: number; + ChargeCutoffSoc: number; + ControlPermession: number; + DischargeCutoffSoc: number; + EmsCommunicationFailureTime: number; + EnableCommand: number; + EnableEmsCommunicationFailureTime: number; + EnableSyn: string; + EnergyToGrid: number; + EnergyToUser: number; + FaultMainCode: number; + FaultSubCode: number; + Frequency: number; + GridAbLineVoltage: number; + GridBcLineVoltage: number; + GridCaLineVoltage: number; + InverterActivePower: number; + InverterReactivePower: number; + InverterTemperature: number; + LoadPriorityDischargeCutoffSoc: number; + MaxActivePower: number; + MeterPower: number; + OffGridDischargeCutoffSoc: number; + OperatingPriority: string; + PhaseACurrent: number; + PhaseBCurrent: number; + PhaseCCurrent: number; + PowerFactor: number; + Pv1Current: number; + Pv1Voltage: number; + Pv2Current: number; + Pv2Voltage: number; + PvInputMaxPower: number; + RatedPower: number; + RemoteChargDischargePower: number; + RemotePowerControl: number; + SystemDateTime: string; + SystemOperatingMode: string; + TotalEnergyToGrid: number; + TotalEnergyToUser: number; + VppProtocolVerNumber: number; + }; + + // DcWh: number; + // // NbrOfStrings: number; + // Dc: { [deviceId: string]: Dc }; + // // Dc: { + // // Voltage: number; + // // Current: number; + // // Power: number; + // // }; + // Strings: { + // [PvId: string]: PvString; + // }; + // }; } export const parseChunkJson = ( @@ -497,8 +598,10 @@ export interface I_BoxDataValue { export type ConfigurationValues = { minimumSoC: string | number; gridSetPoint: number; - CalibrationChargeState: number; + calibrationChargeState: number; calibrationChargeDate: Date | null; + calibrationDischargeState: number; + calibrationDischargeDate: Date | null; }; // // export interface Pv { diff --git a/typescript/frontend-marios2/src/content/dashboards/ManageAccess/UserAccess.tsx b/typescript/frontend-marios2/src/content/dashboards/ManageAccess/UserAccess.tsx new file mode 100644 index 000000000..4c39af2ac --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/ManageAccess/UserAccess.tsx @@ -0,0 +1,499 @@ +import React, { + Fragment, + useCallback, + useContext, + useEffect, + useState +} from 'react'; +import { + Alert, + Box, + Container, + Divider, + FormControl, + Grid, + IconButton, + InputLabel, + ListItem, + MenuItem, + Modal, + Select, + useTheme +} from '@mui/material'; +import { TokenContext } from 'src/contexts/tokenContext'; +import { UserContext } from 'src/contexts/userContext'; +import ListItemAvatar from '@mui/material/ListItemAvatar'; +import Avatar from '@mui/material/Avatar'; +import ListItemText from '@mui/material/ListItemText'; +import PersonIcon from '@mui/icons-material/Person'; +import Button from '@mui/material/Button'; +import { Close as CloseIcon } from '@mui/icons-material'; +import { AccessContext } from 'src/contexts/AccessContextProvider'; +import { FormattedMessage } from 'react-intl'; +import { InnovEnergyUser, UserType } from '../../../interfaces/UserTypes'; +import PersonRemoveIcon from '@mui/icons-material/PersonRemove'; +import { + I_Folder, + I_Installation +} from '../../../interfaces/InstallationTypes'; +import axiosConfig from '../../../Resources/axiosConfig'; + +interface UserAccessProps { + current_user: InnovEnergyUser; +} + +function UserAccess(props: UserAccessProps) { + if (props.current_user == undefined) { + return null; + } + + const theme = useTheme(); + const tokencontext = useContext(TokenContext); + const { removeToken } = tokencontext; + const context = useContext(UserContext); + const { currentUser, setUser } = context; + const [openFolder, setOpenFolder] = useState(false); + const [openInstallation, setOpenInstallation] = useState(false); + const [openModal, setOpenModal] = useState(false); + + const [selectedFolderNames, setSelectedFolderNames] = useState([]); + const [selectedInstallationNames, setSelectedInstallationNames] = useState< + string[] + >([]); + + const [folders, setFolders] = useState([]); + const [installations, setInstallations] = useState([]); + const accessContext = useContext(AccessContext); + const { + fetchInstallationsForUser, + accessibleInstallationsForUser, + error, + setError, + updated, + setUpdated, + updatedmessage, + errormessage, + setErrorMessage, + setUpdatedMessage, + RevokeAccessFromResource + } = accessContext; + + const fetchFolders = useCallback(async () => { + return axiosConfig + .get('/GetAllFolders') + .then((res) => { + setFolders(res.data); + }) + .catch((err) => { + if (err.response && err.response.status == 401) { + removeToken(); + } + }); + }, [setFolders]); + + const fetchInstallations = useCallback(async () => { + try { + // fetch product 0 + const res0 = await axiosConfig.get( + `/GetAllInstallationsFromProduct?product=0` + ); + const installations0 = res0.data; + + // fetch product 1 + const res1 = await axiosConfig.get( + `/GetAllInstallationsFromProduct?product=3` + ); + const installations1 = res1.data; + + // aggregate + const combined = [...installations0, ...installations1]; + + // update + setInstallations(combined); + } catch (err) { + if (err.response && err.response.status === 401) { + removeToken(); + } + } finally { + } + }, [setInstallations]); + + useEffect(() => { + fetchInstallationsForUser(props.current_user.id); + }, [props.current_user]); + + const handleGrantAccess = () => { + fetchFolders(); + fetchInstallations(); + setOpenModal(true); + setSelectedFolderNames([]); + setSelectedInstallationNames([]); + }; + + const handleFolderChange = (event) => { + setSelectedFolderNames(event.target.value); + }; + + const handleInstallationChange = (event) => { + setSelectedInstallationNames(event.target.value); + }; + const handleOpenFolder = () => { + setOpenFolder(true); + }; + + const handleCloseFolder = () => { + setOpenFolder(false); + }; + const handleCancel = () => { + setOpenModal(false); + }; + const handleOpenInstallation = () => { + setOpenInstallation(true); + }; + + const handleCloseInstallation = () => { + setOpenInstallation(false); + }; + + const handleSubmit = async () => { + for (const folderName of selectedFolderNames) { + const folder = folders.find((folder) => folder.name === folderName); + + await axiosConfig + .post( + `/GrantUserAccessToFolder?UserId=${props.current_user.id}&FolderId=${folder.id}` + ) + .then((response) => { + if (response) { + setUpdatedMessage( + 'Granted access to user ' + props.current_user.name + ); + setUpdated(true); + } + }) + .catch((err) => { + setErrorMessage('An error has occured'); + setError(true); + }); + } + + for (const installationName of selectedInstallationNames) { + const installation = installations.find( + (installation) => installation.name === installationName + ); + + await axiosConfig + .post( + `/GrantUserAccessToInstallation?UserId=${props.current_user.id}&InstallationId=${installation.id}` + ) + .then((response) => { + if (response) { + setUpdatedMessage( + 'Granted access to user ' + props.current_user.name + ); + setUpdated(true); + } + }) + .catch((err) => { + setErrorMessage('An error has occured'); + setError(true); + }); + } + + setOpenModal(false); + fetchInstallationsForUser(props.current_user.id); + }; + + return ( + + + + {updated && ( + + {updatedmessage} + setUpdated(false)} + > + + + + )} + + {error && ( + + {errormessage} + setError(false)} + sx={{ + marginLeft: '10px' + }} + > + + + + )} + + {}} + aria-labelledby="error-modal" + aria-describedby="error-modal-description" + > + + +
+ + + + + + +
+ +
+ + + + + + +
+ + + +
+
+
+ + +
+ + {accessibleInstallationsForUser.map((installation, index) => { + const isLast = index === accessibleInstallationsForUser.length - 1; + + return ( + + { + RevokeAccessFromResource( + 'ToInstallation', + props.current_user.id, + 'InstallationId', + installation.id, + props.current_user.name + ); + + fetchInstallationsForUser(props.current_user.id); + }} + edge="end" + > + + + ) + } + > + + + + + + + + + + ); + })} + + {accessibleInstallationsForUser.length == 0 && ( + + + + + )} + +
+
+ ); +} + +export default UserAccess; diff --git a/typescript/frontend-marios2/src/content/dashboards/PvView/PvView.tsx b/typescript/frontend-marios2/src/content/dashboards/PvView/PvView.tsx index 49434e2a6..e246f0f7d 100644 --- a/typescript/frontend-marios2/src/content/dashboards/PvView/PvView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/PvView/PvView.tsx @@ -24,34 +24,37 @@ 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.PvOnDc - ? Object.entries(props.values.PvOnDc.Strings) - .map(([pvId, pv]) => { - return { pvId, pv }; // Here we return an object with the id and device - }) - .sort((a, b) => parseInt(b.pvId) - parseInt(a.pvId)) - : []; + // ✅ Flatten, sort, and assign unique displayId from 1-N + const sortedPvView = props.values?.PvOnDc + ? Object.entries(props.values.PvOnDc) + .flatMap(([deviceId, device]) => + Object.entries(device.Strings).map(([pvId, pv], index) => ({ + pvId, + pv, + deviceId, + displayId: `CU${deviceId} -> AMPT ${index + 1}` + })) + ) + .sort((a, b) => { + if (a.deviceId === b.deviceId) { + return parseInt(a.pvId) - parseInt(b.pvId); + } + return a.deviceId.localeCompare(b.deviceId); + }) + : []; - const [loading, setLoading] = useState(sortedPvView.length == 0); + 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) { + if (sortedPvView.length === 0) { setLoading(true); } else { setLoading(false); @@ -84,6 +87,7 @@ function PvView(props: PvViewProps) { )} + {loading && props.connected && ( - - - - - Pv - Power - Voltage - Current - - - - {sortedPvView.map(({ pvId, pv }) => ( - - - {'AMPT ' + pvId} - - - - {pv.Power + ' W'} - - - {pv.Voltage + ' V'} - - - {pv.Current + ' A'} - + {Object.entries( + sortedPvView.reduce((acc, entry) => { + if (!acc[entry.deviceId]) acc[entry.deviceId] = []; + acc[entry.deviceId].push(entry); + return acc; + }, {} as Record) + ).map(([deviceId, entries]) => ( + +
+ + + Pv + Power + Voltage + Current - ))} - -
-
+ + + {entries.map(({ displayId, pv }, index) => ( + + + {displayId} + + + {pv.Power} W + + + {pv.Voltage} V + + + {pv.Current} A + + + ))} + + + + ))}
)} diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx index a8563ee3c..72b6e1a67 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/FlatInstallationView.tsx @@ -55,7 +55,7 @@ const FlatInstallationView = (props: FlatInstallationViewProps) => { routes.installation + `${installationID}` + '/' + - routes.live, + routes.batteryview, { replace: true } diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx index 2eaeed85e..9fa696a25 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/Installation.tsx @@ -19,8 +19,11 @@ import HistoryOfActions from '../History/History'; import BuildIcon from '@mui/icons-material/Build'; import AccessContextProvider from '../../../contexts/AccessContextProvider'; import Access from '../ManageAccess/Access'; -import InformationSodioHome from '../Information/InformationSodioHome'; -import CryptoJS from 'crypto-js'; +import Information from '../Information/Information'; +import { TimeSpan, UnixTime } from '../../../dataCache/time'; +import { fetchDataJson } from '../Installations/fetchData'; +import { FetchResult } from '../../../dataCache/dataCache'; +import BatteryViewSodioHome from '../BatteryView/BatteryViewSodioHome'; interface singleInstallationProps { current_installation?: I_Installation; @@ -31,6 +34,19 @@ function SodioHomeInstallation(props: singleInstallationProps) { if (props.current_installation == undefined) { return null; } + const S3data = { + s3Region: props.current_installation.s3Region, + s3Provider: props.current_installation.s3Provider, + s3Key: props.current_installation.s3Key, + s3Secret: props.current_installation.s3Secret, + s3BucketId: props.current_installation.s3BucketId + }; + + const s3Bucket = + props.current_installation.s3BucketId.toString() + + '-' + + 'e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa'; + const context = useContext(UserContext); const { currentUser } = context; const location = useLocation().pathname; @@ -45,6 +61,7 @@ function SodioHomeInstallation(props: singleInstallationProps) { const [connected, setConnected] = useState(true); const [loading, setLoading] = useState(true); const [fetchFunctionCalled, setFetchFunctionCalled] = useState(false); + const s3Credentials = { s3Bucket, ...S3data }; //In React, useRef creates a mutable object that persists across renders without triggering re-renders when its value changes. //While fetching, we check the value of continueFetching, if its false, we break. This means that either the user changed tab or the object has been finished its execution (return) @@ -91,48 +108,75 @@ function SodioHomeInstallation(props: singleInstallationProps) { } const fetchDataPeriodically = async () => { - while (continueFetching.current) { - //Fetch data from Bitwatt cloud - console.log('Fetching from Bitwatt cloud'); + var timeperiodToSearch = 200; + let res; + let timestampToFetch; - console.log(props.current_installation.serialNumber); - console.log(props.current_installation.s3WriteKey); - console.log(props.current_installation.s3WriteSecret); - - const timeStamp = Date.now().toString(); - - // Encrypt timestamp using AES-ECB with PKCS7 padding - const key = CryptoJS.enc.Utf8.parse( - props.current_installation.s3WriteSecret - ); - const encrypted = CryptoJS.AES.encrypt(timeStamp, key, { - mode: CryptoJS.mode.ECB, - padding: CryptoJS.pad.Pkcs7 - }).toString(); - - // Set headers - const headers = { - 'X-Signature': encrypted, - 'X-AccessKey': props.current_installation.s3WriteKey, - 'Content-Type': 'application/json' - }; - - // API URL - const url = `https://www.biwattpower.com/gateway/admin/open/device/currentEnergyFlowData/${props.current_installation.serialNumber}`; + for (var i = 0; i < timeperiodToSearch; i += 2) { + if (!continueFetching.current) { + return false; + } + timestampToFetch = UnixTime.now().earlier(TimeSpan.fromSeconds(i)); try { - const response = await fetch(url, { method: 'GET', headers }); - const result = await response.json(); - console.log('API Response:', result); - } catch (error) { - console.error('Request failed:', error); + res = await fetchDataJson(timestampToFetch, s3Credentials, false); + if (res !== FetchResult.notAvailable && res !== FetchResult.tryLater) { + break; + } + } catch (err) { + console.error('Error fetching data:', err); + return false; + } + } + + if (i >= timeperiodToSearch) { + setConnected(false); + setLoading(false); + return false; + } + setConnected(true); + setLoading(false); + + while (continueFetching.current) { + for (const timestamp of Object.keys(res)) { + if (!continueFetching.current) { + setFetchFunctionCalled(false); + return false; + } + console.log(`Timestamp: ${timestamp}`); + console.log(res[timestamp]); + + setValues(res[timestamp]); + await timeout(2000); } - // Wait for 2 seconds before fetching again - await timeout(200000); - console.log('ssssssssssssssssssssssssssssssssssssss'); + timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(60)); + console.log('NEW TIMESTAMP TO FETCH IS ' + timestampToFetch); + + for (i = 0; i < 30; i++) { + if (!continueFetching.current) { + return false; + } + + try { + console.log('Trying to fetch timestamp ' + timestampToFetch); + res = await fetchDataJson(timestampToFetch, s3Credentials, false); + if ( + res !== FetchResult.notAvailable && + res !== FetchResult.tryLater + ) { + break; + } + } catch (err) { + console.error('Error fetching data:', err); + return false; + } + timestampToFetch = timestampToFetch.later(TimeSpan.fromSeconds(1)); + } + if (i == 30) { + return false; + } } - setFetchFunctionCalled(false); }; useEffect(() => { @@ -147,11 +191,16 @@ function SodioHomeInstallation(props: singleInstallationProps) { }, [status]); useEffect(() => { - console.log(currentTab); - if (currentTab == 'live' || location.includes('batteryview')) { - //Fetch periodically if the tab is live or batteryview + if ( + currentTab == 'live' || + currentTab == 'pvview' || + currentTab == 'configuration' || + location.includes('batteryview') + ) { + //Fetch periodically if the tab is live, pvview or batteryview if ( currentTab == 'live' || + currentTab == 'pvview' || (location.includes('batteryview') && !location.includes('mainstats')) ) { if (!continueFetching.current) { @@ -163,6 +212,10 @@ function SodioHomeInstallation(props: singleInstallationProps) { } } } + //Fetch only one time in configuration tab + // if (currentTab == 'configuration') { + // fetchDataForOneTime(); + // } return () => { continueFetching.current = false; @@ -322,10 +375,11 @@ function SodioHomeInstallation(props: singleInstallationProps) { + > } /> @@ -351,28 +405,17 @@ function SodioHomeInstallation(props: singleInstallationProps) { } /> - {/**/} - {/* }*/} - {/*/>*/} - - {/**/} - {/* }*/} - {/*>*/} + + } + > {currentUser.userType == UserType.admin && ( void; - submit: () => void; - parentid: number; -} - -function SodiohomeInstallationForm(props: SodiohomeInstallationFormProps) { - const theme = useTheme(); - const [open, setOpen] = useState(true); - const [formValues, setFormValues] = useState>({ - name: '', - region: '', - location: '', - country: '', - serialNumber: '', - s3WriteSecret: '', - s3WriteKey: '' - }); - const requiredFields = ['name', 'location', 'country', 'serialNumber']; - - const installationContext = useContext(InstallationsContext); - const { createInstallation, loading, setLoading, error, setError } = - installationContext; - - const handleChange = (e) => { - const { name, value } = e.target; - - setFormValues({ - ...formValues, - [name]: value - }); - }; - const handleSubmit = async (e) => { - setLoading(true); - formValues.parentId = props.parentid; - formValues.product = 2; - const responseData = await createInstallation(formValues); - props.submit(); - }; - const handleCancelSubmit = (e) => { - props.cancel(); - }; - - const areRequiredFieldsFilled = () => { - for (const field of requiredFields) { - if (!formValues[field]) { - return false; - } - } - return true; - }; - - const isMobile = window.innerWidth <= 1490; - - return ( - <> - {}} - aria-labelledby="error-modal" - aria-describedby="error-modal-description" - > - - -
- - } - name="name" - value={formValues.name} - onChange={handleChange} - required - error={formValues.name === ''} - /> -
-
- } - name="region" - value={formValues.region} - onChange={handleChange} - required - error={formValues.region === ''} - /> -
-
- - } - name="location" - value={formValues.location} - onChange={handleChange} - required - error={formValues.location === ''} - /> -
- -
- - } - name="country" - value={formValues.country} - onChange={handleChange} - required - error={formValues.country === ''} - /> -
- -
- - } - name="serialNumber" - value={formValues.serialNumber} - onChange={handleChange} - required - error={formValues.serialNumber === ''} - /> -
- -
- - } - name="s3WriteKey" - value={formValues.s3WriteKey} - onChange={handleChange} - required - error={formValues.s3WriteKey === ''} - /> -
- -
- - } - name="s3WriteSecret" - value={formValues.s3WriteSecret} - onChange={handleChange} - required - error={formValues.s3WriteSecret === ''} - /> -
- -
- - } - name="information" - value={formValues.information} - onChange={handleChange} - /> -
-
-
- - - - - {loading && ( - - )} - - {error && ( - - - setError(false)} - sx={{ marginLeft: '4px' }} - > - - - - )} -
-
-
- - ); -} - -export default SodiohomeInstallationForm; diff --git a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx index 790bca9e7..826e0f85e 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SodiohomeInstallations/index.tsx @@ -14,12 +14,17 @@ import { ProductIdContext } from '../../../contexts/ProductIdContextProvider'; import { UserType } from '../../../interfaces/UserTypes'; import SodioHomeInstallation from './Installation'; -function SodioHomeInstallationTabs() { +interface SodioHomeInstallationTabsProps { + product: number; +} + +function SodioHomeInstallationTabs(props: SodioHomeInstallationTabsProps) { const location = useLocation(); const context = useContext(UserContext); const { currentUser } = context; const tabList = [ 'live', + 'batteryview', 'information', 'manage', 'overview', @@ -53,26 +58,14 @@ function SodioHomeInstallationTabs() { }, [location]); useEffect(() => { - if (sodiohomeInstallations.length === 0 && fetchedInstallations === false) { + setProduct(props.product); + }, [props.product]); + + useEffect(() => { + if (product == props.product) { fetchAllSodiohomeInstallations(); - setFetchedInstallations(true); } - }, [sodiohomeInstallations]); - - useEffect(() => { - if (sodiohomeInstallations && sodiohomeInstallations.length > 0) { - if (!socket) { - openSocket(2); - } else if (product != 2) { - closeSocket(); - openSocket(2); - } - } - }, [sodiohomeInstallations]); - - useEffect(() => { - setProduct(2); - }, []); + }, [product]); const handleTabsChange = (_event: ChangeEvent<{}>, value: string): void => { setCurrentTab(value); @@ -101,14 +94,23 @@ function SodioHomeInstallationTabs() { const singleInstallationTabs = currentUser.userType == UserType.admin ? [ + // { + // value: 'live', + // label: + // }, { - value: 'live', - label: - }, - { - value: 'overview', - label: + value: 'batteryview', + label: ( + + ) }, + // { + // value: 'overview', + // label: + // }, { value: 'log', label: @@ -141,14 +143,14 @@ function SodioHomeInstallationTabs() { } ] : [ - { - value: 'live', - label: - }, - { - value: 'overview', - label: - }, + // { + // value: 'live', + // label: + // }, + // { + // value: 'overview', + // label: + // }, { value: 'information', @@ -172,14 +174,23 @@ function SodioHomeInstallationTabs() { value: 'tree', icon: }, + // { + // value: 'live', + // label: + // }, { - value: 'live', - label: - }, - { - value: 'overview', - label: + value: 'batteryview', + label: ( + + ) }, + // { + // value: 'overview', + // label: + // }, { value: 'log', label: @@ -224,10 +235,10 @@ function SodioHomeInstallationTabs() { value: 'tree', icon: }, - { - value: 'live', - label: - }, + // { + // value: 'live', + // label: + // }, { value: 'overview', label: diff --git a/typescript/frontend-marios2/src/content/dashboards/Topology/Topology.tsx b/typescript/frontend-marios2/src/content/dashboards/Topology/Topology.tsx index 154d28305..90c775702 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Topology/Topology.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Topology/Topology.tsx @@ -29,7 +29,6 @@ function Topology(props: TopologyProps) { const { product, setProduct } = useContext(ProductIdContext); - //console.log('product VALUE IS ', product); const [showValues, setShowValues] = useState(false); const handleSwitch = () => () => { @@ -38,6 +37,13 @@ function Topology(props: TopologyProps) { const isMobile = window.innerWidth <= 1490; + const totalPvPower = props.values?.PvOnDc + ? Object.values(props.values.PvOnDc).reduce( + (sum, device) => sum + (device?.Dc?.Power || 0), + 0 + ) + : 0; + return ( @@ -408,7 +414,7 @@ function Topology(props: TopologyProps) { data: props.values?.PvOnDc ? [ { - value: props.values.PvOnDc.Dc.Power, + value: totalPvPower, unit: 'W' } ] @@ -421,15 +427,12 @@ function Topology(props: TopologyProps) { position: 'top', data: props.values?.PvOnDc ? { - value: props.values.PvOnDc.Dc.Power, + value: totalPvPower, unit: 'W' } : undefined, amount: props.values?.PvOnDc - ? getAmount( - highestConnectionValue, - props.values.PvOnDc.Dc.Power - ) + ? getAmount(highestConnectionValue, totalPvPower) : 0, showValues: showValues }} diff --git a/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx b/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx index a43668830..fcceca0ff 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Tree/Information.tsx @@ -26,7 +26,6 @@ import { InstallationsContext } from '../../../contexts/InstallationsContextProv import { UserType } from '../../../interfaces/UserTypes'; import InstallationForm from '../Installations/installationForm'; import SalidomoInstallationForm from '../SalidomoInstallations/SalidomoInstallationForm'; -import SodiohomeInstallationForm from '../SodiohomeInstallations/SodiohomeInstallationForm'; interface TreeInformationProps { folder: I_Folder; @@ -65,7 +64,7 @@ function TreeInformation(props: TreeInformationProps) { // console.log('Selected Product:', e.target.value); }; - const ProductTypes = ['Salimax', 'Salidomo', 'Sodiohome']; + const ProductTypes = ['Salimax', 'Salidomo', 'Sodiohome', 'SodistoreMax']; const isMobile = window.innerWidth <= 1490; @@ -322,13 +321,17 @@ function TreeInformation(props: TreeInformationProps) { )} - {openModalInstallation && product == 'Salimax' && ( - - )} + {openModalInstallation && + (product == 'Salimax' || + product == 'Sodiohome' || + product == 'SodistoreMax') && ( + + )} {openModalInstallation && product == 'Salidomo' && ( )} - {openModalInstallation && product == 'Sodiohome' && ( - - )} + {/*{openModalInstallation && product == 'Sodiohome' && (*/} + {/* */} + {/*)}*/} { return ( - + @@ -113,13 +113,14 @@ const FlatUsersView = (props: FlatUsersViewProps) => { - - {selectedUser && ( - - )} + + {selectedUser && ( + + )} + ); }; diff --git a/typescript/frontend-marios2/src/content/dashboards/Users/User.tsx b/typescript/frontend-marios2/src/content/dashboards/Users/User.tsx index c9c408ee7..25725a759 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Users/User.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Users/User.tsx @@ -26,6 +26,7 @@ import { InnovEnergyUser } from 'src/interfaces/UserTypes'; import { TokenContext } from 'src/contexts/tokenContext'; import { TabsContainerWrapper } from 'src/layouts/TabsContainerWrapper'; import { FormattedMessage } from 'react-intl'; +import UserAccess from '../ManageAccess/UserAccess'; interface singleUserProps { current_user: InnovEnergyUser; @@ -41,7 +42,10 @@ function User(props: singleUserProps) { const [formValues, setFormValues] = useState(props.current_user); const tokencontext = useContext(TokenContext); const { removeToken } = tokencontext; - const tabs = [{ value: 'user', label: 'User' }]; + const tabs = [ + { value: 'user', label: 'User' }, + { value: 'manage', label: 'Access Management' } + ]; const [openModalDeleteUser, setOpenModalDeleteUser] = useState(false); const UserTypes = ['Client', 'Partner', 'Admin']; @@ -226,7 +230,7 @@ function User(props: singleUserProps) { )} - + )} + {currentTab === 'manage' && ( + + )} diff --git a/typescript/frontend-marios2/src/content/dashboards/Users/userForm.tsx b/typescript/frontend-marios2/src/content/dashboards/Users/userForm.tsx index 99015e890..57cef9bf5 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Users/userForm.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Users/userForm.tsx @@ -69,25 +69,38 @@ function userForm(props: userFormProps) { const fetchInstallations = useCallback(async () => { setLoading(true); - return axiosConfig - .get('/GetAllInstallations') - .then((res) => { - setInstallations(res.data); - setLoading(false); - }) - .catch((err) => { - setLoading(false); - if (err.response && err.response.status == 401) { - removeToken(); - } - }); + + try { + // fetch product 0 + const res0 = await axiosConfig.get( + `/GetAllInstallationsFromProduct?product=0` + ); + const installations0 = res0.data; + + // fetch product 1 + const res1 = await axiosConfig.get( + `/GetAllInstallationsFromProduct?product=3` + ); + const installations1 = res1.data; + + // aggregate + const combined = [...installations0, ...installations1]; + + // update + setInstallations(combined); + } catch (err) { + if (err.response && err.response.status === 401) { + removeToken(); + } + } finally { + setLoading(false); + } }, [setInstallations]); useEffect(() => { fetchFolders(); fetchInstallations(); }, []); - const handleChange = (e) => { const { name, value } = e.target; setFormValues({ diff --git a/typescript/frontend-marios2/src/contexts/AccessContextProvider.tsx b/typescript/frontend-marios2/src/contexts/AccessContextProvider.tsx index b25d977b1..5280ff674 100644 --- a/typescript/frontend-marios2/src/contexts/AccessContextProvider.tsx +++ b/typescript/frontend-marios2/src/contexts/AccessContextProvider.tsx @@ -12,8 +12,11 @@ import { InnovEnergyUser } from '../interfaces/UserTypes'; import { FormattedMessage } from 'react-intl'; +import { I_Installation } from '../interfaces/InstallationTypes'; interface AccessContextProviderProps { + fetchInstallationsForUser: (userId: number) => void; + accessibleInstallationsForUser: I_Installation[]; availableUsers: InnovEnergyUser[]; fetchAvailableUsers: () => Promise; usersWithDirectAccess: InnovEnergyUser[]; @@ -44,6 +47,8 @@ interface AccessContextProviderProps { } export const AccessContext = createContext({ + fetchInstallationsForUser: () => Promise.resolve(), + accessibleInstallationsForUser: [], availableUsers: [], fetchAvailableUsers: () => { return Promise.resolve(); @@ -74,6 +79,8 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => { InnovEnergyUser[] >([]); const [availableUsers, setAvailableUsers] = useState([]); + const [accessibleInstallationsForUser, setAccessibleInstallationsForUser] = + useState([]); const [usersWithInheritedAccess, setUsersWithInheritedAccess] = useState< I_UserWithInheritedAccess[] @@ -104,6 +111,26 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => { [] ); + const fetchInstallationsForUser = useCallback(async (userId: number) => { + axiosConfig + .get(`/GetInstallationsTheUserHasAccess?userId=${userId}`) + .then((response) => { + if (response) { + setAccessibleInstallationsForUser(response.data); + } + }) + .catch((error) => { + setError(true); + const message = ( + + ).props.defaultMessage; + setErrorMessage(message); + }); + }, []); + const fetchUsersWithInheritedAccessForResource = useCallback( async (tempresourceType: string, id: number) => { axiosConfig @@ -192,6 +219,8 @@ const AccessContextProvider = ({ children }: { children: ReactNode }) => { return ( { axiosConfig .get('/GetAllSodioHomeInstallations') - .then((res: AxiosResponse) => - setSodiohomeInstallations(res.data) - ) + .then((res: AxiosResponse) => { + setSodiohomeInstallations(res.data); + openSocket(res.data); + }) .catch((err: AxiosError) => { if (err.response?.status === 401) { removeToken(); diff --git a/typescript/frontend-marios2/src/interfaces/Chart.tsx b/typescript/frontend-marios2/src/interfaces/Chart.tsx index b3ccf5013..4f47fbdd2 100644 --- a/typescript/frontend-marios2/src/interfaces/Chart.tsx +++ b/typescript/frontend-marios2/src/interfaces/Chart.tsx @@ -320,7 +320,7 @@ export const transformInputToDailyDataJson = async ( //'Battery.Dc.Power' for salimax, // 'Battery.Power', 'GridMeter.Ac.Power.Active', - 'PvOnDc.Dc.Power', + 'PvOnDc', 'DcDc.Dc.Link.Voltage', 'LoadOnAcGrid.Power.Active', 'LoadOnDc.Power' @@ -420,23 +420,35 @@ export const transformInputToDailyDataJson = async ( // eslint-disable-next-line @typescript-eslint/no-loop-func pathsToSearch.forEach((path) => { if (get(result, path) !== undefined) { - const value = path - .split('.') - .reduce((o, key) => (o ? o[key] : undefined), result); + let value: number | undefined = undefined; - if (value < chartOverview[categories[category_index]].min) { - chartOverview[categories[category_index]].min = value; + if (category_index === 4) { + // Custom logic for 'PvOnDc.Dc.Power' + value = Object.values( + result.PvOnDc as Record + ).reduce((sum, device) => sum + (device.Dc?.Power || 0), 0); + } else if (get(result, path) !== undefined) { + // Default path-based extraction + value = path + .split('.') + .reduce((o, key) => (o ? o[key] : undefined), result); } - if (value > chartOverview[categories[category_index]].max) { - chartOverview[categories[category_index]].max = value; + // Only push value if defined + if (value !== undefined) { + if (value < chartOverview[categories[category_index]].min) { + chartOverview[categories[category_index]].min = value; + } + + if (value > chartOverview[categories[category_index]].max) { + chartOverview[categories[category_index]].max = value; + } + + chartData[categories[category_index]].data.push([ + adjustedTimestampArray[i], + value + ]); } - chartData[categories[category_index]].data.push([ - adjustedTimestampArray[i], - value - ]); - } else { - //data[path].push([adjustedTimestamp, null]); } category_index++; }); diff --git a/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/SidebarMenu/index.tsx b/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/SidebarMenu/index.tsx index e827a321c..ba117b6f8 100644 --- a/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/SidebarMenu/index.tsx +++ b/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/SidebarMenu/index.tsx @@ -213,7 +213,7 @@ function SidebarMenu() { diff --git a/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/index.tsx b/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/index.tsx index 7d21bfb95..aafedb626 100644 --- a/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/index.tsx +++ b/typescript/frontend-marios2/src/layouts/SidebarLayout/Sidebar/index.tsx @@ -1,7 +1,8 @@ import { useContext } from 'react'; import Scrollbar from 'src/components/Scrollbar'; import { SidebarContext } from 'src/contexts/SidebarContext'; -import innovenergyLogo from 'src/Resources/images/innovenergy-Logo_Speichern-mit-Salz_R_color.svg'; +import inescoLogo from 'src/Resources/images/inesco_logo.png'; + import { alpha, Box, @@ -17,8 +18,8 @@ import SidebarMenu from './SidebarMenu'; const SidebarWrapper = styled(Box)( ({ theme }) => ` - width: ${theme.sidebar.width}; - min-width: ${theme.sidebar.width}; + width: 280px; /* previously theme.sidebar.width */ + min-width: 280px; color: ${theme.colors.alpha.trueWhite[70]}; position: relative; z-index: 7; @@ -54,16 +55,24 @@ function Sidebar() { innovenergy logo @@ -105,8 +114,8 @@ function Sidebar() { }} > innovenergy logo { flex: 1, pt: `${theme.header.height}`, [theme.breakpoints.up('lg')]: { - ml: `${theme.sidebar.width}` + ml: '260px' } }} > diff --git a/typescript/frontend-marios2/src/theme/schemes/PureLightTheme.ts b/typescript/frontend-marios2/src/theme/schemes/PureLightTheme.ts index 4e4c2a62b..4da030c5e 100644 --- a/typescript/frontend-marios2/src/theme/schemes/PureLightTheme.ts +++ b/typescript/frontend-marios2/src/theme/schemes/PureLightTheme.ts @@ -1,4 +1,4 @@ -import { alpha, createTheme, lighten, darken } from '@mui/material'; +import { alpha, createTheme, darken, lighten } from '@mui/material'; import '@mui/lab/themeAugmentation'; const themeColors = { From 076dcda4a2f607927e5d68edb49de6613d8aa368 Mon Sep 17 00:00:00 2001 From: Noe Date: Mon, 1 Sep 2025 13:05:39 +0200 Subject: [PATCH 2/4] Update backend to support SodioHome installations, update backend controller to support new user access management features --- csharp/App/Backend/Controller.cs | 19 ++- .../App/Backend/DataTypes/Methods/ExoCmd.cs | 11 +- .../Backend/DataTypes/Methods/Installation.cs | 12 +- .../App/Backend/DataTypes/Methods/Session.cs | 20 +-- csharp/App/Backend/DataTypes/Methods/User.cs | 3 + csharp/App/Backend/Database/Db.cs | 1 + csharp/App/Backend/MailerConfig.json | 10 +- csharp/App/Backend/Program.cs | 5 +- csharp/App/Backend/Relations/Session.cs | 4 + .../Backend/Websockets/WebsockerManager.cs | 114 ++++------------- .../CerboReleaseFiles/flows.json | 2 +- .../src/contexts/WebSocketContextProvider.tsx | 120 +----------------- 12 files changed, 81 insertions(+), 240 deletions(-) diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 199a57ed1..30bf96887 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -344,6 +344,18 @@ public class Controller : ControllerBase .Select(u => u.HidePassword()) .ToList(); } + + [HttpGet(nameof(GetInstallationsTheUserHasAccess))] + public ActionResult> GetInstallationsTheUserHasAccess(Int64 userId, Token authToken) + { + var user = Db.GetUserById(userId); + if (user == null) + return Unauthorized(); + + + + return user.AccessibleInstallations().ToList(); + } [HttpGet(nameof(GetUsersWithInheritedAccessToInstallation))] public ActionResult> GetUsersWithInheritedAccessToInstallation(Int64 id, Token authToken) @@ -926,10 +938,12 @@ public class Controller : ControllerBase public async Task>> EditInstallationConfig([FromBody] Configuration config, Int64 installationId,Token authToken) { var session = Db.GetSession(authToken); + + Console.WriteLine("CONFIG IS " + config.GetConfigurationString()); // Send configuration changes var success = await session.SendInstallationConfig(installationId, config); - + // Record configuration change if (success) { @@ -940,6 +954,7 @@ public class Controller : ControllerBase Timestamp = DateTime.Now, Description = config.GetConfigurationString() }; + Console.WriteLine(action.Description); var actionSuccess = await session.InsertUserAction(action); return actionSuccess?Ok():Unauthorized(); @@ -1020,7 +1035,7 @@ public class Controller : ControllerBase Db.DeleteUserPassword(user); - return Redirect($"https://monitor.innov.energy/?username={user.Email}&reset=true"); // TODO: move to settings file + return Redirect($"https://monitor.inesco.energy/?username={user.Email}&reset=true"); // TODO: move to settings file } } diff --git a/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs b/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs index 7944039ad..2f0297ca8 100644 --- a/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs +++ b/csharp/App/Backend/DataTypes/Methods/ExoCmd.cs @@ -413,6 +413,8 @@ public static class ExoCmd byte[] data = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(config)); udpClient.Send(data, data.Length, installation.VpnIp, port); + Console.WriteLine(config.GetConfigurationString()); + //Console.WriteLine($"Sent UDP message to {installation.VpnIp}:{port}: {config}"); Console.WriteLine($"Sent UDP message to {installation.VpnIp}:{port}"+" GridSetPoint is "+config.GridSetPoint +" and MinimumSoC is "+config.MinimumSoC); @@ -435,15 +437,6 @@ public static class ExoCmd } } } - - return false; - - //var s3Region = new S3Region($"https://{installation.S3Region}.{installation.S3Provider}", S3Credentials!); - //var url = s3Region.Bucket(installation.BucketName()).Path("config.json"); - //return await url.PutObject(config); - - } - } \ No newline at end of file diff --git a/csharp/App/Backend/DataTypes/Methods/Installation.cs b/csharp/App/Backend/DataTypes/Methods/Installation.cs index e2f225074..7072e785a 100644 --- a/csharp/App/Backend/DataTypes/Methods/Installation.cs +++ b/csharp/App/Backend/DataTypes/Methods/Installation.cs @@ -7,12 +7,9 @@ namespace InnovEnergy.App.Backend.DataTypes.Methods; public static class InstallationMethods { - private static readonly String BucketNameSalt = - // Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "" - // ? "stage" :"3e5b3069-214a-43ee-8d85-57d72000c19d"; - "3e5b3069-214a-43ee-8d85-57d72000c19d"; - + private static readonly String BucketNameSalt = "3e5b3069-214a-43ee-8d85-57d72000c19d"; private static readonly String SalidomoBucketNameSalt = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e"; + private static readonly String SodioHomeBucketNameSalt = "e7b9a240-3c5d-4d2e-a019-6d8b1f7b73fa"; public static String BucketName(this Installation installation) { @@ -20,6 +17,11 @@ public static class InstallationMethods { return $"{installation.S3BucketId}-{BucketNameSalt}"; } + + if (installation.Product == (int)ProductType.SodioHome) + { + return $"{installation.S3BucketId}-{SodioHomeBucketNameSalt}"; + } return $"{installation.S3BucketId}-{SalidomoBucketNameSalt}"; diff --git a/csharp/App/Backend/DataTypes/Methods/Session.cs b/csharp/App/Backend/DataTypes/Methods/Session.cs index a4f720db2..c0f59596e 100644 --- a/csharp/App/Backend/DataTypes/Methods/Session.cs +++ b/csharp/App/Backend/DataTypes/Methods/Session.cs @@ -223,7 +223,7 @@ public static class SessionMethods } - if (installation.Product == (int)ProductType.SodiStoreMax) + if (installation.Product == (int)ProductType.SodiStoreMax || installation.Product == (int)ProductType.SodioHome) { return user is not null && user.UserType != 0 @@ -244,15 +244,15 @@ public static class SessionMethods && await installation.CreateBucket() && await installation.RenewS3Credentials(); } - - if (installation.Product == (int)ProductType.SodioHome) - { - return user is not null - && user.UserType != 0 - && user.HasAccessToParentOf(installation) - && Db.Create(installation); - } - + // + // if (installation.Product == (int)ProductType.SodioHome) + // { + // return user is not null + // && user.UserType != 0 + // && user.HasAccessToParentOf(installation) + // && Db.Create(installation); + // } + // return false; diff --git a/csharp/App/Backend/DataTypes/Methods/User.cs b/csharp/App/Backend/DataTypes/Methods/User.cs index 9628fe9a7..cd6b992c9 100644 --- a/csharp/App/Backend/DataTypes/Methods/User.cs +++ b/csharp/App/Backend/DataTypes/Methods/User.cs @@ -229,6 +229,9 @@ public static class UserMethods public static Task SendEmail(this User user, String subject, String body) { + + Console.WriteLine(user.Name); + Console.WriteLine(subject); return Mailer.Send(user.Name, user.Email, subject, body); } diff --git a/csharp/App/Backend/Database/Db.cs b/csharp/App/Backend/Database/Db.cs index 39d11b9c5..f83e33cac 100644 --- a/csharp/App/Backend/Database/Db.cs +++ b/csharp/App/Backend/Database/Db.cs @@ -284,6 +284,7 @@ public static partial class Db } catch { + Console.WriteLine("return false"); return false; } } diff --git a/csharp/App/Backend/MailerConfig.json b/csharp/App/Backend/MailerConfig.json index b5de08366..80840398f 100644 --- a/csharp/App/Backend/MailerConfig.json +++ b/csharp/App/Backend/MailerConfig.json @@ -1,8 +1,8 @@ { - "SmtpServerUrl" : "mail.agenturserver.de", - "SmtpUsername" : "p518526p69", - "SmtpPassword" : "i;b*xqm4iB5uhl", + "SmtpServerUrl" : "smtp.gmail.com", + "SmtpUsername" : "angelis@inesco.energy", + "SmtpPassword" : "huvu pkqd kakz hqtm ", "SmtpPort" : 587, - "SenderName" : "InnovEnergy", - "SenderAddress" : "noreply@innov.energy" + "SenderName" : "Inesco Energy", + "SenderAddress" : "noreply@inesco.energy" } diff --git a/csharp/App/Backend/Program.cs b/csharp/App/Backend/Program.cs index 50f03dab0..c32572eff 100644 --- a/csharp/App/Backend/Program.cs +++ b/csharp/App/Backend/Program.cs @@ -28,9 +28,8 @@ public static class Program RabbitMqManager.InitializeEnvironment(); RabbitMqManager.StartRabbitMqConsumer().SupressAwaitWarning(); - WebsocketManager.MonitorSalimaxInstallationTable().SupressAwaitWarning(); - WebsocketManager.MonitorSalidomoInstallationTable().SupressAwaitWarning(); - WebsocketManager.MonitorSodistoreInstallationTable().SupressAwaitWarning(); + + WebsocketManager.MonitorInstallationTable().SupressAwaitWarning(); // Task.Run(() => DeleteOldDataFromS3.DeleteOldData()); diff --git a/csharp/App/Backend/Relations/Session.cs b/csharp/App/Backend/Relations/Session.cs index 067761a51..6128ce237 100644 --- a/csharp/App/Backend/Relations/Session.cs +++ b/csharp/App/Backend/Relations/Session.cs @@ -49,6 +49,10 @@ public class Session : Relation AccessToSalidomo = user.AccessibleInstallations(product: (int)ProductType.Salidomo).ToList().Count > 0; AccessToSodistoreMax = user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count > 0; AccessToSodioHome = user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count > 0; + + Console.WriteLine("salimax" +user.AccessibleInstallations(product: (int)ProductType.Salimax).ToList().Count); + Console.WriteLine("AccessToSodistoreMax" +user.AccessibleInstallations(product: (int)ProductType.SodiStoreMax).ToList().Count); + Console.WriteLine("sodio" + user.AccessibleInstallations(product: (int)ProductType.SodioHome).ToList().Count); } private static String CreateToken() diff --git a/csharp/App/Backend/Websockets/WebsockerManager.cs b/csharp/App/Backend/Websockets/WebsockerManager.cs index 07dacead3..b87c8ffdd 100644 --- a/csharp/App/Backend/Websockets/WebsockerManager.cs +++ b/csharp/App/Backend/Websockets/WebsockerManager.cs @@ -13,101 +13,41 @@ public static class WebsocketManager { public static Dictionary InstallationConnections = new Dictionary(); - //Every 1 minute, check the timestamp of the latest received message for every installation. - //If the difference between the two timestamps is more than two minutes, we consider this Salimax installation unavailable. - public static async Task MonitorSalimaxInstallationTable() + public static async Task MonitorInstallationTable() { - while (true){ - lock (InstallationConnections){ - // Console.WriteLine("MONITOR SALIMAX INSTALLATIONS\n"); - foreach (var installationConnection in InstallationConnections){ - - if (installationConnection.Value.Product==(int)ProductType.Salimax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)){ - - // Console.WriteLine("Installation ID is "+installationConnection.Key); - // Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp); + while (true) + { + lock (InstallationConnections) + { + Console.WriteLine("Monitoring installation table..."); + foreach (var installationConnection in InstallationConnections) + { + Console.WriteLine("installationConnection ID is " + installationConnection.Key + "latest timestamp is" +installationConnection.Value.Timestamp + "product is "+ installationConnection.Value.Product + + "and time diff is "+ (DateTime.Now - installationConnection.Value.Timestamp)); + + if ((installationConnection.Value.Product == (int)ProductType.Salimax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) || + (installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60)) || + (installationConnection.Value.Product == (int)ProductType.SodioHome && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) || + (installationConnection.Value.Product == (int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) + ) + { + + Console.WriteLine("Installation ID is " + installationConnection.Key); + Console.WriteLine("installationConnection.Value.Timestamp is " + installationConnection.Value.Timestamp); // Console.WriteLine("diff is "+(DateTime.Now-installationConnection.Value.Timestamp)); - - installationConnection.Value.Status = (int)StatusType.Offline; - Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.Salimax && f.Id == installationConnection.Key); - installation.Status = (int)StatusType.Offline; - installation.Apply(Db.Update); - if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);} - } - } - // Console.WriteLine("FINISHED MONITORING SALIMAX INSTALLATIONS\n"); - } - - await Task.Delay(TimeSpan.FromMinutes(1)); - } - } - - //Every 1 minute, check the timestamp of the latest received message for every installation. - //If the difference between the two timestamps is more than 1 hour, we consider this Salidomo installation unavailable. - public static async Task MonitorSalidomoInstallationTable() - { - while (true){ - //Console.WriteLine("TRY TO LOCK FOR MONITOR SALIDOMO INSTALLATIONS\n"); - lock (InstallationConnections){ - //Console.WriteLine("MONITOR SALIDOMO INSTALLATIONS\n"); - foreach (var installationConnection in InstallationConnections) - { - //Console.WriteLine("Installation ID is "+installationConnection.Key); - // if (installationConnection.Value.Product == (int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) < TimeSpan.FromMinutes(60)){ - // Console.WriteLine("Installation ID is "+installationConnection.Key + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp)); - // } - if (installationConnection.Value.Product==(int)ProductType.Salidomo && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(60)) - { - - //Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp); - //Console.WriteLine("timestamp now is is "+(DateTime.Now)); - - Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.Salidomo && f.Id == installationConnection.Key); - //Console.WriteLine("Installation ID is "+installation.Name + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp)); + installationConnection.Value.Status = (int)StatusType.Offline; + Installation installation = Db.Installations.FirstOrDefault(f => f.Product == installationConnection.Value.Product && f.Id == installationConnection.Key); installation.Status = (int)StatusType.Offline; installation.Apply(Db.Update); - - installationConnection.Value.Status = (int)StatusType.Offline; - if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);} - //else{Console.WriteLine("NONE IS CONNECTED TO THAT INSTALLATION-------------------------------------------------------------");} + if (installationConnection.Value.Connections.Count > 0) + { + InformWebsocketsForInstallation(installationConnection.Key); + } } } - //Console.WriteLine("FINISHED WITH UPDATING\n"); } - await Task.Delay(TimeSpan.FromMinutes(1)); - } - } - - //Every 1 minute, check the timestamp of the latest received message for every installation. - //If the difference between the two timestamps is more than two minutes, we consider this Sodistore installation unavailable. - public static async Task MonitorSodistoreInstallationTable() - { - while (true){ - //Console.WriteLine("TRY TO LOCK FOR MONITOR SODISTORE INSTALLATIONS\n"); - lock (InstallationConnections){ - //Console.WriteLine("MONITOR SODISTORE INSTALLATIONS\n"); - foreach (var installationConnection in InstallationConnections) - { - if (installationConnection.Value.Product==(int)ProductType.SodiStoreMax && (DateTime.Now - installationConnection.Value.Timestamp) > TimeSpan.FromMinutes(2)) - { - - //Console.WriteLine("installationConnection.Value.Timestamp is "+installationConnection.Value.Timestamp); - //Console.WriteLine("timestamp now is is "+(DateTime.Now)); - - Installation installation = Db.Installations.FirstOrDefault(f => f.Product == (int)ProductType.SodiStoreMax && f.Id == installationConnection.Key); - //Console.WriteLine("Installation ID is "+installation.Name + " diff is "+(DateTime.Now-installationConnection.Value.Timestamp)); - installation.Status = (int)StatusType.Offline; - installation.Apply(Db.Update); - - installationConnection.Value.Status = (int)StatusType.Offline; - if (installationConnection.Value.Connections.Count > 0){InformWebsocketsForInstallation(installationConnection.Key);} - //else{Console.WriteLine("NONE IS CONNECTED TO THAT INSTALLATION-------------------------------------------------------------");} - } - } - //Console.WriteLine("FINISHED WITH UPDATING\n"); - } await Task.Delay(TimeSpan.FromMinutes(1)); } } @@ -140,6 +80,7 @@ public static class WebsocketManager } } + public static async Task HandleWebSocketConnection(WebSocket currentWebSocket) { var buffer = new byte[4096]; @@ -155,6 +96,7 @@ public static class WebsocketManager var message = Encoding.UTF8.GetString(buffer, 0, result.Count); var installationIds = JsonSerializer.Deserialize(message); + Console.WriteLine("Received Websocket message: " + message); //This is a ping message to keep the connection alive, reply with a pong if (installationIds[0] == -1) diff --git a/firmware/Cerbo_Release/CerboReleaseFiles/flows.json b/firmware/Cerbo_Release/CerboReleaseFiles/flows.json index ebce8f6f3..8fc15129c 100644 --- a/firmware/Cerbo_Release/CerboReleaseFiles/flows.json +++ b/firmware/Cerbo_Release/CerboReleaseFiles/flows.json @@ -5191,7 +5191,7 @@ "order": 5, "width": 0, "height": 0, - "format": " Battery Monitor \n", + "format": " Battery Monitor \n", "storeOutMessages": true, "fwdInMessages": true, "resendOnRefresh": true, diff --git a/typescript/frontend-marios2/src/contexts/WebSocketContextProvider.tsx b/typescript/frontend-marios2/src/contexts/WebSocketContextProvider.tsx index b24ba238e..cdd754462 100644 --- a/typescript/frontend-marios2/src/contexts/WebSocketContextProvider.tsx +++ b/typescript/frontend-marios2/src/contexts/WebSocketContextProvider.tsx @@ -55,112 +55,6 @@ const WebSocketContextProvider = ({ children }: { children: ReactNode }) => { [] ); - useEffect(() => { - // if (sortedInstallations) { - // const tokenString = localStorage.getItem('token'); - // const token = tokenString !== null ? tokenString : ''; - // const urlWithToken = `wss://monitor.innov.energy/api/CreateWebSocket?authToken=${token}`; - // - // const socket = new WebSocket(urlWithToken); - // // Connection opened - // socket.addEventListener('open', (event) => { - // socket.send( - // JSON.stringify( - // sortedInstallations.map((installation) => installation.id) - // ) - // ); - // }); - // - // // Periodically send ping messages to keep the connection alive - // const pingInterval = setInterval(() => { - // if (socket.readyState === WebSocket.OPEN) { - // socket.send(JSON.stringify([-1])); - // } - // }, 10000); // Send a ping every 10 seconds - // - // let messageBuffer = []; - // let isProcessing = false; - // - // socket.addEventListener('message', (event) => { - // const message = JSON.parse(event.data); // Parse the JSON data - // - // if (Array.isArray(message)) { - // message.forEach((item) => { - // console.log('status is ' + item.status); - // // Update status and testingMode for each installation received - // // updateInstallationStatus(item.id, item.status, item.testingMode); - // }); - // } - // // } else if (message.id !== -1) { - // // // Handle individual messages for installations - // // updateInstallationStatus( - // // message.id, - // // message.status, - // // message.testingMode - // // ); - // // } - // - // // if (Array.isArray(message)) { - // // // Existing code for handling arrays, if necessary - // // setInstallationMode((prevMode) => { - // // const newMode = new Map(prevMode); - // // message.forEach((item) => { - // // newMode.set(item.id, item.testingMode); - // // }); - // // return newMode; - // // }); - // // - // // setInstallationStatus((prevStatus) => { - // // const newStatus = new Map(prevStatus); - // // message.forEach((item) => { - // // newStatus.set(item.id, item.status); - // // }); - // // return newStatus; - // // }); - // // } else if (message.id != -1) { - // // // Accumulate messages in the buffer - // // messageBuffer.push(message); - // // - // // // Process the buffer if not already processing - // // if (!isProcessing) { - // // isProcessing = true; - // // - // // // Use setTimeout to process the buffer periodically - // // setTimeout(() => { - // // const newInstallationMode = new Map(); - // // const newInstallationStatus = new Map(); - // // - // // // Process all accumulated messages - // // messageBuffer.forEach((msg) => { - // // newInstallationMode.set(msg.id, msg.testingMode); - // // newInstallationStatus.set(msg.id, msg.status); - // // }); - // // - // // // Update the state with the accumulated messages - // // setInstallationMode( - // // (prevMode) => new Map([...prevMode, ...newInstallationMode]) - // // ); - // // setInstallationStatus( - // // (prevStatus) => - // // new Map([...prevStatus, ...newInstallationStatus]) - // // ); - // // - // // // Clear the buffer after processing - // // messageBuffer = []; - // // isProcessing = false; // Reset processing flag - // // }, 100); // Adjust the delay as needed to control processing frequency - // // } - // // } - // }); - // - // setSocket(socket); - // } - }, [sortedInstallations]); - - // const openSocket = (installations: I_Installation[]) => { - // setInstallations(installations); - // }; - const openSocket = (installations) => { // setSortedInstallations(installations.sort((a, b) => b.status - a.status)); // Sort installations by status }; @@ -168,19 +62,7 @@ const WebSocketContextProvider = ({ children }: { children: ReactNode }) => { const closeSocket = () => { // socket.close(); }; - - // const getStatus = (installationId: number) => { - // return installationStatus.get(installationId); - // // if (installationStatus.has(installationId)) { - // // installationStatus.get(installationId); - // // } else { - // // return -2; - // // } - // }; - - // const getTestingMode = (installationId: number) => { - // return installationMode.get(installationId); - // }; + return ( Date: Mon, 1 Sep 2025 13:07:26 +0200 Subject: [PATCH 3/4] Add Data Collector App --- csharp/DataCollectorWebApp/Controller.cs | 271 ++++++++++++++++++ .../DataCollectorWebApp.csproj | 14 + csharp/DataCollectorWebApp/Program.cs | 19 ++ csharp/InnovEnergy.sln | 7 + 4 files changed, 311 insertions(+) create mode 100644 csharp/DataCollectorWebApp/Controller.cs create mode 100644 csharp/DataCollectorWebApp/DataCollectorWebApp.csproj create mode 100644 csharp/DataCollectorWebApp/Program.cs diff --git a/csharp/DataCollectorWebApp/Controller.cs b/csharp/DataCollectorWebApp/Controller.cs new file mode 100644 index 000000000..e5b13d481 --- /dev/null +++ b/csharp/DataCollectorWebApp/Controller.cs @@ -0,0 +1,271 @@ +using System.Text; +using Microsoft.AspNetCore.Mvc; + +namespace DataCollectorWebApp; + +public class LoginResponse +{ + public string Token { get; set; } + public object User { get; set; } // or a User class if needed + public bool AccessToSalimax { get; set; } + public bool AccessToSalidomo { get; set; } + public bool AccessToSodiohome { get; set; } + public bool AccessToSodistoreMax { get; set; } +} + + +public class Installation +{ + //Each installation has 2 roles, a read role and a write role. + //There are 2 keys per role a public key and a secret + //Product can be 0 or 1, 0 for Salimax, 1 for Salidomo + public String Name { get; set; } + public String Location { get; set; } + public String Region { get; set; } = ""; + public String Country { get; set; } = ""; + public String VpnIp { get; set; } = ""; + public String InstallationName { get; set; } = ""; + + public String S3Region { get; set; } = "sos-ch-dk-2"; + public String S3Provider { get; set; } = "exo.io"; + public String S3WriteKey { get; set; } = ""; + public String S3Key { get; set; } = ""; + public String S3WriteSecret { get; set; } = ""; + public String S3Secret { get; set; } = ""; + public int S3BucketId { get; set; } = 0; + public String ReadRoleId { get; set; } = ""; + public String WriteRoleId { get; set; } = ""; + public Boolean TestingMode { get; set; } = false; + public int Status { get; set; } = -1; + public int Product { get; set; } = 0; + public int Device { get; set; } = 0; + public string SerialNumber { get; set; } = ""; + + public String OrderNumbers { get; set; } + public String VrmLink { get; set; } = ""; +} + +[Controller] +public class InstallationsController : Controller +{ + + + +[HttpGet] +[Route("/Installations")] +[Produces("text/html")] +public async Task Index() +{ + const string HtmlHeader = @" + + + Inesco Energy Installations Overview + + + +

Installation Overview

+ + + + + + + + + + + + +"; + + const string HtmlFooter = @" + +
NameProductLocationVPN IPStatus
+ + +"; + + + + string GetProductName(int productId) + { + return productId switch + { + 0 => "Salimax", + 1 => "Salidomo", + 2 => "SodioHome", + 3 => "SodistoreMax", + }; + } + + string GetStatusHtml(int status) + { + if (status == -1) + { + return "×"; + } + + + var statusClass = $"status-{status}"; + var title = status switch + { + 0 => "Online", + 1 => "Warning", + 2 => "Error", + _ => "Unknown" + }; + + return $""; + } + + + string BuildRowHtml(Installation i) => $@" + + {i.Name} + {GetProductName(i.Product)} + {i.Location} + {i.VpnIp} + {GetStatusHtml(i.Status)} + +"; + + var installations = await FetchInstallationsFromApi(); + + var sb = new StringBuilder(); + sb.Append(HtmlHeader); + + foreach (var i in installations) + { + sb.Append(BuildRowHtml(i)); + } + + sb.Append(HtmlFooter); + return Content(sb.ToString(), "text/html"); +} + + + public async Task?> FetchInstallationsFromApi() + { + + var username = "baumgartner@innov.energy"; + var password = "1234"; + + using var http = new HttpClient { BaseAddress = new Uri("https://monitor.inesco.energy/api/") }; + + // Step 1: Login + var loginResponse = await http.PostAsync($"Login?username={username}&password={password}", null); + if (!loginResponse.IsSuccessStatusCode) + { + Console.WriteLine("Login failed with status code {StatusCode}", loginResponse.StatusCode); + return null; + } + + var loginData = await loginResponse.Content.ReadFromJsonAsync(); + if (loginData?.Token is null) + { + Console.WriteLine("Login succeeded but token was missing"); + return null; + } + + var token = loginData.Token; + Console.WriteLine($"Token: {token}"); + var installations = new List(); + + var getInstallationsRequestResponse = await http.GetAsync($"GetAllInstallationsFromProduct?product=0&authToken={token}"); + + var newInstallations= await getInstallationsRequestResponse.Content.ReadFromJsonAsync>(); + if (newInstallations != null) + { + installations.AddRange(newInstallations); + } + + getInstallationsRequestResponse = await http.GetAsync($"GetAllInstallationsFromProduct?product=1&authToken={token}"); + newInstallations= await getInstallationsRequestResponse.Content.ReadFromJsonAsync>(); + if (newInstallations != null) + { + installations.AddRange(newInstallations); + } + + getInstallationsRequestResponse = await http.GetAsync($"GetAllInstallationsFromProduct?product=2&authToken={token}"); + newInstallations= await getInstallationsRequestResponse.Content.ReadFromJsonAsync>(); + if (newInstallations != null) + { + installations.AddRange(newInstallations); + } + + getInstallationsRequestResponse = await http.GetAsync($"GetAllInstallationsFromProduct?product=3&authToken={token}"); + newInstallations= await getInstallationsRequestResponse.Content.ReadFromJsonAsync>(); + if (newInstallations != null) + { + installations.AddRange(newInstallations); + } + + + //Console.WriteLine("Installations retrieved ",installations); + return installations; + } + +} \ No newline at end of file diff --git a/csharp/DataCollectorWebApp/DataCollectorWebApp.csproj b/csharp/DataCollectorWebApp/DataCollectorWebApp.csproj new file mode 100644 index 000000000..3bde501d5 --- /dev/null +++ b/csharp/DataCollectorWebApp/DataCollectorWebApp.csproj @@ -0,0 +1,14 @@ + + + + net7.0 + enable + enable + + + + + + + + diff --git a/csharp/DataCollectorWebApp/Program.cs b/csharp/DataCollectorWebApp/Program.cs new file mode 100644 index 000000000..6420e3bf6 --- /dev/null +++ b/csharp/DataCollectorWebApp/Program.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Builder; + +namespace InnovEnergy.App.DataCollectorWebApp; + +public static class Program +{ + public static async Task Main(string[] args) + { + Console.WriteLine("Starting DataCollectorWebApp"); + + var builder = WebApplication.CreateBuilder(args); + + builder.Services.AddControllers(); + var app = builder.Build(); + + app.MapControllers(); + await app.RunAsync(); + } +} \ No newline at end of file diff --git a/csharp/InnovEnergy.sln b/csharp/InnovEnergy.sln index c5ec2b4fa..b81e69618 100644 --- a/csharp/InnovEnergy.sln +++ b/csharp/InnovEnergy.sln @@ -103,6 +103,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GrowattCommunication", "App EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "WITGrowatt4-15K", "Lib\Devices\WITGrowatt4-15K\WITGrowatt4-15K.csproj", "{44DD9E5E-2AD3-4579-A47D-7A40FD28D369}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DataCollectorWebApp", "DataCollectorWebApp\DataCollectorWebApp.csproj", "{6069D487-DBAB-4253-BFA1-CF994B84BE49}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -274,6 +276,10 @@ Global {44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Debug|Any CPU.Build.0 = Debug|Any CPU {44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Release|Any CPU.ActiveCfg = Release|Any CPU {44DD9E5E-2AD3-4579-A47D-7A40FD28D369}.Release|Any CPU.Build.0 = Release|Any CPU + {6069D487-DBAB-4253-BFA1-CF994B84BE49}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6069D487-DBAB-4253-BFA1-CF994B84BE49}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6069D487-DBAB-4253-BFA1-CF994B84BE49}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6069D487-DBAB-4253-BFA1-CF994B84BE49}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {CF4834CB-91B7-4172-AC13-ECDA8613CD17} = {145597B4-3E30-45E6-9F72-4DD43194539A} @@ -321,5 +327,6 @@ Global {39B83793-49DB-4940-9C25-A7F944607407} = {145597B4-3E30-45E6-9F72-4DD43194539A} {DC0BE34A-368F-46DC-A081-70C9A1EFE9C0} = {145597B4-3E30-45E6-9F72-4DD43194539A} {44DD9E5E-2AD3-4579-A47D-7A40FD28D369} = {4931A385-24DC-4E78-BFF4-356F8D6D5183} + {6069D487-DBAB-4253-BFA1-CF994B84BE49} = {145597B4-3E30-45E6-9F72-4DD43194539A} EndGlobalSection EndGlobal From 57b7a7198419a7de64aa855fbce50cf77b710e86 Mon Sep 17 00:00:00 2001 From: Noe Date: Mon, 1 Sep 2025 13:07:52 +0200 Subject: [PATCH 4/4] updated documentation for S3 Extracting Tool --- S3ExtractingTool/ExtractS3README.txt | 8 ++++-- S3ExtractingTool/extractS3data.py | 42 +++++++++++++++++----------- 2 files changed, 32 insertions(+), 18 deletions(-) diff --git a/S3ExtractingTool/ExtractS3README.txt b/S3ExtractingTool/ExtractS3README.txt index c4cd1cb04..18e0e7119 100644 --- a/S3ExtractingTool/ExtractS3README.txt +++ b/S3ExtractingTool/ExtractS3README.txt @@ -77,14 +77,18 @@ The script dynamically generates headers for the output CSV file based on the ke extracted data, providing a clear and understandable format for subsequent analysis. The headers correspond to the keys used for data extraction, making it easy to identify and analyze the extracted data. + 4)Advanced Data Processing Capabilities: -Booleans as Numbers: The --booleans_as_numbers flag allows users to convert boolean values (True/False) into numeric representations (1/0). This feature +i) Booleans as Numbers: The --booleans_as_numbers flag allows users to convert boolean values (True/False) into numeric representations (1/0). This feature is particularly useful for analytical tasks that require numerical data processing. +ii) Sampling Stepsize: The --sampling_stepsize parameter enables users to define the granularity of the time range for data extraction. By specifying the number +of 1 minute intervals, users can adjust the sampling interval, allowing for flexible data retrieval based on time. Example Command: -python3 extractS3data.py 1749062721 1749106001 --keys AcDc/SystemControl/ResetAlarmsAndWarnings,AcDc/Devices/1/Status/Ac/L1/Voltage --bucket-number 12 --product_name=SodistoreMax +python3 extractS3data.py 1749062721 1749106001 --keys AcDc/SystemControl/ResetAlarmsAndWarnings,AcDc/Devices/1/Status/Ac/L1/Voltage --bucket-number 12 --product_name=SodistoreMax --sampling_stepsize 2 --booleans_as_numbers This command extracts data for AcDc/SystemControl/ResetAlarmsAndWarnings and AcDc/Devices/1/Status/Ac/L1/Voltage keys from bucket number 12, between the specified timestamps, with boolean values converted to numbers. +The script will fetch data in 2 minutes intervals diff --git a/S3ExtractingTool/extractS3data.py b/S3ExtractingTool/extractS3data.py index a40765351..167540951 100644 --- a/S3ExtractingTool/extractS3data.py +++ b/S3ExtractingTool/extractS3data.py @@ -18,14 +18,15 @@ def extract_timestamp(filename): except ValueError: return 0 +import subprocess -def list_files_in_range(start_timestamp, end_timestamp, sampling_stepsize,product_type,bucket_number): - if product_type == "Salimax" or product_type=="SodistoreMax": +def list_files_in_range(start_timestamp, end_timestamp, sampling_stepsize, product_type, bucket_number): + if product_type in ["Salimax", "SodistoreMax"]: hash = "3e5b3069-214a-43ee-8d85-57d72000c19d" elif product_type == "Salidomo": hash = "c0436b6a-d276-4cd8-9c44-1eae86cf5d0e" else: - raise ValueError("Invalid product type option. Use Salimax or Salidomo or SodistoreMax") + raise ValueError("Invalid product type option.") # Find common prefix common_prefix = "" @@ -43,20 +44,31 @@ def list_files_in_range(start_timestamp, end_timestamp, sampling_stepsize,produc output = subprocess.check_output(s3cmd_command, shell=True, text=True) files = [line.split()[-1] for line in output.strip().split("\n") if line.strip()] filenames = [] + count=0 + for f in files: - name = f.split("/")[-1] # e.g., 1748802020.json - timestamp_str = name.split(".")[0] # extract '1748802020' - if timestamp_str.isdigit() and int(timestamp_str) <= int(end_timestamp): - filenames.append(name) - else: - break + name = f.split("/")[-1] + timestamp_str = name.split(".")[0] + + if timestamp_str.isdigit(): + timestamp = int(timestamp_str) + + + if start_timestamp <= timestamp <= end_timestamp : + if count % sampling_stepsize == 0: + filenames.append(name) + count += 1 + + print(filenames) return filenames + except subprocess.CalledProcessError: print(f"No files found for prefix {common_prefix}") return [] + def get_nested_value(data, key_path): try: for key in key_path: @@ -151,7 +163,7 @@ def download_files(bucket_number, filenames_to_download, product_type): print(f"Files with prefix '{filename}' downloaded successfully.") decompress_file(os.path.join(output_directory, filename), output_directory) except subprocess.CalledProcessError as e: - # print(f"Error downloading files: {e}") + print(f"Error downloading files: {e}") continue else: print(f"File '{filename}.json' already exists locally. Skipping download.") @@ -187,7 +199,7 @@ def get_last_component(path): path_without_slashes = path.replace('/', '') return path_without_slashes -def download_and_process_files(bucket_number, start_timestamp, end_timestamp, sampling_stepsize, keys, booleans_as_numbers, exact_match, product_type): +def download_and_process_files(bucket_number, start_timestamp, end_timestamp, sampling_stepsize, keys, booleans_as_numbers, product_type): output_directory = f"S3cmdData_{bucket_number}" #if os.path.exists(output_directory): @@ -200,7 +212,7 @@ def download_and_process_files(bucket_number, start_timestamp, end_timestamp, sa filenames_to_check = list_files_in_range(start_timestamp, end_timestamp, sampling_stepsize,product_type,bucket_number) existing_files = [filename for filename in filenames_to_check if os.path.exists(os.path.join(output_directory, f"{filename}.json"))] files_to_download = set(filenames_to_check) - set(existing_files) - print(files_to_download) + #print(files_to_download) #if os.listdir(output_directory): # print("Files already exist in the local folder. Skipping download.") @@ -231,9 +243,8 @@ def main(): parser.add_argument('end_timestamp', type=int, help='The end timestamp for the range (even number)') parser.add_argument('--keys', type=parse_keys, required=True, help='The part to match from each CSV file, can be a single key or a comma-separated list of keys') parser.add_argument('--bucket-number', type=int, required=True, help='The number of the bucket to download from') - parser.add_argument('--sampling_stepsize', type=int, required=False, default=1, help='The number of 2sec intervals, which define the length of the sampling interval in S3 file retrieval') + parser.add_argument('--sampling_stepsize', type=int, required=False, default=1, help='The number of 1 minute intervals, which define the length of the sampling interval in S3 file retrieval') parser.add_argument('--booleans_as_numbers', action="store_true", required=False, help='If key used, then booleans are converted to numbers [0/1], if key not used, then booleans maintained as text [False/True]') - parser.add_argument('--exact_match', action="store_true", required=False, help='If key used, then key has to match exactly "=", else it is enough that key is found "in" text') parser.add_argument('--product_name', required=True, help='Use Salimax, Salidomo or SodistoreMax') args = parser.parse_args() @@ -243,14 +254,13 @@ def main(): bucket_number = args.bucket_number sampling_stepsize = args.sampling_stepsize booleans_as_numbers = args.booleans_as_numbers - exact_match = args.exact_match # new arg for product type product_type = args.product_name if start_timestamp >= end_timestamp: print("Error: start_timestamp must be smaller than end_timestamp.") return - download_and_process_files(bucket_number, start_timestamp, end_timestamp, sampling_stepsize, keys, booleans_as_numbers, exact_match, product_type) + download_and_process_files(bucket_number, start_timestamp, end_timestamp, sampling_stepsize, keys, booleans_as_numbers, product_type) if __name__ == "__main__": main()