From 9e56dffb273c258fb38ebc6cc1ef2ed1d7da45fa Mon Sep 17 00:00:00 2001 From: Sina Blattmann Date: Mon, 19 Jun 2023 17:23:09 +0200 Subject: [PATCH] [WIP] add color and style to liveview, add new connections, refactor components, nearly finished --- .../UsersWithInheritedAccess.tsx | 7 +- .../Installations/Log/DateRangePicker.tsx | 13 +- .../src/components/Installations/Log/Log.tsx | 1 - .../Installations/Log/ScalarGraph.tsx | 79 ++++++----- .../Installations/Log/ShortcutButton.tsx | 1 + .../Installations/Log/TopologyBox.tsx | 62 +++++--- .../Installations/Log/TopologyFlow.scss | 4 +- .../Installations/Log/TopologyFlow.tsx | 40 +++--- .../Installations/Log/TopologyView.tsx | 132 ++++++++++++++---- typescript/Frontend/src/util/graph.util.tsx | 112 +++++++++------ 10 files changed, 286 insertions(+), 165 deletions(-) diff --git a/typescript/Frontend/src/components/Groups/AccessManagement/UsersWithInheritedAccess.tsx b/typescript/Frontend/src/components/Groups/AccessManagement/UsersWithInheritedAccess.tsx index d6ea6e4d8..d4ea094f9 100644 --- a/typescript/Frontend/src/components/Groups/AccessManagement/UsersWithInheritedAccess.tsx +++ b/typescript/Frontend/src/components/Groups/AccessManagement/UsersWithInheritedAccess.tsx @@ -53,10 +53,13 @@ const UsersWithInheritedAccess = () => { - {folderName} + {` ${folderName}`} } diff --git a/typescript/Frontend/src/components/Installations/Log/DateRangePicker.tsx b/typescript/Frontend/src/components/Installations/Log/DateRangePicker.tsx index 24eef4ed4..329457d3c 100644 --- a/typescript/Frontend/src/components/Installations/Log/DateRangePicker.tsx +++ b/typescript/Frontend/src/components/Installations/Log/DateRangePicker.tsx @@ -4,6 +4,8 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import dayjs from "dayjs"; import { FormattedMessage } from "react-intl"; import ShortcutButton from "./ShortcutButton"; +import { createTimes } from "../../../util/graph.util"; +import { TimeRange, UnixTime } from "../../../dataCache/time"; interface DateRangePickerProps { setRange: (value: Date[]) => void; @@ -14,8 +16,15 @@ const DateRangePicker = (props: DateRangePickerProps) => { const { setRange, range, getCacheSeries } = props; const handleChange = (fromDate: Date, toDate: Date) => { - setRange([fromDate, toDate]); - getCacheSeries(fromDate.getMilliseconds(), toDate.getMilliseconds()); + const timeRange = createTimes( + TimeRange.fromTimes( + UnixTime.fromDate(fromDate), + UnixTime.fromDate(toDate) + ), + 100 + ); + setRange([timeRange[0].toDate(), timeRange[timeRange.length - 1].toDate()]); + getCacheSeries(timeRange[0].ticks, timeRange[timeRange.length - 1].ticks); }; return ( diff --git a/typescript/Frontend/src/components/Installations/Log/Log.tsx b/typescript/Frontend/src/components/Installations/Log/Log.tsx index e5850300e..93517caab 100644 --- a/typescript/Frontend/src/components/Installations/Log/Log.tsx +++ b/typescript/Frontend/src/components/Installations/Log/Log.tsx @@ -6,7 +6,6 @@ const Log = () => { return ( <> - ); diff --git a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx index a35c95f67..d43f46f72 100644 --- a/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx +++ b/typescript/Frontend/src/components/Installations/Log/ScalarGraph.tsx @@ -1,7 +1,6 @@ import Plot from "react-plotly.js"; -import { RecordSeries } from "../../../dataCache/data"; +import { DataRecord, RecordSeries } from "../../../dataCache/data"; import { - Csv, GraphData, createTimes, flattenToggles, @@ -481,12 +480,43 @@ export const testData = `/AcDc/SystemControl/Alarms;; /Config/HoldSocZone;1; /Config/ControllerPConstant;0.5; /SystemState/Message;Panic: Unknown State!; -/SystemState/Id;100;`; +/SystemState/Id;100; +/LoadOnDc/Power;100;`; + +const s3Access = new S3Access( + "saliomameiringen", + "sos-ch-dk-2", + "exo.io", + "EXO464a9ff62fdfa407aa742855", + "f2KtCWN4EHFqtvH2kotdyI0w5SjjdHVPAADdcD3ik8g", + "" +); +export const fetchData = ( + timestamp: UnixTime +): Promise> => { + const s3Path = `${timestamp.ticks}.csv`; + return s3Access + .get(s3Path) + .then(async (r) => { + if (r.status === 404) { + return Promise.resolve(FetchResult.notAvailable); + } else if (r.status === 200) { + const text = await r.text(); + return parseCsv(text); + } else { + console.error("unexpected status code"); + return Promise.resolve(FetchResult.notAvailable); + } + }) + .catch((e) => { + return Promise.resolve(FetchResult.tryLater); + }); +}; const ScalarGraph = () => { const timeRange = createTimes( UnixTime.now() /* .fromTicks(1682085650) */ - .rangeBefore(TimeSpan.fromDays(7)), + .rangeBefore(TimeSpan.fromHours(5)), NUMBER_OF_NODES ); const [timeSeries, setTimeSeries] = useState([]); @@ -502,15 +532,6 @@ const ScalarGraph = () => { const times$ = useMemo(() => new BehaviorSubject(timeRange), []); - const s3Access = new S3Access( - "saliomameiringen", - "sos-ch-dk-2", - "exo.io", - "EXO464a9ff62fdfa407aa742855", - "f2KtCWN4EHFqtvH2kotdyI0w5SjjdHVPAADdcD3ik8g", - "" - ); - useEffect(() => { const subscription = cache.gotData .pipe( @@ -532,26 +553,6 @@ const ScalarGraph = () => { return () => subscription.unsubscribe(); }, [toggles]); - const fetchData = (timestamp: UnixTime): Promise> => { - const s3Path = `${timestamp.ticks}.csv`; - return s3Access - .get(s3Path) - .then(async (r) => { - if (r.status === 404) { - return Promise.resolve(FetchResult.notAvailable); - } else if (r.status === 200) { - const text = await r.text(); - return parseCsv(text); - } else { - console.error("unexpected status code"); - return Promise.resolve(FetchResult.notAvailable); - } - }) - .catch((e) => { - return Promise.resolve(FetchResult.tryLater); - }); - }; - const cache = useMemo( () => new DataCache(fetchData, TimeSpan.fromSeconds(2)), [] @@ -570,9 +571,7 @@ const ScalarGraph = () => { marker: { color: stringToColor(key) }, }; } - transformedObject[key].x.push( - new Date(item.time.ticks * 1000).toISOString() - ); + transformedObject[key].x.push(new Date(item.time.ticks * 1000)); transformedObject[key].y.push(item.value?.[key].value); }); } @@ -600,12 +599,11 @@ const ScalarGraph = () => { const getCacheSeries = (xaxisRange0: number, xaxisRange1: number) => { const times = createTimes( TimeRange.fromTimes( - UnixTime.fromDate(new Date(xaxisRange0)), - UnixTime.fromDate(new Date(xaxisRange1)) + UnixTime.fromTicks(xaxisRange0), + UnixTime.fromTicks(xaxisRange1) ), NUMBER_OF_NODES ); - console.log("getcacheseries"); cache.getSeries(times); times$.next(times); }; @@ -615,7 +613,6 @@ const ScalarGraph = () => { const visibleGraphs = Object.keys(data).filter((path) => { return checkedToggles ? checkedToggles[path] : false; }); - if (data[visibleGraphs[index]]) { const isScalar = isNumeric(data[visibleGraphs[index]].y[0]); const graphData = isScalar @@ -685,7 +682,9 @@ const ScalarGraph = () => { checkedToggles && Object.keys(checkedToggles).find((toggle) => checkedToggles[toggle]) ) { + console.log("timeseries", timeSeries); const coordinateTimeSeries = transformToGraphData(timeSeries); + console.log("timeSeries", timeSeries); return ( <> diff --git a/typescript/Frontend/src/components/Installations/Log/ShortcutButton.tsx b/typescript/Frontend/src/components/Installations/Log/ShortcutButton.tsx index ee3e23a42..f9e89540f 100644 --- a/typescript/Frontend/src/components/Installations/Log/ShortcutButton.tsx +++ b/typescript/Frontend/src/components/Installations/Log/ShortcutButton.tsx @@ -17,6 +17,7 @@ const ShortcutButton = (props: ShortcutButtonProps) => { UnixTime.now().rangeBefore(TimeSpan.fromDays(props.dayRange)), 100 ); + console.log("weekrange", weekRange[0].ticks); props.setRange([ weekRange[0].toDate(), weekRange[weekRange.length - 1].toDate(), diff --git a/typescript/Frontend/src/components/Installations/Log/TopologyBox.tsx b/typescript/Frontend/src/components/Installations/Log/TopologyBox.tsx index e5a0b9728..541a5a0e4 100644 --- a/typescript/Frontend/src/components/Installations/Log/TopologyBox.tsx +++ b/typescript/Frontend/src/components/Installations/Log/TopologyBox.tsx @@ -8,45 +8,61 @@ export type BoxData = { export type TopologyBoxProps = { title?: string; - data?: BoxData[]; + data?: BoxData; }; const isInt = (value: number) => { return value % 1 === 0; }; -export const BOX_SIZE = 150; +export const BOX_SIZE = 85; const TopologyBox = (props: TopologyBoxProps) => { return ( - -

{props.title}

- {props.data && - props.data.map((el) => ( -
- {el.label} - {el.values.map((value) => { - console.log("value", value); - return ( -

{`${ - !isInt(Number(value)) ? Number(value).toPrecision(8) : value - }${el.unit}`}

- ); - })} -
- ))} -
+

+ {props.title} +

+
+ {props.data && ( + <> + {props.data.values.map((value) => { + return ( +

{`${ + !isInt(Number(value)) ? Number(value).toPrecision(4) : value + }${props.data?.unit}`}

+ ); + })} + + )} +
); }; diff --git a/typescript/Frontend/src/components/Installations/Log/TopologyFlow.scss b/typescript/Frontend/src/components/Installations/Log/TopologyFlow.scss index f4eb526d9..150964dc3 100644 --- a/typescript/Frontend/src/components/Installations/Log/TopologyFlow.scss +++ b/typescript/Frontend/src/components/Installations/Log/TopologyFlow.scss @@ -9,8 +9,8 @@ .container { position: relative; - width: 130px; - height: 130px; + width: 150px; + height: 150px; overflow: hidden; } diff --git a/typescript/Frontend/src/components/Installations/Log/TopologyFlow.tsx b/typescript/Frontend/src/components/Installations/Log/TopologyFlow.tsx index 5db85a8a9..7de1442c6 100644 --- a/typescript/Frontend/src/components/Installations/Log/TopologyFlow.tsx +++ b/typescript/Frontend/src/components/Installations/Log/TopologyFlow.tsx @@ -5,12 +5,12 @@ import "./TopologyFlow.scss"; export type TopologyFlowProps = { orientation?: "vertical" | "horizontal"; amount?: number; - rightToLeft?: boolean; - data?: BoxData[]; + data?: BoxData; hidden?: boolean; }; const TopologyFlow = (props: TopologyFlowProps) => { const length = Math.abs((props.amount ?? 1) * BOX_SIZE); + const values = props.data?.values; return (
{ backgroundColor: "#f4b3504d", visibility: props.hidden || !props.data ? "hidden" : "visible", display: "flex", + alignItems: "center", + position: "relative", + justifyContent: "center", }} > +

+ {values?.map( + (value) => `${Math.round(value as number)} ${props.data?.unit}` + )} +

-

- {props.data?.map((value) => value.values)} -

-
+
diff --git a/typescript/Frontend/src/components/Installations/Log/TopologyView.tsx b/typescript/Frontend/src/components/Installations/Log/TopologyView.tsx index c406bc76b..6d9c4103b 100644 --- a/typescript/Frontend/src/components/Installations/Log/TopologyView.tsx +++ b/typescript/Frontend/src/components/Installations/Log/TopologyView.tsx @@ -1,60 +1,119 @@ import { Box } from "@mui/material"; import TopologyColumn from "./TopologyColumn"; -import { UnixTime } from "../../../dataCache/time"; -import { extractTopologyValues, parseCsv } from "../../../util/graph.util"; -import { testData } from "./ScalarGraph"; +import { TimeSpan, UnixTime } from "../../../dataCache/time"; +import { + TopologyValues, + extractTopologyValues, + getAmount, + getHighestConnectionValue, +} from "../../../util/graph.util"; +import { fetchData } from "./ScalarGraph"; +import { useEffect, useState } from "react"; +import { FetchResult } from "../../../dataCache/dataCache"; const TopologyView = () => { - const values = extractTopologyValues({ - time: UnixTime.fromTicks(192384239), - value: parseCsv(testData), - }); - console.log("csvValue", parseCsv(testData)); + const [values, setValues] = useState(null); + + useEffect(() => { + const interval = setInterval(() => { + const now = UnixTime.now().earlier(TimeSpan.fromSeconds(15)); + fetchData(now.round(2)) + .then((res) => { + if ( + res !== FetchResult.notAvailable && + res !== FetchResult.tryLater + ) { + setValues( + extractTopologyValues({ + time: now, + value: res, + }) + ); + } + }) + .catch((err) => console.log(err)); + }, 2000); + + return () => clearInterval(interval); + }, []); + if (values) { + const highestConnectionValue = getHighestConnectionValue(values); return (
@@ -62,32 +121,40 @@ const TopologyView = () => {
@@ -95,11 +162,14 @@ const TopologyView = () => {
@@ -107,7 +177,7 @@ const TopologyView = () => {
diff --git a/typescript/Frontend/src/util/graph.util.tsx b/typescript/Frontend/src/util/graph.util.tsx index 0f06c4bcb..a2e52162c 100644 --- a/typescript/Frontend/src/util/graph.util.tsx +++ b/typescript/Frontend/src/util/graph.util.tsx @@ -35,29 +35,60 @@ export interface GraphData { [path: string]: GraphCoordinates; } +// connections must have the word Connection in the prop name, so the topology works correctly export type TopologyValues = { - gridToAcIn: BoxData[]; - acInBus: BoxData[]; - acOutBus: BoxData[]; - dcBus: BoxData[]; - mpptToDcBus: BoxData[]; - dcBusToDcDc: BoxData[]; - dcDCToBattery: BoxData[]; - battery: BoxData[]; + gridToAcInConnection: BoxData; + gridBus: BoxData; + islandBus: BoxData; + dcBus: BoxData; + dcBusToDcDcConnection: BoxData; + dcDCToBatteryConnection: BoxData; + battery: BoxData; + dcBusToLoadOnDcConnection: BoxData; + islandBusToLoadOnIslandBusConnection: BoxData; + gridBusToPvOnGridbusConnection: BoxData; + gridBusToLoadOnGridBusConnection: BoxData; }; +type TopologyPaths = { [key in keyof TopologyValues]: string[] }; + +export const topologyPaths: TopologyPaths = { + gridToAcInConnection: [ + "/GridMeter/Ac/L1/Power/Apparent", + "/GridMeter/Ac/L2/Power/Apparent", + "/GridMeter/Ac/L3/Power/Apparent", + ], + gridBus: ["/GridMeter/Ac/L2/Voltage"], + islandBus: [ + "/AcDc/Ac/L1/Voltage", + "/AcDc/Ac/L2/Voltage", + "/AcDc/Ac/L3/Voltage", + ], + dcBus: ["/AcDc/Dc/Voltage"], + dcBusToDcDcConnection: ["/DcDc/Dc/Link/Power"], + dcDCToBatteryConnection: ["/DcDc/Dc/Battery/Power"], + battery: ["/Battery/Soc", "/Battery/Dc/Voltage"], + dcBusToLoadOnDcConnection: ["/LoadOnDc/Power"], + islandBusToLoadOnIslandBusConnection: ["/LoadOnAcIsland/Ac/Power/Apparent"], + gridBusToPvOnGridbusConnection: ["/PvOnAcGrid/Power/Apparent"], + gridBusToLoadOnGridBusConnection: ["/LoadOnAcGrid/Power/Apparent"], +}; + +const getBoxColor = () => {}; + export const extractTopologyValues = ( timeSeriesData: DataPoint ): TopologyValues | null => { const timeSeriesValue = timeSeriesData.value; - let topologyValues: (string | number)[]; if (isDefined(timeSeriesValue)) { return Object.keys(topologyPaths).reduce((acc, topologyKey) => { - const values = topologyPaths[topologyKey].map( + let topologyValues: (string | number)[]; + const values = topologyPaths[topologyKey as keyof TopologyValues].map( (topologyPath) => timeSeriesValue[topologyPath] ); - switch (topologyKey) { - case "gridToAcIn": + console.log("AAA", topologyKey); + switch (topologyKey as keyof TopologyValues) { + case "gridToAcInConnection": topologyValues = [ values.reduce((acc, curr) => Number(acc) + Number(curr.value), 0), ]; @@ -65,50 +96,45 @@ export const extractTopologyValues = ( default: topologyValues = values.map(({ value }) => value); } + console.log("topologyValues", topologyValues); return { ...acc, - [topologyKey]: [ - { - values: topologyValues, - label: topologyPaths[topologyKey][0].split("/").pop(), - unit: values[0].unit, - } as BoxData, - ], + [topologyKey]: { + values: topologyValues, + label: topologyPaths[topologyKey as keyof TopologyValues][0] + .split("/") + .pop(), + unit: values[0].unit, + } as BoxData, }; }, {} as TopologyValues); } return null; }; -export const topologyPaths: { [key: string]: string[] } = { - gridToAcIn: [ - "/GridMeter/Ac/L1/Power/Apparent", - "/GridMeter/Ac/L2/Power/Apparent", - "/GridMeter/Ac/L3/Power/Apparent", - ], - acInBus: ["/GridMeter/Ac/L2/Voltage"], - acOutBus: [ - "/AcDc/Ac/L1/Voltage", - "/AcDc/Ac/L2/Voltage", - "/AcDc/Ac/L3/Voltage", - ], - dcBus: ["/AcDc/Dc/Voltage"], - mpptToDcBus: ["/Mppt/Dc/Power"], - dcBusToDcDc: ["/DcDc/Dc/Link/Power"], - dcDCToBattery: ["/DcDc/Dc/Battery/Power"], - battery: ["/Battery/Soc", "/Battery/Dc/Voltage"], +export const getHighestConnectionValue = (values: TopologyValues) => + Object.keys(values) + .filter((value) => value.includes("Connection")) + .reduce((acc, curr) => { + const value = values[curr as keyof TopologyValues].values[0] as number; + console.log("reduce", value, acc); + return value > acc ? value : acc; + }, 0); + +export const getAmount = ( + highestConnectionValue: number, + values: (string | number)[] +) => { + console.log("amount", values[0] as number, highestConnectionValue); + return (values[0] as number) / highestConnectionValue; }; export interface CsvEntry { value: string | number; unit: string; } -export interface Csv { - [key: string]: CsvEntry; -} -export const parseCsv = (text: string): Csv => { - console.log("split", text.split(/\r?\n/)); +export const parseCsv = (text: string): DataRecord => { const y = text .split(/\r?\n/) .filter((split) => split.length > 0) @@ -118,12 +144,12 @@ export const parseCsv = (text: string): Csv => { const x = y .map((fields) => { - if (typeof fields[1] === "string") { + if (isNaN(Number(fields[1]))) { return { [fields[0]]: { value: fields[1], unit: fields[2] } }; } return { [fields[0]]: { value: parseFloat(fields[1]), unit: fields[2] } }; }) - .reduce((acc, current) => ({ ...acc, ...current }), {} as Csv); + .reduce((acc, current) => ({ ...acc, ...current }), {} as DataRecord); return x; };