import json import os import string from math import cos from datetime import datetime import dash import numpy as np from dash import Dash, html, dcc, Output, Input, State, MATCH, ctx, ClientsideFunction, ALL from dash.exceptions import PreventUpdate from python.gui.cytoscape import style, buildEdges, buildNodes from python.gui.flatten import flattenDevice from python.gui.templates import Device, find_Device, now, find_Device_strict import dash_cytoscape as cyto # TODO Repair the Cytoscape Node Click Callbacks toggling visibility of the associated graphs # TODO CSS BUG: the graphs don't float to the top. (check only DcDc current for example) # TODO Startup Bug: ~~This should resolve itself once we have realtime data~~ # TODO Beautify and Test (E.G Units) # External css and javascript for the dash app external_stylesheets = [ './assets/header.css', ] external_scripts = [ './assets/inserted.js', ] # Main Dash App Webserver app = Dash(__name__, external_stylesheets=external_stylesheets, external_scripts=external_scripts) # First: Build Topology # Grabs device names and connections from topology file # NOTE: Device changes are only possible on restart TODO grab topology from s3 deviceNames = [] connectionsL = [] connectionsT = [] connectionsB = [] connectionsR = [] devices = [] topology = json.load(open(f"./assets/data/topology1671435263")) def psum(l : list) -> float: sum = 0 for k in l: if k: sum += k return float(sum) #This builds the cytoscape nodes (deviceNames, and Edges (connections) for i, bus in enumerate(topology["Topology"]): # if not bus["Name"] in deviceNames: for key in bus.keys(): nodeName = bus[key].split(':',1)[0] if not nodeName or nodeName == "None" or nodeName == "Losses": continue if not nodeName in deviceNames: deviceNames.append(nodeName) dev = Device(nodeName) new = True elif key == "Name": new = False dev = find_Device_strict(nodeName, devices)[0] else: continue invisible = False pos = [0,0] buspower = [] if len(bus[key].split(':'))>2 and bus[key].split(':')[2] == "hide": invisible = True if invisible: dev.connection = [bus["Name"], nodeName] match key: case "Left": pos = [i, 1] if invisible: if topology["Topology"][i - 1]["Name"] != None: nodeName = topology["Topology"][i - 1]["Name"] if not (bus["Name"], nodeName) in connectionsL: connectionsL.append((bus["Name"], nodeName)) connectionsR.append((nodeName, bus["Name"])) case "Right": pos = [i + 2, 1] if invisible: if len(topology["Topology"]) > i and topology["Topology"][i + 1]["Name"] != None != "None": nodeName = topology["Topology"][i + 1]["Name"] if not (bus["Name"], nodeName) in connectionsR: connectionsR.append((bus["Name"], nodeName)) connectionsL.append((nodeName, bus["Name"])) case "Name": pos = [i + 1, 1] if new: buspower = [bus[key] for key in bus.keys() if not key == "Name"] if bus["Name"] == nodeName == "Dc": connectionsR.append(("Inverter", bus["Name"])) connectionsL.append((nodeName, "Inverter")) if bus["Name"] == nodeName == "DcDc": connectionsL.append((nodeName, "Dc")) case "Top": pos = [i + 1, 0] if not (bus["Name"], nodeName) in connectionsT: connectionsT.append((nodeName, bus["Name"])) case "Bottom": pos = [i + 1, 2] if not (bus["Name"], nodeName) in connectionsB: connectionsB.append((bus["Name"], nodeName)) if buspower != []: dev.Buspower = buspower if invisible: pos = [0, 0] dev.position = pos if new: devices.append(dev) # NOTE: Device changes are only possible on restart #deviceNames = ['Grid','PvOnAcIn','ac_in','ac_last','ac_out','Inverter','dc_last','PvOnDc','dc_bus','DcDc','battery'] # devices = [Device(name) for name in deviceNames] # data for the cytoscape graph found in ./cytoscape.py nodes = buildNodes(devices) edges = buildEdges(connectionsT + connectionsB + connectionsL + connectionsR) elements = nodes + edges # Second: Grab initial GraphData # Building initial device Data TODO implement Local/S3 History path = "./assets/data3/" allFileNames = os.listdir(path) fileNames = allFileNames[0:180] # fileNames = [f"./assets/data/{i}" for i in range(1671435263, 1671435443, 2)] #range(1669730732, 1669731092, 2)] # timeData is a dict of dicts with the keys being the timestamps timeData = {} for i, file in enumerate(fileNames): file = json.load(open(path + file)) timeData.update({fileNames[i]:{}}) for device in file["Devices"]: timeData[fileNames[i]].update(flattenDevice(device)) # Building initial device graphs (this considerably reduces input lag, but increases start time of the program) for time in timeData.keys(): for key in timeData[time].keys(): device = find_Device_strict(key.split("/")[0], devices)[0] if not device: continue #TODO THROW ERROR if not time in (device.data.keys()): device.data.update({time: {}}) if not len(key.split("/", 1)) < 2: device.data[time].update({key.split("/", 1)[1]: timeData[time][key]}) debug = [] #----------------------------------------------------------------------------------------------------------------------- # Callbacks # This shifts the website viewpoint to the graphs of the node that has been clicked on # And toggles visibility of graphs associated with the node-device # using Javascript found in clientside.js app.clientside_callback( ClientsideFunction( namespace='clientside', function_name='clickNode' ), Output('placeholder', 'data'), Input('view', 'data') ) # Clock callback, every second updates the time TODO MAKE ME CLIENTSIDE? # This also resets tapNodeData and tapEdgeData for correct handling of clicks on nodes/edges @app.callback([Output('title', 'children'), Output('cytoscape-elements', 'tapNodeData'), Output('cytoscape-elements', 'tapEdgeData')], Input('interval-component3', 'n_intervals')) def update_time_live(n): return ''' Echtzeit Daten-Monitoring ''' + str(np.datetime64('now')).replace('T', ' '), {}, {} # Updates the powergraph connections every 2 seconds, also tracks the highest power measured in 'max-power' dcc.Store @app.callback(Output('cytoscape-elements', 'elements'), Output('max-power', 'data'), Input('interval-component-2', 'n_intervals'), State('max-power', 'data')) def update_power_graph(n, maxPower): for i, device in enumerate(devices): # device.UpdatePower() if device.name == "Battery": continue power = device.power #TODO as long as device is not inverter None will be in power if power == [None, None] and device.Buspower == []: continue rpower = [] # Left top bottom right sum = psum(power) if sum > maxPower: maxPower = sum #TODO handle inverter two power measurements.. # Setting nodes to either a producer or consumer depending on power out/in for node in ( nodes[y] for y in range(0, len(nodes)) if nodes[y]['data']['id'] == device.name): if sum >= 0: node.update({'classes': node.get('classes') + ' producer'}) else: node.update({'classes': node.get('classes') + ' consumer'}) # Updates the power values on the connections if device.Buspower != []: for remote in device.Buspower: remoteDevice = find_Device_strict(remote.split(":", 1)[0], devices)[0] if None in remoteDevice.power: remotePower = remoteDevice.power[0] if remoteDevice.power[0] != None else remoteDevice.power[1] else: #Inverter if "Ac" in device.name: remotePower = remoteDevice.power[0] else: remotePower = remoteDevice.power[1] rpower.append(remotePower) #or edges[y]['data']['source'] in device.Buspower) if None in power: if rpower != []: if rpower[0] <= 0: for connection in (y for y in connectionsL if device.name == y[0]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str((rpower[0]/maxPower) * 20) + 'px'}}) edge['data'].update({'label': [str(int(abs(rpower[0]))) + "W"]}) for connection in (y for y in connectionsR if device.name == y[0]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str(0) + 'px'}}) edge['data'].update({'label': []}) else: for connection in (y for y in connectionsR if device.name == y[1]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str((rpower[0]/maxPower) * 20) + 'px'}}) edge['data'].update({'label': [str(int(abs(rpower[0]))) + "W"]}) for connection in (y for y in connectionsL if device.name == y[1]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str(0) + 'px'}}) edge['data'].update({'label': []}) if rpower[3] >= 0: #This should always be the case for connection in (y for y in connectionsR if device.name == y[0]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str((rpower[3]/maxPower) * 20) + 'px'}}) edge['data'].update({'label': [str(int(rpower[3])) + "W"]}) for connection in (y for y in connectionsL if device.name == y[1]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str(0) + 'px'}}) edge['data'].update({'label': []}) else: for connection in (y for y in connectionsL if device.name == y[1]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str((rpower[3] / maxPower) * 20) + 'px'}}) edge['data'].update({'label': [str(int(abs(rpower[3]))) + "W"]}) for connection in (y for y in connectionsR if device.name == y[0]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str(0) + 'px'}}) edge['data'].update({'label': []}) if not rpower[2]: rpower[2] = 0 for connection in (y for y in connectionsB if device.name == y[0]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str((abs(rpower[0] + rpower[2] - rpower[3]) / maxPower) * 20) + 'px'}}) edge['data'].update({'label': [str(int(abs(rpower[0] + rpower[2] - rpower[3]))) + "W"]}) continue if not sum: continue if sum >= 0: for connection in (y for y in connectionsR + connectionsT if device.name == y[0]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str((sum/maxPower) * 20) + 'px'}}) edge['data'].update({'label': [str(int(sum)) + "W"]}) for connection in (y for y in connectionsL if device.name == y[1]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str(0) + 'px'}}) edge['data'].update({'label': []}) elif sum < 0: for connection in (y for y in connectionsR + connectionsT if device.name == y[0]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str(0) + 'px'}}) edge['data'].update({'label': []}) for connection in (y for y in connectionsL if device.name == y[1]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str((sum/maxPower) * 20) + 'px'}}) edge['data'].update({'label': [str(abs(int(sum))) + "W"]}) else: #This is the inverter if not sum: continue # if power[0] < 0: # #Ac side # # for connection in (y for y in connectionsL if device.name == y[0]): # # for edge in (edges[y] for y in range(0, len(edges)) if # # edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): # # edge.update({'style': {'width': str((power[0]/maxPower) * 10) + 'px'}}) # # edge['data'].update({'label': [str(int(power[0])) + "W"]}) # # else: # # for connection in (y for y in connectionsL if device.name == y[0]): # # for edge in (edges[y] for y in range(0, len(edges)) if # # edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): # # edge.update({'style': {'width': 0 + 'px'}}) # # edge['data'].update({'label': []}) if power[1] > 0: #DC side for connection in (y for y in connectionsR if device.name == y[0]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str((power[1]/maxPower) * 20) + 'px'}}) edge['data'].update({'label': [str(int(power[1])) + "W"]}) for connection in (y for y in connectionsL if device.name == y[1]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str(0) + 'px'}}) edge['data'].update({'label': []}) elif power[1] < 0: #DC side for connection in (y for y in connectionsL if device.name == y[1]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str(abs(power[1]/maxPower) * 20) + 'px'}}) edge['data'].update({'label': [str(int(abs(power[1]))) + "W"]}) for connection in (y for y in connectionsR if device.name == y[0]): for edge in (edges[y] for y in range(0, len(edges)) if edges[y]['data']['source'] == connection[0] and edges[y]['data']['target'] == connection[1]): edge.update({'style': {'width': str(0) + 'px'}}) edge['data'].update({'label': []}) return elements, maxPower # Handles the visibility checkmarks through wildcards @app.callback([Output({'type': 'graphs', 'index': MATCH}, "children"), Output({'type': 'checklist', 'index': MATCH}, "value")], [Input({'type': 'checklist', 'index': MATCH}, "value"), Input({'type': 'topChecklist', 'index': MATCH}, "value")], State({'type': 'graphs', 'index': MATCH}, "children") , prevent_initiall_call=True) def graph_visibility_checklist(value, top, graphs): if not graphs or not ctx.triggered_id: raise PreventUpdate #Setting all invisible and turning selected back on for graph in graphs: graph["props"]["hidden"] = True if top != [] and ctx.triggered_id.type == 'topChecklist': id = [] for graph in graphs: id.append(graph["props"]["children"]["props"]["id"]) graph["props"]["hidden"] = False for i in id: for dev in find_Device(i, devices): dev.hidden = False return graphs, id elif ctx.triggered_id.type == 'topChecklist': return graphs, [] if not value: return graphs, dash.no_update for graph in (graph for graph in graphs if (any(graph["props"]["children"]["props"]["id"]["role"] in i["role"] for i in value) or any(i["role"] in graph["props"]["children"]["props"]["id"]["role"] for i in value))): graph["props"]["hidden"] = False for i in value: for dev in find_Device(i["role"], devices): dev.hidden = False return graphs, dash.no_update # The small number next to the graph triggers are the latest Value or an average thereof (AC) @app.callback(Output({'type': 'checklist', 'index': ALL}, "options"), Input('interval-component3', 'n_intervals'), [State({'type': 'graphs', 'index': ALL}, "children"), State({'type': 'checklist', 'index': ALL}, "options")] ) def lable_previews(n, graphs, options): for x in graphs: if x != []: for g in x: for l in options: if l!=[]: for a in l: if g["props"]["children"]["props"]["id"]["role"].split(" ",1)[1].replace(" ", "") in a["label"][0].replace(" ", ""): val = 0 if g["props"]["children"]["props"]["figure"]["data"] != [] and len(g["props"]["children"]["props"]["figure"]["data"][0]["y"]) != 0: if len(g["props"]["children"]["props"]["figure"]["data"]) == 3: val = round((g["props"]["children"]["props"]["figure"]["data"][0]["y"][-1] + g["props"]["children"]["props"]["figure"]["data"][1]["y"][-1] + g["props"]["children"]["props"]["figure"]["data"][2]["y"][-1])/3, 1) else: if g["props"]["children"]["props"]["figure"]["data"][0]["y"][-1] != []: val = round(g["props"]["children"]["props"]["figure"]["data"][0]["y"][-1], 1) a["label"][0] = (a["label"][0].rstrip(string.digits + string.punctuation + string.whitespace) + " " + str(val)) return options # the second output triggers the viewport callback nodeViewShift TODO MAKE ME CLIENTSIDE? @app.callback(Output('view', 'data'), [Input('cytoscape-elements', 'tapNodeData'), Input('cytoscape-elements', 'tapEdgeData')], State('view', 'data'),) def cytoscape_visibility_clicks(nodeData, edgeData, view): if nodeData and nodeData != {}: return '{"index":%s,"type":"topChecklist"}' % (find_Device_strict(nodeData["id"], devices)[0].index) elif edgeData and edgeData != {}: return '{"index":%s,"type":"topChecklist"}' % (find_Device_strict(edgeData["source"], devices)[0].index) return dash.no_update # Updates the individual graphs every second @app.callback(Output('OurGraphs', 'children'), Input('interval-component', 'n_intervals'), State('OurGraphs', 'children'), prevent_initiall_call=True) def update_graphs(n, graphs): # TODO GRAB NEW DATA FROM DISK OR S3 timeData = {} #Local copy of allFileNames because we loop over them at the moment number = [item for item in allFileNames if item not in fileNames][n % len(allFileNames)-1] fileNames.pop(0) fileNames.append(str(number)) filename = path + str(number) file = json.load(open(filename)) timeData.update({filename: {}}) for device in file["Devices"]: timeData[filename].update(flattenDevice(device)) for device in devices: if not filename in (device.data.keys()): device.data.update({filename: {}}) for key in (key for key in timeData[filename].keys() if key.split("/",1)[0] == device.name): device.data[filename].update({key.split("/", 1)[1]: timeData[filename][key]}) for k, device in enumerate(graphs): for graph in device["props"]["children"][0]["props"]["children"]: id = graph["props"]["children"]["props"]["id"] fig = graph["props"]["children"]["props"]["figure"] if "Power" in id["role"].split(" ", 1)[1]: if "Ac" in id["role"].split(" ", 1)[1]: # 3-phase AC fig["data"][0]['y'][0] = (devices[k].data[list(devices[k].data.keys())[-1]]["Ac"][0]["Voltage"] * devices[k].data[list(devices[k].data.keys())[-1]]["Ac"][0]["Current"] * cos(devices[k].data[list(devices[k].data.keys())[-1]]["Ac"][0]["Phi"])+ devices[k].data[list(devices[k].data.keys())[-1]]["Ac"][1]["Voltage"] * devices[k].data[list(devices[k].data.keys())[-1]]["Ac"][1]["Current"] * cos(devices[k].data[list(devices[k].data.keys())[-1]]["Ac"][1]["Phi"])+ devices[k].data[list(devices[k].data.keys())[-1]]["Ac"][2]["Voltage"] * devices[k].data[list(devices[k].data.keys())[-1]]["Ac"][2]["Current"] * cos(devices[k].data[list(devices[k].data.keys())[-1]]["Ac"][2]["Phi"])) devices[k].power[0] = fig["data"][0]['y'][0] if "Dc" in id["role"].split(" ", 1)[1] and not "Dc48" in devices[k].data[list(devices[k].data.keys())[-1]]: # DC fig["data"][0]['y'][0] = (devices[k].data[list(devices[k].data.keys())[-1]]["Dc"]["Voltage"] * devices[k].data[list(devices[k].data.keys())[-1]]["Dc"]["Current"]) devices[k].power[1] = fig["data"][0]['y'][0] if "Dc48" in devices[k].data[list(devices[k].data.keys())[-1]]: fig["data"][0]['y'][0] = (devices[k].data[list(devices[k].data.keys())[-1]]["Dc48"]["Voltage"] * devices[k].data[list(devices[k].data.keys())[-1]]["Dc48"]["Current"]) devices[k].power[1] = fig["data"][0]['y'][0] fig["data"][0]['x'][0] = now(filename) fig["data"][0]['y'] = fig["data"][0]['y'][-1:] + fig["data"][0]['y'][:-1] fig["data"][0]['x'] = fig["data"][0]['x'][-1:] + fig["data"][0]['x'][:-1] continue if "Dc" in id["role"].split(" ",1)[1] or "Ac" in id["role"].split(" ",1)[1] or "Alarm" in id["role"] and "Actual" not in id["role"] : for i, g in enumerate(fig["data"]): if g["x"] == [] or g["y"] == []: continue g["x"].pop(0) g["y"].pop(0) g["x"].append(now(filename)) if "Ac" in id["role"].split(" ",1)[1] and "Actual" not in id["role"] and "Power" not in id["role"]: g["y"].append(devices[k].data[list(devices[k].data.keys())[-1]][ id["role"].split(" ", 1)[1].split(' ')[0]][i][ id["role"].split(" ", 1)[1].split(' ')[1]]) elif "Dc48" in devices[k].data[list(devices[k].data.keys())[-1]] and "Actual" not in id["role"] and "Power" not in id["role"]: g["y"].append(devices[k].data[list(devices[k].data.keys())[-1]]["Dc48"][ id["role"].split(" ", 1)[1].split(' ')[1]]) elif "Dc" in id["role"].split(" ",1)[1] and "Actual" not in id["role"] and "Power" not in id["role"]: g["y"].append(devices[k].data[list(devices[k].data.keys())[-1]][ id["role"].split(" ", 1)[1].split(' ')[0]][ id["role"].split(" ", 1)[1].split(' ')[1]]) elif "Alarm" in id["role"].split(" ",1)[1]: g["y"].append(1 if g["name"] in devices[k].data[list(devices[k].data.keys())[-1]][ id["role"].split(" ", 1)[1].split(' ')[0]] else 0) continue elif "Actual" in id["role"] or fig["data"] ==[]: continue fig["data"][0]['y'][0] = devices[k].data[list(devices[k].data.keys())[-1]][id["role"].split(" ", 1)[1]] fig["data"][0]['x'][0] = now(filename) fig["data"][0]['y'] = fig["data"][0]['y'][-1:] + fig["data"][0]['y'][:-1] fig["data"][0]['x'] = fig["data"][0]['x'][-1:] + fig["data"][0]['x'][:-1] return graphs # Main HTML Layout of the app ------------------------------------------------------------------------------------------ app.layout = html.Div(children=[ # Static Header with logo and Clock html.Header(className='bp-page-header', children=[ html.Img(src="./assets/innovenergy_Logo_onOrange.png", className="logo"), html.H1(children='Salimax Gui Demo'), html.H4(className="clock", id="title", children=[''' Echtzeit Daten-Monitoring ''' + str(datetime.now().replace(microsecond=0))]) ]), #Big powergraph "overview" html.Div(id="powerGraph", className='powerGraph', children= cyto.Cytoscape( id='cytoscape-elements', layout={'name': 'preset'}, style={'position': 'relative', 'width': '100%', 'height': '600px', 'margin': 'auto'}, elements=elements, stylesheet=style, userZoomingEnabled=False, userPanningEnabled=False, ) ), #html.Div(id="Divider", className="divider"), # html.Div(id="graph_buttons", children=[device.BuildButton() for device in devices]), # Don't be fooled, these are all the plots! They are built up in the callbacks html.Div(id="OurGraphs",className="graphs",children=[device.BuildDiv(i) for i,device in enumerate(devices)]), html.Div(id="Checklist",className="sidenav",children=[device.BuildChecklist(i) for i,device in enumerate(devices)]), # Every Second triggers updates to clock and plots dcc.Interval( id='interval-component3', interval=1 * 1000, # in milliseconds n_intervals=0 ), # Every two seconds updates plots dcc.Interval( id='interval-component', interval=1 * 1000, # in milliseconds n_intervals=0 ), # Every 5 seconds triggers PowerGraph Update dcc.Interval( id='interval-component-2', interval=1 * 1100, # in milliseconds n_intervals=0 ), # fake Cookie for the highest seen power used for graphing dcc.Store( id='max-power', data=1000 ), # fake cookie used to transfer viewport shift id data to clientside javascript dcc.Store( id='view', data=0 ), # Placeholder for Output callbacks which have no return value dcc.Store(id='placeholder', data=0) ]) # Main App Start on specified ip (here local) if __name__ == '__main__': app.run_server(host= 'localhost', debug=False, port=8080)