[WIP] add color and style to liveview, add new connections, refactor components, nearly finished

This commit is contained in:
Sina Blattmann 2023-06-19 17:23:09 +02:00
parent 4b9f3e6a84
commit 9e56dffb27
10 changed files with 286 additions and 165 deletions

View File

@ -53,10 +53,13 @@ const UsersWithInheritedAccess = () => {
<Link
id={"inherited-access-user-link-" + user.id}
to={
routes.groups + routes.manageAccess + user.parentId
routes.installations +
routes.tree +
routes.manageAccess +
user.parentId
}
>
{folderName}
{` ${folderName}`}
</Link>
</>
}

View File

@ -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 (

View File

@ -6,7 +6,6 @@ const Log = () => {
return (
<>
<TopologyView />
<ScalarGraph />
</>
);

View File

@ -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<FetchResult<DataRecord>> => {
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<RecordSeries>([]);
@ -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<FetchResult<Csv>> => {
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 (
<>
<LocalizationProvider dateAdapter={AdapterDayjs}>

View File

@ -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(),

View File

@ -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 (
<Box
sx={{
border: "1px solid grey",
visibility: props.title ? "visible" : "hidden",
height: BOX_SIZE + "px",
width: BOX_SIZE + "px",
borderRadius: "4px",
color: "white",
}}
>
<Box sx={{ padding: "5px" }}>
<p style={{ marginBlockStart: "2px" }}>{props.title}</p>
{props.data &&
props.data.map((el) => (
<div>
{el.label}
{el.values.map((value) => {
console.log("value", value);
return (
<p
style={{ marginBlockStart: "2px", marginBlockEnd: "2px" }}
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
>{`${
!isInt(Number(value)) ? Number(value).toPrecision(8) : value
}${el.unit}`}</p>
);
})}
</div>
))}
</Box>
<p
style={{
marginBlockStart: "0",
marginBlockEnd: "0",
backgroundColor: "#e74c3c",
padding: "5px",
borderTopLeftRadius: "4px",
borderTopRightRadius: "4px",
}}
>
{props.title}
</p>
<div
style={{
backgroundColor: "#c0392b",
borderBottomLeftRadius: "4px",
borderBottomRightRadius: "4px",
padding: "5px",
height: "52px",
}}
>
{props.data && (
<>
{props.data.values.map((value) => {
return (
<p
style={{ marginBlockStart: "0", marginBlockEnd: "2px" }}
// eslint-disable-next-line formatjs/no-literal-string-in-jsx
>{`${
!isInt(Number(value)) ? Number(value).toPrecision(4) : value
}${props.data?.unit}`}</p>
);
})}
</>
)}
</div>
</Box>
);
};

View File

@ -9,8 +9,8 @@
.container {
position: relative;
width: 130px;
height: 130px;
width: 150px;
height: 150px;
overflow: hidden;
}

View File

@ -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 (
<div
style={{
@ -28,38 +28,36 @@ const TopologyFlow = (props: TopologyFlowProps) => {
backgroundColor: "#f4b3504d",
visibility: props.hidden || !props.data ? "hidden" : "visible",
display: "flex",
alignItems: "center",
position: "relative",
justifyContent: "center",
}}
>
<p
style={{
position: "absolute",
zIndex: 1,
}}
>
{values?.map(
(value) => `${Math.round(value as number)} ${props.data?.unit}`
)}
</p>
<div
className="container"
style={{
transform:
props.orientation === "vertical"
? "rotate(90deg)"
: props.rightToLeft
: Number(values?.[0]) < 0
? "rotate(180deg)"
: "",
overflow: "hidden",
display: "flex",
height: BOX_SIZE,
width: BOX_SIZE,
alignItems: "center",
justifyContent: "center",
}}
>
<p
style={{
position: "absolute",
zIndex: 1,
transform:
props.orientation === "vertical"
? "rotate(-90deg)"
: props.rightToLeft
? "rotate(-180deg)"
: "",
}}
>
{props.data?.map((value) => value.values)}
</p>
<div className="data-flow" style={{ overflow: "hidden" }}>
<div className="data-flow">
<div className="dot"></div>
<div className="dot"></div>
<div className="dot"></div>

View File

@ -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<TopologyValues | null>(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 (
<Box
sx={{
display: "flex",
flexDirection: "row",
overflow: "auto",
padding: 2,
fontFamily: `"Ubuntu", sans-serif`,
fontSize: "12px",
}}
>
<div>
<TopologyColumn
centerBox={{
title: "Grid",
data: values.acInBus,
}}
centerConnection={{
amount: 0.6,
data: values.gridToAcIn,
rightToLeft: true,
amount: getAmount(
highestConnectionValue,
values.gridToAcInConnection.values
),
data: values.gridToAcInConnection,
}}
/>
</div>
<div>
<TopologyColumn
centerBox={{
title: "AcInBus",
data: values.acInBus,
title: "GridBus",
data: values.islandBus,
}}
centerConnection={{
amount: 0.3,
data: values.gridToAcIn,
data: values.gridToAcInConnection,
}}
topConnection={{
amount: getAmount(
highestConnectionValue,
values.gridBusToPvOnGridbusConnection.values
),
data: values.gridBusToPvOnGridbusConnection,
}}
topBox={{
title: "PvOnGridBus",
}}
bottomConnection={{
amount: getAmount(
highestConnectionValue,
values.gridBusToLoadOnGridBusConnection.values
),
data: values.gridBusToLoadOnGridBusConnection,
}}
bottomBox={{
title: "LoadOnGridBus",
}}
/>
</div>
<div>
<TopologyColumn
centerBox={{
title: "AcOutBus",
data: values.acOutBus,
title: "IslandBus",
data: values.islandBus,
}}
centerConnection={{
amount: 0.5,
data: values.gridToAcIn,
data: values.gridToAcInConnection,
}}
bottomBox={{
title: "LoadOnIslandBus",
}}
bottomConnection={{
amount: getAmount(
highestConnectionValue,
values.islandBusToLoadOnIslandBusConnection.values
),
data: values.islandBusToLoadOnIslandBusConnection,
}}
/>
</div>
@ -62,32 +121,40 @@ const TopologyView = () => {
<TopologyColumn
centerBox={{
title: "Inverter",
data: values.acOutBus,
data: values.islandBus,
}}
centerConnection={{
amount: 0.3,
data: values.gridToAcIn,
data: values.gridToAcInConnection,
}}
/>
</div>
<div>
<TopologyColumn
topBox={{
title: "MPPT",
data: values.acOutBus,
title: "PvOnDc",
}}
topConnection={{
amount: 0.6,
data: values.gridToAcIn,
data: values.gridToAcInConnection,
}}
centerBox={{
title: "DcBus",
data: values.acOutBus,
data: values.dcBus,
}}
centerConnection={{
amount: getAmount(
highestConnectionValue,
values.dcBusToDcDcConnection.values
),
data: values.dcBusToDcDcConnection,
}}
bottomConnection={{
amount: 0.2,
data: values.gridToAcIn,
rightToLeft: true,
data: values.dcBusToLoadOnDcConnection,
}}
bottomBox={{
title: "LoadOnDc",
}}
/>
</div>
@ -95,11 +162,14 @@ const TopologyView = () => {
<TopologyColumn
centerBox={{
title: "DcDc",
data: values.acOutBus,
data: values.islandBus,
}}
centerConnection={{
amount: 0.8,
data: values.gridToAcIn,
amount: getAmount(
highestConnectionValue,
values.dcDCToBatteryConnection.values
),
data: values.dcDCToBatteryConnection,
}}
/>
</div>
@ -107,7 +177,7 @@ const TopologyView = () => {
<TopologyColumn
centerBox={{
title: "Battery",
data: values.acOutBus,
data: values.battery,
}}
/>
</div>

View File

@ -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;
};