From 1ad2e79be4e5cda709bef87a4d4f4a3b2eb9b288 Mon Sep 17 00:00:00 2001 From: atef Date: Mon, 10 Jun 2024 14:59:42 +0200 Subject: [PATCH 1/8] Add number of strings --- csharp/Lib/Devices/AMPT/AmptDevices.cs | 1 + csharp/Lib/Devices/AMPT/AmptStatus.cs | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/csharp/Lib/Devices/AMPT/AmptDevices.cs b/csharp/Lib/Devices/AMPT/AmptDevices.cs index b1d6d3ec2..1674baffe 100644 --- a/csharp/Lib/Devices/AMPT/AmptDevices.cs +++ b/csharp/Lib/Devices/AMPT/AmptDevices.cs @@ -83,6 +83,7 @@ public class AmptDevices return new AmptStatus { Dc = dc, + NbrOfStrings = nStringOptimizers, Strings = strings, DcWh = dailyOutputEnergy }; diff --git a/csharp/Lib/Devices/AMPT/AmptStatus.cs b/csharp/Lib/Devices/AMPT/AmptStatus.cs index bbaa5fd05..259bc86f5 100644 --- a/csharp/Lib/Devices/AMPT/AmptStatus.cs +++ b/csharp/Lib/Devices/AMPT/AmptStatus.cs @@ -5,8 +5,9 @@ namespace InnovEnergy.Lib.Devices.AMPT; public class AmptStatus : IMppt { - public required DcBus Dc { get; init; } - public required IReadOnlyList Strings { get; init; } - public required Double DcWh { get; init; } //Daily integrated string output energy in Wh + public required DcBus Dc { get; init; } + public required UInt16 NbrOfStrings { get; init; } + public required IReadOnlyList Strings { get; init; } + public required Double DcWh { get; init; } //Daily integrated string output energy in Wh } From 9481a9be668bc2b09b8c8775c9775cc891d08f13 Mon Sep 17 00:00:00 2001 From: Noe Date: Mon, 10 Jun 2024 15:12:11 +0200 Subject: [PATCH 2/8] Added history tab Fixed bug when zooming --- csharp/App/SaliMax/src/Program.cs | 55 ++-- csharp/App/SaliMax/src/S3Config.cs | 3 +- .../src/Resources/routes.json | 1 + .../dashboards/BatteryView/MainStats.tsx | 4 +- .../Configuration/Configuration.tsx | 30 +- .../content/dashboards/History/History.tsx | 275 ++++++++++++++++++ .../dashboards/Installations/Installation.tsx | 13 + .../dashboards/Installations/index.tsx | 21 +- .../content/dashboards/Overview/overview.tsx | 4 +- .../SalidomoInstallations/Installation.tsx | 39 ++- .../content/dashboards/Topology/Topology.tsx | 1 + .../src/interfaces/S3Types.tsx | 9 + 12 files changed, 404 insertions(+), 51 deletions(-) create mode 100644 typescript/frontend-marios2/src/content/dashboards/History/History.tsx diff --git a/csharp/App/SaliMax/src/Program.cs b/csharp/App/SaliMax/src/Program.cs index ee0a76355..e54ddb987 100644 --- a/csharp/App/SaliMax/src/Program.cs +++ b/csharp/App/SaliMax/src/Program.cs @@ -678,35 +678,36 @@ internal static class Program // This is temporary for Wittman, but now it's for all Instalattion await File.WriteAllTextAsync("/var/www/html/status.csv", csv.SplitLines().Where(l => !l.Contains("Secret")).JoinLines()); - + + var response = await request.PutAsync(new StringContent(csv)); // Compress CSV data to a byte array - byte[] compressedBytes; - using (var memoryStream = new MemoryStream()) - { - //Create a zip directory and put the compressed file inside - using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) - { - var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive - using (var entryStream = entry.Open()) - using (var writer = new StreamWriter(entryStream)) - { - writer.Write(csv); - } - } - - compressedBytes = memoryStream.ToArray(); - } - - // Encode the compressed byte array as a Base64 string - string base64String = Convert.ToBase64String(compressedBytes); - - // Create StringContent from Base64 string - var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64"); - - // Upload the compressed data (ZIP archive) to S3 - var response = await request.PutAsync(stringContent); - + // byte[] compressedBytes; + // using (var memoryStream = new MemoryStream()) + // { + // //Create a zip directory and put the compressed file inside + // using (var archive = new ZipArchive(memoryStream, ZipArchiveMode.Create, true)) + // { + // var entry = archive.CreateEntry("data.csv", CompressionLevel.SmallestSize); // Add CSV data to the ZIP archive + // using (var entryStream = entry.Open()) + // using (var writer = new StreamWriter(entryStream)) + // { + // writer.Write(csv); + // } + // } + // + // compressedBytes = memoryStream.ToArray(); + // } + // + // // Encode the compressed byte array as a Base64 string + // string base64String = Convert.ToBase64String(compressedBytes); + // + // // Create StringContent from Base64 string + // var stringContent = new StringContent(base64String, Encoding.UTF8, "application/base64"); + // + // // Upload the compressed data (ZIP archive) to S3 + // var response = await request.PutAsync(stringContent); + // if (response.StatusCode != 200) { Console.WriteLine("ERROR: PUT"); diff --git a/csharp/App/SaliMax/src/S3Config.cs b/csharp/App/SaliMax/src/S3Config.cs index 741a5c565..cbf2a92f9 100644 --- a/csharp/App/SaliMax/src/S3Config.cs +++ b/csharp/App/SaliMax/src/S3Config.cs @@ -70,7 +70,8 @@ public record S3Config // CanonicalizedResource; - contentType = "application/base64; charset=utf-8"; + //contentType = "application/base64; charset=utf-8"; + contentType = "text/plain; charset=utf-8"; var payload = $"{method}\n{md5Hash}\n{contentType}\n{date}\n/{bucket.Trim('/')}/{s3Path.Trim('/')}"; using var hmacSha1 = new HMACSHA1(UTF8.GetBytes(s3Secret)); diff --git a/typescript/frontend-marios2/src/Resources/routes.json b/typescript/frontend-marios2/src/Resources/routes.json index f86097f4b..538990ff5 100644 --- a/typescript/frontend-marios2/src/Resources/routes.json +++ b/typescript/frontend-marios2/src/Resources/routes.json @@ -15,6 +15,7 @@ "live": "live", "information": "information", "configuration": "configuration", + "history": "history", "mainstats": "mainstats", "detailed_view": "detailed_view/" } diff --git a/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx index 7861a57c8..63ca5b839 100644 --- a/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/BatteryView/MainStats.tsx @@ -246,8 +246,8 @@ function MainStats(props: MainStatsProps) { chartOverview: BatteryOverviewInterface; }> = transformInputToBatteryViewData( props.s3Credentials, - UnixTime.fromTicks(startX), - UnixTime.fromTicks(endX) + UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)), + UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2)) ); resultPromise diff --git a/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx index 66b682adf..449d12308 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx @@ -15,7 +15,7 @@ import { Typography, useTheme } from '@mui/material'; -import React, { useState } from 'react'; +import React, { useContext, useState } from 'react'; import { FormattedMessage } from 'react-intl'; import Button from '@mui/material/Button'; import { Close as CloseIcon } from '@mui/icons-material'; @@ -29,6 +29,8 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import dayjs from 'dayjs'; import axiosConfig from '../../../Resources/axiosConfig'; import utc from 'dayjs/plugin/utc'; +import { Action } from '../../../interfaces/S3Types'; +import { UserContext } from '../../../contexts/userContext'; interface ConfigurationProps { values: TopologyValues; @@ -82,6 +84,8 @@ function Configuration(props: ConfigurationProps) { const [updated, setUpdated] = useState(false); const [dateSelectionError, setDateSelectionError] = useState(''); const [isErrorDateModalOpen, setErrorDateModalOpen] = useState(false); + const context = useContext(UserContext); + const { currentUser, setUser } = context; const [formValues, setFormValues] = useState({ minimumSoC: props.values.minimumSoC[0].value, @@ -133,6 +137,13 @@ function Configuration(props: ConfigurationProps) { .toDate() }; + const historyAction: Action = { + configuration: configurationToSend, + date: new Date().toISOString().split('T')[0], // Gets the current date in YYYY-MM-DD format + time: new Date().toISOString().split('T')[1].split('.')[0], // Gets the current time in HH:MM:SS format + user: currentUser.name + }; + // console.log('will send ', dayjs(formValues.calibrationChargeDate)); setLoading(true); @@ -149,8 +160,21 @@ function Configuration(props: ConfigurationProps) { }); if (res) { - setUpdated(true); - setLoading(false); + const historyRes = await axiosConfig + .post( + `/UpdateActionHistory?installationId=${props.id}`, + historyAction + ) + .catch((err) => { + if (err.response) { + setError(true); + setLoading(false); + } + }); + if (historyRes) { + setUpdated(true); + setLoading(false); + } } } }; diff --git a/typescript/frontend-marios2/src/content/dashboards/History/History.tsx b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx new file mode 100644 index 000000000..49ffd8a68 --- /dev/null +++ b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx @@ -0,0 +1,275 @@ +import React, { useContext, useEffect, useState } from 'react'; +import { + Alert, + Card, + Container, + Divider, + Grid, + IconButton, + useTheme +} from '@mui/material'; +import Typography from '@mui/material/Typography'; +import { FormattedMessage } from 'react-intl'; +import axiosConfig from '../../../Resources/axiosConfig'; +import { AxiosError, AxiosResponse } from 'axios/index'; +import routes from '../../../Resources/routes.json'; +import { useNavigate } from 'react-router-dom'; +import { TokenContext } from '../../../contexts/tokenContext'; +import { Action } from '../../../interfaces/S3Types'; + +interface HistoryProps { + errorLoadingS3Data: boolean; + id: number; +} + +function HistoryOfActions(props: HistoryProps) { + const theme = useTheme(); + const searchParams = new URLSearchParams(location.search); + + const [history, setHistory] = useState([]); + const navigate = useNavigate(); + const tokencontext = useContext(TokenContext); + const { removeToken } = tokencontext; + + useEffect(() => { + axiosConfig + .get(`/GetHistoryForInstallation?id=${props.id}`) + .then((res: AxiosResponse) => { + setHistory(res.data); + }) + .catch((err: AxiosError) => { + if (err.response && err.response.status == 401) { + removeToken(); + navigate(routes.login); + } + }); + }, []); + + return ( + + + + {history.length > 0 && ( + + +
+
+
+ + + +
+ +
+ + + +
+
+ + + +
+ {/**/} + {/* */} + {/* */} + {/* */} + {/*
*/} +
+ +
+ {history.map((action, index) => ( + <> + +
+
+ + {action.user} + +
+ +
+ + {action.date} + +
+
+ + {action.time} + +
+
+ + ))} +
+ +
+ )} + + {!props.errorLoadingS3Data && history.length == 0 && ( + + + + + )} +
+
+ + + {props.errorLoadingS3Data && ( + + + + + )} + +
+ ); +} + +export default HistoryOfActions; diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx index e812e91fa..909250ad5 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx @@ -23,6 +23,7 @@ import routes from '../../../Resources/routes.json'; import Information from '../Information/Information'; import BatteryView from '../BatteryView/BatteryView'; import { UserType } from '../../../interfaces/UserTypes'; +import HistoryOfActions from '../History/History'; interface singleInstallationProps { current_installation?: I_Installation; @@ -342,6 +343,18 @@ function Installation(props: singleInstallationProps) { } /> )} + {currentUser.userType == UserType.admin && ( + + } + /> + )} + {currentUser.userType == UserType.admin && ( (undefined); @@ -137,6 +138,15 @@ function InstallationTabs() { defaultMessage="Configuration" /> ) + }, + { + value: 'history', + label: ( + + ) } ] : currentUser.userType == UserType.partner @@ -248,6 +258,15 @@ function InstallationTabs() { defaultMessage="Configuration" /> ) + }, + { + value: 'history', + label: ( + + ) } ] : currentUser.userType == UserType.partner diff --git a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx index 8881c7f1e..1bd5daa80 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Overview/overview.tsx @@ -119,8 +119,8 @@ function Overview(props: OverviewProps) { chartOverview: overviewInterface; }> = transformInputToDailyData( props.s3Credentials, - UnixTime.fromTicks(startX), - UnixTime.fromTicks(endX) + UnixTime.fromTicks(startX).earlier(TimeSpan.fromHours(2)), + UnixTime.fromTicks(endX).earlier(TimeSpan.fromHours(2)) ); let isComponentMounted = true; diff --git a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx index b673f0d31..6e11a2a10 100644 --- a/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/SalidomoInstallations/Installation.tsx @@ -58,6 +58,28 @@ function Installation(props: singleInstallationProps) { const s3Credentials = { s3Bucket, ...S3data }; + const fetchDataOnlyOneTime = async () => { + var timeperiodToSearch = 70; + + for (var i = timeperiodToSearch; i > 0; i -= 2) { + const now = UnixTime.now().earlier(TimeSpan.fromSeconds(i)); + + const res = await fetchData(now, s3Credentials); + + if (res != FetchResult.notAvailable && res != FetchResult.tryLater) { + setConnected(true); + setFailedToCommunicateWithInstallation(0); + setValues( + extractValues({ + time: now, + value: res + }) + ); + return now; + } + } + }; + const fetchDataPeriodically = async () => { const now = UnixTime.now().earlier(TimeSpan.fromSeconds(20)); @@ -76,7 +98,7 @@ function Installation(props: singleInstallationProps) { return true; } else { setFailedToCommunicateWithInstallation((prevCount) => { - if (prevCount + 1 >= 3) { + if (prevCount + 1 >= 20) { setConnected(false); } return prevCount + 1; @@ -87,19 +109,6 @@ function Installation(props: singleInstallationProps) { } }; - const fetchDataOnlyOneTime = async () => { - let success = false; - const max_retransmissions = 3; - - for (let i = 0; i < max_retransmissions; i++) { - success = await fetchDataPeriodically(); - await new Promise((resolve) => setTimeout(resolve, 1000)); - if (success) { - break; - } - } - }; - useEffect(() => { let path = location.split('/'); @@ -118,7 +127,7 @@ function Installation(props: singleInstallationProps) { currentTab == 'live' || (location.includes('batteryview') && !location.includes('mainstats')) ) { - fetchDataPeriodically(); + fetchDataOnlyOneTime(); interval = setInterval(fetchDataPeriodically, 2000); } if (currentTab == 'configuration' || location.includes('mainstats')) { diff --git a/typescript/frontend-marios2/src/content/dashboards/Topology/Topology.tsx b/typescript/frontend-marios2/src/content/dashboards/Topology/Topology.tsx index 17a73e4b2..e8c4b8260 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Topology/Topology.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Topology/Topology.tsx @@ -222,6 +222,7 @@ function Topology(props: TopologyProps) { }} bottomBox={{ title: 'AC Loads', + data: props.values.islandBusToLoadOnIslandBusConnection, connected: props.values.loadOnIslandBusBox[0].value.toString() != diff --git a/typescript/frontend-marios2/src/interfaces/S3Types.tsx b/typescript/frontend-marios2/src/interfaces/S3Types.tsx index 7309b1cb5..078a011bc 100644 --- a/typescript/frontend-marios2/src/interfaces/S3Types.tsx +++ b/typescript/frontend-marios2/src/interfaces/S3Types.tsx @@ -1,3 +1,5 @@ +import { ConfigurationValues } from '../content/dashboards/Log/graph.util'; + export interface I_S3Credentials { s3Region: string; s3Provider: string; @@ -16,3 +18,10 @@ export interface ErrorMessage { deviceCreatedTheMessage: string; seen: boolean; } + +export interface Action { + configuration: ConfigurationValues; + date: string; + time: string; + user: string; +} From c19c32a1ecfb90043fd96663149d362a58e7b128 Mon Sep 17 00:00:00 2001 From: Noe Date: Mon, 10 Jun 2024 15:36:38 +0200 Subject: [PATCH 3/8] Marios push test --- .../src/Resources/routes.json | 1 + .../dashboards/Installations/Installation.tsx | 14 +++++ .../dashboards/Installations/index.tsx | 51 ++++++++++++------- .../src/content/dashboards/Log/graph.util.tsx | 14 +++++ .../src/content/dashboards/PvView/PvView.tsx | 0 5 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 typescript/frontend-marios2/src/content/dashboards/PvView/PvView.tsx diff --git a/typescript/frontend-marios2/src/Resources/routes.json b/typescript/frontend-marios2/src/Resources/routes.json index 538990ff5..88f219a2a 100644 --- a/typescript/frontend-marios2/src/Resources/routes.json +++ b/typescript/frontend-marios2/src/Resources/routes.json @@ -11,6 +11,7 @@ "overview": "overview", "manage": "manage", "batteryview": "batteryview", + "pvview": "pvview", "log": "log", "live": "live", "information": "information", diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx index 909250ad5..d6f4cf84b 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx @@ -310,6 +310,20 @@ function Installation(props: singleInstallationProps) { > } > + + + } + > + } diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx index 878ff948f..34d6e9e21 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx @@ -27,7 +27,8 @@ function InstallationTabs() { 'log', 'information', 'configuration', - 'history' + 'history', + 'pvview' ]; const [currentTab, setCurrentTab] = useState(undefined); @@ -139,14 +140,18 @@ function InstallationTabs() { /> ) }, + // { + // value: 'history', + // label: ( + // + // ) + // }, { - value: 'history', - label: ( - - ) + value: 'pvview', + label: } ] : currentUser.userType == UserType.partner @@ -168,6 +173,10 @@ function InstallationTabs() { /> ) }, + { + value: 'pvview', + label: + }, { value: 'information', @@ -227,6 +236,10 @@ function InstallationTabs() { /> ) }, + { + value: 'pvview', + label: + }, { value: 'manage', label: ( @@ -258,16 +271,16 @@ function InstallationTabs() { defaultMessage="Configuration" /> ) - }, - { - value: 'history', - label: ( - - ) } + // { + // value: 'history', + // label: ( + // + // ) + // } ] : currentUser.userType == UserType.partner ? [ @@ -299,6 +312,10 @@ function InstallationTabs() { /> ) }, + { + value: 'pvview', + label: + }, { value: 'information', 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 79237a1e5..ec4dad776 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx @@ -35,6 +35,8 @@ export type ConfigurationValues = { calibrationChargeDate: Date | null; }; +export interface Pv {} + export interface Battery { BatteryId: number; FwVersion: I_BoxDataValue; @@ -163,11 +165,19 @@ export type TopologyValues = { additionalCalibrationChargeDate: I_BoxDataValue[]; batteryView: Battery[]; + + pvView: Pv[]; }; type TopologyPaths = { [key in keyof TopologyValues]: string[] }; const batteryIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; +const PvPaths = [ + '/PvOnDc/Strings/%id%/Voltage', + '/PvOnDc/Strings/%id%/Current', + '/PvOnDc/Strings/%id%/Power' +]; + const batteryPaths = [ '/Battery/Devices/%id%/FwVersion', '/Battery/Devices/%id%/Dc/Power', @@ -290,6 +300,10 @@ export const topologyPaths: TopologyPaths = { batteryPaths.map((path) => path.replace('%id%', id.toString())) ), + pvView: batteryIds.flatMap((id) => + PvPaths.map((path) => path.replace('%id%', id.toString())) + ), + minimumSoC: ['/Config/MinSoc'], installedDcDcPower: ['/DcDc/SystemControl/NumberOfConnectedSlaves'], gridSetPoint: ['/Config/GridSetPoint'], diff --git a/typescript/frontend-marios2/src/content/dashboards/PvView/PvView.tsx b/typescript/frontend-marios2/src/content/dashboards/PvView/PvView.tsx new file mode 100644 index 000000000..e69de29bb From 0357980b3a4b7bfcdf008cd1e2c9979269de698b Mon Sep 17 00:00:00 2001 From: atef Date: Mon, 10 Jun 2024 15:44:21 +0200 Subject: [PATCH 4/8] fixing the library --- .../App/Backend/DataTypes/Methods/Session.cs | 10 +- .../src/AggregationService/Aggregator.cs | 4 +- .../IEM3kGridMeter/IEM3kGridMeterDevice.cs | 83 +-------- .../Protocols/Modbus/Channels/TcpChannel.cs | 104 +---------- .../Modbus/Clients/ModbusTcpClient.cs | 171 +----------------- 5 files changed, 13 insertions(+), 359 deletions(-) diff --git a/csharp/App/Backend/DataTypes/Methods/Session.cs b/csharp/App/Backend/DataTypes/Methods/Session.cs index f8ff5d571..d9346fbf0 100644 --- a/csharp/App/Backend/DataTypes/Methods/Session.cs +++ b/csharp/App/Backend/DataTypes/Methods/Session.cs @@ -75,14 +75,14 @@ public static class SessionMethods await Task.Run(() => { - Process process = new Process(); + var process = new Process(); process.StartInfo.FileName = "/bin/bash"; process.StartInfo.Arguments = $"{scriptPath} {vpnIp} {batteryNode} {version}"; process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.Start(); - string output = process.StandardOutput.ReadToEnd(); + var output = process.StandardOutput.ReadToEnd(); process.WaitForExit(); Console.WriteLine(output); }); @@ -121,7 +121,7 @@ public static class SessionMethods //Salimax installation - if (installation.Product==0) + if (installation.Product == 0) { return user is not null && user.UserType != 0 @@ -134,7 +134,7 @@ public static class SessionMethods } - if (installation.Product==1) + if (installation.Product == 1) { return user is not null && user.UserType != 0 @@ -153,7 +153,7 @@ public static class SessionMethods var original = Db.GetInstallationById(installation?.Id); //Salimax installation - if (installation.Product==0) + if (installation.Product == 0) { return user is not null diff --git a/csharp/App/SaliMax/src/AggregationService/Aggregator.cs b/csharp/App/SaliMax/src/AggregationService/Aggregator.cs index bd37c99d8..8a6a0ea70 100644 --- a/csharp/App/SaliMax/src/AggregationService/Aggregator.cs +++ b/csharp/App/SaliMax/src/AggregationService/Aggregator.cs @@ -227,8 +227,8 @@ public static class Aggregator Console.WriteLine($"Max SOC: {aggregatedData.MaxSoc}"); Console.WriteLine($"Min SOC: {aggregatedData.MinSoc}"); - Console.WriteLine($"ChargingBatteryPower: {aggregatedData.DischargingBatteryPower}"); - Console.WriteLine($"DischargingBatteryBattery: {aggregatedData.ChargingBatteryPower}"); + Console.WriteLine($"DischargingBatteryBattery: {aggregatedData.DischargingBatteryPower}"); + Console.WriteLine($"ChargingBatteryPower: {aggregatedData.ChargingBatteryPower}"); Console.WriteLine($"SumGridExportPower: {aggregatedData.GridExportPower}"); Console.WriteLine($"SumGridImportPower: {aggregatedData.GridImportPower}"); diff --git a/csharp/Lib/Devices/IEM3kGridMeter/IEM3kGridMeterDevice.cs b/csharp/Lib/Devices/IEM3kGridMeter/IEM3kGridMeterDevice.cs index 7a1106c5f..863a30c9f 100644 --- a/csharp/Lib/Devices/IEM3kGridMeter/IEM3kGridMeterDevice.cs +++ b/csharp/Lib/Devices/IEM3kGridMeter/IEM3kGridMeterDevice.cs @@ -1,4 +1,4 @@ -/*using InnovEnergy.Lib.Protocols.Modbus.Channels; +using InnovEnergy.Lib.Protocols.Modbus.Channels; using InnovEnergy.Lib.Protocols.Modbus.Clients; using InnovEnergy.Lib.Protocols.Modbus.Slaves; using InnovEnergy.Lib.Utils; @@ -49,83 +49,4 @@ public class Iem3KGridMeterDevice: ModbusDevice } -}*/ - -using InnovEnergy.Lib.Protocols.Modbus.Channels; -using InnovEnergy.Lib.Protocols.Modbus.Clients; -using InnovEnergy.Lib.Protocols.Modbus.Slaves; -using InnovEnergy.Lib.Utils; -using System; - -namespace InnovEnergy.Lib.Devices.IEM3kGridMeter -{ - public class Iem3KGridMeterDevice : ModbusDevice - { - private readonly string _hostname; - private readonly ushort _port; - private readonly byte _slaveId; - - public Iem3KGridMeterDevice(string hostname, ushort port = 502, byte slaveId = 1) - : this(new TcpChannel(hostname, port), slaveId) - { - _hostname = hostname ?? throw new ArgumentNullException(nameof(hostname)); - _port = port; - _slaveId = slaveId; - } - - private Iem3KGridMeterDevice(TcpChannel channel, byte slaveId = 1) - : base(new ModbusTcpClient(channel, slaveId)) - { - _hostname = channel.Host; - _port = channel.Port; - _slaveId = slaveId; - Console.WriteLine($"Initializing Iem3KGridMeterDevice with channel: {channel.Host}:{channel.Port}"); - } - - public Iem3KGridMeterDevice(ModbusClient client) - : base(client) - { - if (client is ModbusTcpClient tcpClient) - { - _hostname = tcpClient.Channel.Host; - _port = tcpClient.Channel.Port; - _slaveId = tcpClient.SlaveId; - } - else - { - throw new ArgumentException("Invalid client type", nameof(client)); - } - Console.WriteLine("Initializing Iem3KGridMeterDevice with ModbusClient"); - } - - public new Iem3KGridMeterRegisters? Read() - { - try - { - Console.WriteLine($"Attempting to read data from {_hostname}:{_port} with slaveId {_slaveId}"); - return base.Read(); - } - catch (Exception ex) - { - Console.WriteLine($"Failed to read data from {nameof(Iem3KGridMeterDevice)}: {ex.Message}"); - return null; - } - } - - public new void Write(Iem3KGridMeterRegisters registers) - { - try - { - base.Write(registers); - } - catch (Exception ex) - { - Console.WriteLine($"Failed to write data to {nameof(Iem3KGridMeterDevice)}: {ex.Message}"); - } - } - } -} - - - - +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Channels/TcpChannel.cs b/csharp/Lib/Protocols/Modbus/Channels/TcpChannel.cs index f1f39a09a..22a20d018 100644 --- a/csharp/Lib/Protocols/Modbus/Channels/TcpChannel.cs +++ b/csharp/Lib/Protocols/Modbus/Channels/TcpChannel.cs @@ -2,7 +2,7 @@ using System.Net.Sockets; using InnovEnergy.Lib.Protocols.Modbus.Protocol; using InnovEnergy.Lib.Utils.Net; -/*namespace InnovEnergy.Lib.Protocols.Modbus.Channels; +namespace InnovEnergy.Lib.Protocols.Modbus.Channels; public class TcpChannel : ConnectionChannel { @@ -82,104 +82,4 @@ public class TcpChannel : ConnectionChannel var array = data.ToArray(); tcpClient.GetStream().Write(array, 0, array.Length); } -}*/ - -using System; -using System.Net.Sockets; - -namespace InnovEnergy.Lib.Protocols.Modbus.Channels -{ - public class TcpChannel : Channel, IDisposable - { - public string Host { get; } - public ushort Port { get; } - - private const int TimeoutMs = 500; // TODO: parametrize - private Socket? Socket { get; set; } - private byte[] Buffer { get; } - - public TcpChannel(string hostname, ushort port) - { - Host = hostname ?? throw new ArgumentNullException(nameof(hostname)); - Port = port; - Buffer = new byte[8192]; // Buffer size can be adjusted - } - - public override IReadOnlyList Read(int nBytes) - { - if (Socket == null) - throw new InvalidOperationException("Socket is not connected."); - - var buffer = new byte[nBytes]; - int bytesRead = 0; - - while (bytesRead < nBytes) - { - var read = Socket.Receive(buffer, bytesRead, nBytes - bytesRead, SocketFlags.None); - if (read == 0) - throw new Exception("Socket closed."); - - bytesRead += read; - } - - return buffer; - } - - public override void Write(IReadOnlyList bytes) - { - if (Socket == null) - throw new InvalidOperationException("Socket is not connected."); - - Socket.Send(bytes.ToArray(), SocketFlags.None); - } - - public void Connect() - { - if (Socket != null) - return; - - Socket = new Socket(SocketType.Stream, ProtocolType.Tcp) - { - Blocking = true, - NoDelay = true, - LingerState = new LingerOption(false, 0), - ReceiveTimeout = TimeoutMs, - SendTimeout = TimeoutMs - }; - - var cts = new CancellationTokenSource(); - cts.CancelAfter(TimeoutMs); - - try - { - Socket.ConnectAsync(Host, Port).Wait(TimeoutMs); - } - catch - { - Socket = null; - throw; - } - } - - public void Disconnect() - { - if (Socket == null) - return; - - try - { - Socket.Close(); - } - finally - { - Socket = null; - } - } - - public void Dispose() - { - Disconnect(); - } - } -} - +} \ No newline at end of file diff --git a/csharp/Lib/Protocols/Modbus/Clients/ModbusTcpClient.cs b/csharp/Lib/Protocols/Modbus/Clients/ModbusTcpClient.cs index 7966bacb4..41baf7fcb 100644 --- a/csharp/Lib/Protocols/Modbus/Clients/ModbusTcpClient.cs +++ b/csharp/Lib/Protocols/Modbus/Clients/ModbusTcpClient.cs @@ -12,7 +12,7 @@ namespace InnovEnergy.Lib.Protocols.Modbus.Clients; using UInt16s = IReadOnlyCollection; using Booleans = IReadOnlyCollection; -/*public class ModbusTcpClient : ModbusClient +public class ModbusTcpClient : ModbusClient { public const UInt16 DefaultPort = 502; private UInt16 _Id; @@ -184,171 +184,4 @@ using Booleans = IReadOnlyCollection; return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian); } -}*/ - -public class ModbusTcpClient : ModbusClient -{ - public const ushort DefaultPort = 502; - private ushort _Id; - public TcpChannel Channel { get; } - - public ModbusTcpClient(TcpChannel channel, byte slaveId) : base(channel, slaveId) - { - Channel = channel; - Channel.Connect(); - } - - private ushort NextId() => unchecked(++_Id); - - public override MbData ReadCoils(ushort readAddress, ushort nValues) - { - var id = NextId(); // TODO: check response id - - var cmd = new ReadCoilsCommandFrame(SlaveId, readAddress, nValues); - var hdr = new MbapHeader(id, cmd.Data.Count); - var frm = new ModbusTcpFrame(hdr, cmd); - - Channel.Write(frm.Data); - - var hData = Channel.Read(MbapHeader.Size).ToArray(); - var rxHdr = new MbapHeader(hData); - - var rxFrm = Channel - .Read(rxHdr.FrameLength) - .ToArray() - .Apply(ReadCoilsResponseFrame.Parse) - .Apply(cmd.VerifyResponse); - - return new MbData(rxFrm.Coils.RawData, readAddress, Endian); - } - - public override MbData ReadDiscreteInputs(ushort readAddress, ushort nValues) - { - var id = NextId(); // TODO: check response id - - var cmd = new ReadDiscreteInputsCommandFrame(SlaveId, readAddress, nValues); - var hdr = new MbapHeader(id, cmd.Data.Count); - var frm = new ModbusTcpFrame(hdr, cmd); - - Channel.Write(frm.Data); - - var hData = Channel.Read(MbapHeader.Size).ToArray(); - var rxHdr = new MbapHeader(hData); - - var rxFrm = Channel - .Read(rxHdr.FrameLength) - .ToArray() - .Apply(ReadDiscreteInputsResponseFrame.Parse) - .Apply(cmd.VerifyResponse); - - return new MbData(rxFrm.Inputs.RawData, readAddress, Endian); - } - - public override MbData ReadInputRegisters(ushort readAddress, ushort nValues) - { - var id = NextId(); // TODO: check response id - - var cmd = new ReadInputRegistersCommandFrame(SlaveId, readAddress, nValues); - var hdr = new MbapHeader(id, cmd.Data.Count); - var frm = new ModbusTcpFrame(hdr, cmd); - - Channel.Write(frm.Data); - - var hData = Channel.Read(MbapHeader.Size).ToArray(); - var rxHdr = new MbapHeader(hData); - - var rxFrm = Channel - .Read(rxHdr.FrameLength) - .ToArray() - .Apply(ReadInputRegistersResponseFrame.Parse) - .Apply(cmd.VerifyResponse); - - return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian); - } - - public override MbData ReadHoldingRegisters(ushort readAddress, ushort nValues) - { - var id = NextId(); // TODO: check response id - - var cmd = new ReadHoldingRegistersCommandFrame(SlaveId, readAddress, nValues); - var hdr = new MbapHeader(id, cmd.Data.Count); - var frm = new ModbusTcpFrame(hdr, cmd); - - Channel.Write(frm.Data); - - var hData = Channel.Read(MbapHeader.Size).ToArray(); - var rxHdr = new MbapHeader(hData); - - var rxFrm = Channel - .Read(rxHdr.FrameLength) - .ToArray() - .Apply(ReadHoldingRegistersResponseFrame.Parse) - .Apply(cmd.VerifyResponse); - - return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian); - } - - public override ushort WriteCoils(ushort writeAddress, Booleans coils) - { - var id = NextId(); // TODO: check response id - var cmd = new WriteCoilsCommandFrame(SlaveId, writeAddress, coils); - var hdr = new MbapHeader(id, cmd.Data.Count); - var frm = new ModbusTcpFrame(hdr, cmd); - - Channel.Write(frm.Data); - - var hData = Channel.Read(MbapHeader.Size).ToArray(); - var rxHdr = new MbapHeader(hData); - - var rxFrm = Channel - .Read(rxHdr.FrameLength) - .ToArray() - .Apply(WriteCoilsResponseFrame.Parse) - .Apply(cmd.VerifyResponse); - - return rxFrm.NbWritten; - } - - public override ushort WriteRegisters(ushort writeAddress, UInt16s values) - { - var id = NextId(); // TODO: check response id - var cmd = new WriteRegistersCommandFrame(SlaveId, writeAddress, values); - var hdr = new MbapHeader(id, cmd.Data.Count); - var frm = new ModbusTcpFrame(hdr, cmd); - - Channel.Write(frm.Data); - - var hData = Channel.Read(MbapHeader.Size).ToArray(); - var rxHdr = new MbapHeader(hData); - - var rxFrm = Channel - .Read(rxHdr.FrameLength) - .ToArray() - .Apply(WriteRegistersResponseFrame.Parse) - .Apply(cmd.VerifyResponse); - - return rxFrm.NbWritten; - } - - public override MbData ReadWriteRegisters(ushort readAddress, ushort nbToRead, ushort writeAddress, UInt16s registersToWrite) - { - var id = NextId(); // TODO: check response id - - var cmd = new ReadWriteRegistersCommandFrame(SlaveId, readAddress, nbToRead, writeAddress, registersToWrite); - - var hdr = new MbapHeader(id, cmd.Data.Count); - var frm = new ModbusTcpFrame(hdr, cmd); - - Channel.Write(frm.Data); - - var hData = Enumerable.ToArray(Channel.Read(MbapHeader.Size)); - var rxHdr = new MbapHeader(hData); - - var fData = Enumerable.ToArray(Channel.Read(rxHdr.FrameLength)); - var rxFrm = fData - .Apply(ReadWriteRegistersResponseFrame.Parse) - .Apply(cmd.VerifyResponse); - - return new MbData(rxFrm.RegistersRead.RawData, readAddress, Endian); - } -} \ No newline at end of file +} From c44c7e782d25448bb14b0f1b7cfd6f6678d71d0f Mon Sep 17 00:00:00 2001 From: Noe Date: Mon, 10 Jun 2024 15:45:45 +0200 Subject: [PATCH 5/8] pv view --- .../src/content/dashboards/Log/graph.util.tsx | 50 ++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) 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 ec4dad776..9ee906795 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx @@ -35,7 +35,12 @@ export type ConfigurationValues = { calibrationChargeDate: Date | null; }; -export interface Pv {} +export interface Pv { + PvId: number; + Voltage: I_BoxDataValue; + Power: I_BoxDataValue; + Current: I_BoxDataValue; +} export interface Battery { BatteryId: number; @@ -339,6 +344,49 @@ export const extractValues = ( const paths = topologyPaths[topologyKey]; let topologyValues: { unit: string; value: string | number }[] = []; + if (topologyKey === 'pvView') { + extractedValues[topologyKey] = []; + const node_ids_from_csv = timeSeriesData.value[ + '/Config/Devices/BatteryNodes' + ].value + .toString() + .split(','); + + let pv_index = 0; + let pathIndex = 0; + + while (pathIndex < paths.length) { + let pv = {}; + let existingKeys = 0; + + //We prepare a battery object for each node. We extract the nodes from the '/Config/Devices/BatteryNodes' path. For example, nodes can be [2,4,5,6] (one is missing) + //BatteryKeys[0] is the battery id. We set the battery id to the corresponding node id. + battery[BatteryKeys[0]] = node_ids_from_csv[pv_index]; + //Then, search all the remaining battery keys + for (let i = 1; i < BatteryKeys.length; i++) { + const path = paths[pathIndex]; + if (timeSeriesData.value.hasOwnProperty(path)) { + existingKeys++; + + battery[BatteryKeys[i]] = { + unit: timeSeriesData.value[path].unit.includes('~') + ? timeSeriesData.value[path].unit.replace('~', '') + : timeSeriesData.value[path].unit, + value: + typeof timeSeriesData.value[path].value === 'string' + ? timeSeriesData.value[path].value + : Number(timeSeriesData.value[path].value).toFixed(1) + }; + } + pathIndex++; + } + battery_index++; + if (existingKeys > 0) { + extractedValues[topologyKey].push(battery as Battery); + } + } + } + if (topologyKey === 'batteryView') { extractedValues[topologyKey] = []; const node_ids_from_csv = timeSeriesData.value[ From 8d04d89eabc776819b85d0b0958d7bae6f59fe59 Mon Sep 17 00:00:00 2001 From: atef Date: Tue, 11 Jun 2024 11:39:45 +0200 Subject: [PATCH 6/8] Changed to Output string instead of input --- csharp/Lib/Devices/AMPT/AmptDevices.cs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/csharp/Lib/Devices/AMPT/AmptDevices.cs b/csharp/Lib/Devices/AMPT/AmptDevices.cs index 1674baffe..003e09b20 100644 --- a/csharp/Lib/Devices/AMPT/AmptDevices.cs +++ b/csharp/Lib/Devices/AMPT/AmptDevices.cs @@ -77,8 +77,8 @@ public class AmptDevices Current = busCurrent }; - // flatten the 2 strings of each SO into one array - var strings = soStati.SelectMany(GetStrings).ToArray(nStrings); + // flatten the output strings of each SO into one array + var strings = soStati.SelectMany(GetDc).ToArray(nStrings); return new AmptStatus { @@ -107,6 +107,16 @@ public class AmptDevices }; } + private static IEnumerable GetDc(StringOptimizerRegisters r) + { + // hardcoded: every SO has 2 strings (produced like this by AMPT) + + yield return new() + { + Voltage = r.Voltage, + Current = r.Current, + }; + } private static IEnumerable> StringOptimizers(ModbusClient modbusClient) { From 9e15be4aae106acd93705b339a86f8a7a6ebbb58 Mon Sep 17 00:00:00 2001 From: Yinyin Liu Date: Tue, 11 Jun 2024 14:31:08 +0200 Subject: [PATCH 7/8] Add Record User Action Backend --- csharp/App/Backend/Backend.sln | 25 +++++++++++++ csharp/App/Backend/Controller.cs | 35 ++++++++++++++++--- csharp/App/Backend/DataTypes/Configuration.cs | 4 +++ .../App/Backend/DataTypes/Methods/Session.cs | 22 ++++++++++++ csharp/App/Backend/DataTypes/UserAction.cs | 18 ++++++++++ csharp/App/Backend/Database/Create.cs | 31 ++++++++++++++++ csharp/App/Backend/Database/Db.cs | 5 +++ csharp/App/Backend/Database/Delete.cs | 15 ++++++++ 8 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 csharp/App/Backend/Backend.sln create mode 100644 csharp/App/Backend/DataTypes/UserAction.cs diff --git a/csharp/App/Backend/Backend.sln b/csharp/App/Backend/Backend.sln new file mode 100644 index 000000000..c72530762 --- /dev/null +++ b/csharp/App/Backend/Backend.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Backend", "Backend.csproj", "{161624D7-33B9-48B8-BA05-303DCFAEB03E}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {161624D7-33B9-48B8-BA05-303DCFAEB03E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {161624D7-33B9-48B8-BA05-303DCFAEB03E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {161624D7-33B9-48B8-BA05-303DCFAEB03E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {161624D7-33B9-48B8-BA05-303DCFAEB03E}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {A377D3C5-E56D-4A16-AC4B-F3B0BB4CFCCE} + EndGlobalSection +EndGlobal diff --git a/csharp/App/Backend/Controller.cs b/csharp/App/Backend/Controller.cs index 8afccf35e..b4dabe8cc 100644 --- a/csharp/App/Backend/Controller.cs +++ b/csharp/App/Backend/Controller.cs @@ -100,6 +100,24 @@ public class Controller : ControllerBase .ToList(); } + [HttpGet(nameof(GetHistoryForInstallation))] + public ActionResult> GetHistoryForInstallation(Int64 id, Token authToken) + { + var user = Db.GetSession(authToken)?.User; + if (user == null) + return Unauthorized(); + + var installation = Db.GetInstallationById(id); + + if (installation is null || !user.HasAccessTo(installation)) + return Unauthorized(); + + return Db.UserActions + .Where(action =>action.InstallationId == id) + .OrderByDescending(action => action.Timestamp) + .ToList(); + } + [HttpGet(nameof(GetAllWarningsForInstallation))] public ActionResult> GetAllWarningsForInstallation(Int64 id, Token authToken) { @@ -552,12 +570,19 @@ public class Controller : ControllerBase { var session = Db.GetSession(authToken); //Console.WriteLine(config.GridSetPoint); - - //var installationToUpdate = Db.GetInstallationById(installationId); - return await session.SendInstallationConfig(installationId, config) - ? Ok() - : Unauthorized(); + // Send configuration changes + var success = await session.SendInstallationConfig(installationId, config); + + // Record configuration change + if (success) + { + var actionSuccess = await session.RecordUserAction(installationId, config); + return actionSuccess?Ok():Unauthorized(); + } + + return Unauthorized(); + } [HttpPut(nameof(MoveFolder))] diff --git a/csharp/App/Backend/DataTypes/Configuration.cs b/csharp/App/Backend/DataTypes/Configuration.cs index 7bd6a830c..76ac6edbe 100644 --- a/csharp/App/Backend/DataTypes/Configuration.cs +++ b/csharp/App/Backend/DataTypes/Configuration.cs @@ -6,6 +6,10 @@ public class Configuration public Double GridSetPoint { get; set; } public CalibrationChargeType CalibrationChargeState { get; set; } public DateTime CalibrationChargeDate { get; set; } + public String GetConfigurationString() + { + return $"MinimumSoC: {MinimumSoC}, GridSetPoint: {GridSetPoint}, CalibrationChargeState: {CalibrationChargeState}, CalibrationChargeDate: {CalibrationChargeDate}"; + } } public enum CalibrationChargeType diff --git a/csharp/App/Backend/DataTypes/Methods/Session.cs b/csharp/App/Backend/DataTypes/Methods/Session.cs index f8ff5d571..bc829bdc8 100644 --- a/csharp/App/Backend/DataTypes/Methods/Session.cs +++ b/csharp/App/Backend/DataTypes/Methods/Session.cs @@ -101,6 +101,28 @@ public static class SessionMethods && user.HasAccessTo(installation) && await installation.SendConfig(configuration); } + + public static async Task RecordUserAction(this Session? session, Int64 installationId, Configuration newConfiguration) + { + var user = session?.User; + var timestamp = DateTime.Now; + + if (user is null || user.UserType == 0) + return false; + + // Create a new UserAction object + var action = new UserAction + { + UserName = user.Name, + InstallationId = installationId, + Timestamp = timestamp, + Description = newConfiguration.GetConfigurationString() + }; + + // Save the configuration change to the database + Db.HandleAction(action); + return true; + } public static Boolean Delete(this Session? session, Folder? folder) { diff --git a/csharp/App/Backend/DataTypes/UserAction.cs b/csharp/App/Backend/DataTypes/UserAction.cs new file mode 100644 index 000000000..9a20cff26 --- /dev/null +++ b/csharp/App/Backend/DataTypes/UserAction.cs @@ -0,0 +1,18 @@ +using SQLite; + +namespace InnovEnergy.App.Backend.DataTypes; + +public class UserAction +{ + [PrimaryKey, AutoIncrement] + public Int64 Id { get; set; } // Primary key for the table, auto-incremented + + [Indexed] + public String UserName { get; set; } = null!;// User Name who made the configuration change + + public Int64 InstallationId { get; set; } // Installation ID where the configuration change is made + + public DateTime Timestamp { get; set; } // Timestamp of the configuration change + + public String Description { get; set; } = null!;// Serialized string representing the new configuration +} \ No newline at end of file diff --git a/csharp/App/Backend/Database/Create.cs b/csharp/App/Backend/Database/Create.cs index d1b50e107..b25cc089f 100644 --- a/csharp/App/Backend/Database/Create.cs +++ b/csharp/App/Backend/Database/Create.cs @@ -62,6 +62,37 @@ public static partial class Db return Insert(o2i); } + public static Boolean Create(UserAction action) + { + return Insert(action); + } + + public static void HandleAction(UserAction newAction) + { + //Find the total number of actions for this installation + var totalActions = UserActions.Count(action => action.InstallationId == newAction.InstallationId); + + //If there are 100 actions, remove the one with the oldest timestamp + if (totalActions == 100) + { + var oldestAction = + UserActions.Where(action => action.InstallationId == newAction.InstallationId) + .OrderBy(action => action.Timestamp) + .FirstOrDefault(); + + //Remove the old action + Delete(oldestAction); + + //Add the new action + Create(newAction); + } + else + { + Console.WriteLine("---------------Added the new Error to the database-----------------"); + Create(newAction); + } + } + public static void HandleError(Error newError,int installationId) { //Find the total number of errors for this installation diff --git a/csharp/App/Backend/Database/Db.cs b/csharp/App/Backend/Database/Db.cs index 130c10649..9ec5ecc6d 100644 --- a/csharp/App/Backend/Database/Db.cs +++ b/csharp/App/Backend/Database/Db.cs @@ -36,6 +36,7 @@ public static partial class Db fileConnection.CreateTable(); fileConnection.CreateTable(); fileConnection.CreateTable(); + fileConnection.CreateTable(); return fileConnection; //return CopyDbToMemory(fileConnection); @@ -55,6 +56,7 @@ public static partial class Db memoryConnection.CreateTable(); memoryConnection.CreateTable(); memoryConnection.CreateTable(); + fileConnection.CreateTable(); //Copy all the existing tables from the disk to main memory fileConnection.Table().ForEach(memoryConnection.Insert); @@ -66,6 +68,7 @@ public static partial class Db fileConnection.Table().ForEach(memoryConnection.Insert); fileConnection.Table().ForEach(memoryConnection.Insert); fileConnection.Table().ForEach(memoryConnection.Insert); + fileConnection.Table().ForEach(memoryConnection.Insert); return memoryConnection; } @@ -85,6 +88,7 @@ public static partial class Db public static TableQuery OrderNumber2Installation => Connection.Table(); public static TableQuery Errors => Connection.Table(); public static TableQuery Warnings => Connection.Table(); + public static TableQuery UserActions => Connection.Table(); public static void Init() { @@ -106,6 +110,7 @@ public static partial class Db Connection.CreateTable(); Connection.CreateTable(); Connection.CreateTable(); + Connection.CreateTable(); }); //UpdateKeys(); diff --git a/csharp/App/Backend/Database/Delete.cs b/csharp/App/Backend/Database/Delete.cs index f50fa3761..d9ee41aab 100644 --- a/csharp/App/Backend/Database/Delete.cs +++ b/csharp/App/Backend/Database/Delete.cs @@ -1,6 +1,7 @@ using InnovEnergy.App.Backend.DataTypes; using InnovEnergy.App.Backend.DataTypes.Methods; using InnovEnergy.App.Backend.Relations; +using Microsoft.AspNetCore.Authentication.OAuth.Claims; namespace InnovEnergy.App.Backend.Database; @@ -49,6 +50,20 @@ public static partial class Db } } + public static Boolean Delete(UserAction actionToDelete) + { + var deleteSuccess = RunTransaction(DeleteAction); + if (deleteSuccess) + BackupDatabase(); + return deleteSuccess; + + + Boolean DeleteAction() + { + return UserActions.Delete(action => action.Id == actionToDelete.Id) >0; + } + } + public static Boolean Delete(Warning warningToDelete) { var deleteSuccess = RunTransaction(DeleteWarning); From e6c32b716234f6e7f94062f882fb72589cd1c2e0 Mon Sep 17 00:00:00 2001 From: Noe Date: Tue, 11 Jun 2024 21:35:35 +0200 Subject: [PATCH 8/8] Added history tab --- .../Configuration/Configuration.tsx | 26 +-- .../content/dashboards/History/History.tsx | 201 ++++++++++-------- .../dashboards/Installations/Installation.tsx | 14 +- .../dashboards/Installations/index.tsx | 36 ++-- .../src/content/dashboards/Log/graph.util.tsx | 42 ++-- .../src/content/dashboards/PvView/PvView.tsx | 187 ++++++++++++++++ .../src/interfaces/S3Types.tsx | 11 +- 7 files changed, 354 insertions(+), 163 deletions(-) diff --git a/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx index 449d12308..dbb832c1d 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Configuration/Configuration.tsx @@ -29,7 +29,6 @@ import { AdapterDayjs } from '@mui/x-date-pickers/AdapterDayjs'; import dayjs from 'dayjs'; import axiosConfig from '../../../Resources/axiosConfig'; import utc from 'dayjs/plugin/utc'; -import { Action } from '../../../interfaces/S3Types'; import { UserContext } from '../../../contexts/userContext'; interface ConfigurationProps { @@ -136,14 +135,6 @@ function Configuration(props: ConfigurationProps) { .add(localOffset, 'minute') .toDate() }; - - const historyAction: Action = { - configuration: configurationToSend, - date: new Date().toISOString().split('T')[0], // Gets the current date in YYYY-MM-DD format - time: new Date().toISOString().split('T')[1].split('.')[0], // Gets the current time in HH:MM:SS format - user: currentUser.name - }; - // console.log('will send ', dayjs(formValues.calibrationChargeDate)); setLoading(true); @@ -160,21 +151,8 @@ function Configuration(props: ConfigurationProps) { }); if (res) { - const historyRes = await axiosConfig - .post( - `/UpdateActionHistory?installationId=${props.id}`, - historyAction - ) - .catch((err) => { - if (err.response) { - setError(true); - setLoading(false); - } - }); - if (historyRes) { - setUpdated(true); - setLoading(false); - } + setUpdated(true); + setLoading(false); } } }; diff --git a/typescript/frontend-marios2/src/content/dashboards/History/History.tsx b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx index 49ffd8a68..86923395e 100644 --- a/typescript/frontend-marios2/src/content/dashboards/History/History.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/History/History.tsx @@ -122,102 +122,135 @@ function HistoryOfActions(props: HistoryProps) { - {/**/} - {/* */} - {/* */} - {/* */} - {/**/} +
+ + + +
- {history.map((action, index) => ( - <> - -
-
- - {action.user} - -
+ {history.map((action, index) => { + // Parse the timestamp string to a Date object + const date = new Date(action.timestamp); + // Extract the date part (e.g., "2023-05-31") + const datePart = date.toLocaleDateString(); + + // Extract the time part (e.g., "12:34:56") + const timePart = date.toLocaleTimeString(); + return ( + <> +
- - {action.date} - -
-
- + {action.userName} + +
+ +
- {action.time} - + + {datePart} + +
+
+ + {timePart} + +
+ +
+ + {action.description} + +
-
- - ))} + + ); + })} diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx index d6f4cf84b..23f25107a 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/Installation.tsx @@ -24,6 +24,7 @@ import Information from '../Information/Information'; import BatteryView from '../BatteryView/BatteryView'; import { UserType } from '../../../interfaces/UserTypes'; import HistoryOfActions from '../History/History'; +import PvView from '../PvView/PvView'; interface singleInstallationProps { current_installation?: I_Installation; @@ -124,6 +125,7 @@ function Installation(props: singleInstallationProps) { useEffect(() => { if ( currentTab == 'live' || + currentTab == 'pvview' || currentTab == 'configuration' || location.includes('batteryview') ) { @@ -131,7 +133,8 @@ function Installation(props: singleInstallationProps) { if ( currentTab == 'live' || - (location.includes('batteryview') && !location.includes('mainstats')) + (location.includes('batteryview') && !location.includes('mainstats')) || + currentTab == 'pvview' ) { fetchDataPeriodically(); interval = setInterval(fetchDataPeriodically, 2000); @@ -144,6 +147,7 @@ function Installation(props: singleInstallationProps) { return () => { if ( currentTab == 'live' || + currentTab == 'pvview' || (location.includes('batteryview') && !location.includes('mainstats')) ) { clearInterval(interval); @@ -314,13 +318,7 @@ function Installation(props: singleInstallationProps) { + } > diff --git a/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx b/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx index 34d6e9e21..bd10ab9d4 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Installations/index.tsx @@ -140,15 +140,15 @@ function InstallationTabs() { /> ) }, - // { - // value: 'history', - // label: ( - // - // ) - // }, + { + value: 'history', + label: ( + + ) + }, { value: 'pvview', label: @@ -271,16 +271,16 @@ function InstallationTabs() { defaultMessage="Configuration" /> ) + }, + { + value: 'history', + label: ( + + ) } - // { - // value: 'history', - // label: ( - // - // ) - // } ] : currentUser.userType == UserType.partner ? [ 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 9ee906795..9ecb56639 100644 --- a/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/Log/graph.util.tsx @@ -37,8 +37,8 @@ export type ConfigurationValues = { export interface Pv { PvId: number; - Voltage: I_BoxDataValue; Power: I_BoxDataValue; + Voltage: I_BoxDataValue; Current: I_BoxDataValue; } @@ -86,6 +86,8 @@ export interface Battery { MaxDischargePower: I_BoxDataValue; } +const PvKeys = ['PvId', 'Power', 'Voltage', 'Current']; + const BatteryKeys = [ 'BatteryId', 'FwVersion', @@ -177,10 +179,15 @@ type TopologyPaths = { [key in keyof TopologyValues]: string[] }; const batteryIds = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; +const pvIds = [ + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, + 23, 24, 25, 26, 27, 28, 29, 30 +]; + const PvPaths = [ + '/PvOnDc/Strings/%id%/Power', '/PvOnDc/Strings/%id%/Voltage', - '/PvOnDc/Strings/%id%/Current', - '/PvOnDc/Strings/%id%/Power' + '/PvOnDc/Strings/%id%/Current' ]; const batteryPaths = [ @@ -305,7 +312,7 @@ export const topologyPaths: TopologyPaths = { batteryPaths.map((path) => path.replace('%id%', id.toString())) ), - pvView: batteryIds.flatMap((id) => + pvView: pvIds.flatMap((id) => PvPaths.map((path) => path.replace('%id%', id.toString())) ), @@ -336,9 +343,6 @@ export const extractValues = ( timeSeriesData: DataPoint ): TopologyValues | null => { const extractedValues: TopologyValues = {} as TopologyValues; - - // console.log('timeSeriesData=', timeSeriesData); - for (const topologyKey of Object.keys(topologyPaths)) { //Each topologykey may have more than one paths (for example inverter) const paths = topologyPaths[topologyKey]; @@ -346,12 +350,6 @@ export const extractValues = ( if (topologyKey === 'pvView') { extractedValues[topologyKey] = []; - const node_ids_from_csv = timeSeriesData.value[ - '/Config/Devices/BatteryNodes' - ].value - .toString() - .split(','); - let pv_index = 0; let pathIndex = 0; @@ -359,16 +357,16 @@ export const extractValues = ( let pv = {}; let existingKeys = 0; - //We prepare a battery object for each node. We extract the nodes from the '/Config/Devices/BatteryNodes' path. For example, nodes can be [2,4,5,6] (one is missing) - //BatteryKeys[0] is the battery id. We set the battery id to the corresponding node id. - battery[BatteryKeys[0]] = node_ids_from_csv[pv_index]; + //We prepare a pv object for each node. We extract the number of nodes from the '/PvOnDc/NbrOfStrings' path. + //PvKeys[0] is the pv id. + pv[PvKeys[0]] = pv_index; //Then, search all the remaining battery keys - for (let i = 1; i < BatteryKeys.length; i++) { + for (let i = 1; i < PvKeys.length; i++) { const path = paths[pathIndex]; if (timeSeriesData.value.hasOwnProperty(path)) { existingKeys++; - battery[BatteryKeys[i]] = { + pv[PvKeys[i]] = { unit: timeSeriesData.value[path].unit.includes('~') ? timeSeriesData.value[path].unit.replace('~', '') : timeSeriesData.value[path].unit, @@ -380,14 +378,12 @@ export const extractValues = ( } pathIndex++; } - battery_index++; + pv_index++; if (existingKeys > 0) { - extractedValues[topologyKey].push(battery as Battery); + extractedValues[topologyKey].push(pv as Pv); } } - } - - if (topologyKey === 'batteryView') { + } else if (topologyKey === 'batteryView') { extractedValues[topologyKey] = []; const node_ids_from_csv = timeSeriesData.value[ '/Config/Devices/BatteryNodes' diff --git a/typescript/frontend-marios2/src/content/dashboards/PvView/PvView.tsx b/typescript/frontend-marios2/src/content/dashboards/PvView/PvView.tsx index e69de29bb..548fb36fc 100644 --- a/typescript/frontend-marios2/src/content/dashboards/PvView/PvView.tsx +++ b/typescript/frontend-marios2/src/content/dashboards/PvView/PvView.tsx @@ -0,0 +1,187 @@ +import React, { useEffect, useState } from 'react'; +import { + Container, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography +} from '@mui/material'; +import { TopologyValues } from '../Log/graph.util'; +import { useLocation, useNavigate } from 'react-router-dom'; +import routes from '../../../Resources/routes.json'; +import CircularProgress from '@mui/material/CircularProgress'; + +interface PvViewProps { + values: TopologyValues; + connected: boolean; +} + +function PvView(props: PvViewProps) { + if (props.values === null && props.connected == true) { + return null; + } + const currentLocation = useLocation(); + const navigate = useNavigate(); + const sortedPvView = + props.values != null + ? [...props.values.pvView].sort((a, b) => a.PvId - b.PvId) + : []; + + const [loading, setLoading] = useState(sortedPvView.length == 0); + + const handleMainStatsButton = () => { + navigate(routes.mainstats); + }; + + const findBatteryData = (batteryId: number) => { + for (let i = 0; i < props.values.batteryView.length; i++) { + if (props.values.batteryView[i].BatteryId == batteryId) { + return props.values.batteryView[i]; + } + } + }; + + useEffect(() => { + if (sortedPvView.length == 0) { + setLoading(true); + } else { + setLoading(false); + } + }, [sortedPvView]); + + return ( + <> + {!props.connected && ( + + + + Unable to communicate with the installation + + + Please wait or refresh the page + + + )} + {loading && props.connected && ( + + + + Battery service is not available at the moment + + + Please wait or refresh the page + + + )} + + {!loading && props.connected && ( + + + + + + Pv + Power + Voltage + Current + + + + {sortedPvView.map((pv) => ( + + + {'String ' + pv.PvId} + + + + {pv.Power.value + ' ' + pv.Power.unit} + + + {pv.Voltage.value + ' ' + pv.Voltage.unit} + + + {pv.Current.value + ' ' + pv.Current.unit} + + + ))} + +
+
+
+ )} + + ); +} + +export default PvView; diff --git a/typescript/frontend-marios2/src/interfaces/S3Types.tsx b/typescript/frontend-marios2/src/interfaces/S3Types.tsx index 078a011bc..6d7c56db0 100644 --- a/typescript/frontend-marios2/src/interfaces/S3Types.tsx +++ b/typescript/frontend-marios2/src/interfaces/S3Types.tsx @@ -1,5 +1,3 @@ -import { ConfigurationValues } from '../content/dashboards/Log/graph.util'; - export interface I_S3Credentials { s3Region: string; s3Provider: string; @@ -20,8 +18,9 @@ export interface ErrorMessage { } export interface Action { - configuration: ConfigurationValues; - date: string; - time: string; - user: string; + id: number; + userName: string; + installationId: number; + timestamp: string; + description: String; }