diff --git a/base.py b/base.py index d2e39b2..d8bf5f6 100644 --- a/base.py +++ b/base.py @@ -6,9 +6,11 @@ ONEYEAR = 366 * 24 * 3600 def get_abs_time(times): - """ - Gets the absolute times for the given potential relative times. If a given timestamp is less than - one year, then the value is relative and converted into an absolute timestamp + """Gets the absolute times for the given potential relative times. + + If a given timestamp is less than one year, then the value is + relative (to now, rounded up to a full second) and converted + into an absolute timestamp Parameters : times([(float)]) : an array of unix timestamps or relative duration (< 1 year) as floats diff --git a/chart_config.py b/chart_config.py index a71568a..f61bc43 100644 --- a/chart_config.py +++ b/chart_config.py @@ -1,4 +1,6 @@ from configparser import ConfigParser + + class ChartConfig: """ Class that holds the chart section of a configuration file (for an instrument). @@ -6,14 +8,42 @@ class ChartConfig: Attributes : chart_config (Section) : the Section corresponding to the "chart" section in the given configuration file """ + + KEYS = ["cat", "color", "unit", "label"] + def __init__(self, path): """ Parameters : path (str) : the path to the configuration file """ + self.errors = {} + self.variables = {} cfgp = ConfigParser(interpolation=None) + cfgp.optionxform = str cfgp.read(path) - self.chart_config = cfgp["chart"] + section = cfgp["chart"] + for key, raw_value in section.items(): + if len(key.split('.')) > 1: + self.errors[key] = f'illegal key: {key}' + continue + arguments = raw_value.split(",") + keyword_mode = False + config = {'cat': '*'} + for i, argument in enumerate(arguments): + argname, _, argvalue = argument.rpartition(':') + if argname: + keyword_mode = True + config[argname] = argvalue + else: + if keyword_mode: + self.errors[key] = f"positional arg after keywd arg: {key}={raw_value!r}" + else: + try: + if argvalue: + config[self.KEYS[i]] = argvalue + except Exception as e: + self.errors[key] = f"{e!r} in {key}={raw_value}" + self.variables[key] = config def get_variable_parameter_config(self, key): """ @@ -27,25 +57,4 @@ class ChartConfig: The different options are in this dict if they are found in the chart section for the given key. Returns None if the key is not in the chart section, or if there is a syntax problem for the given key. """ - config = {} - positionnal = ["cat", "color", "unit"] - if key in self.chart_config.keys(): - raw_value = self.chart_config[key] - - arguments = raw_value.split(",") - keyword_mode = False - for i, argument in enumerate(arguments): - pieces = argument.split(":") - if len(pieces) == 2: - keyword_mode = True - if pieces[1] != "": - config[pieces[0]] = pieces[1] - else: - if not keyword_mode: #everything is going well - if pieces[0] != "": - config[positionnal[i]] = pieces[0] - else: #we cannot have a positionnal argument after a keyword argument - return None - return config - else: - return None + return self.variables.get(key) diff --git a/client/jsFiles/SEAWebClientCommunication.js b/client/jsFiles/SEAWebClientCommunication.js index 1b52d03..3cbacde 100644 --- a/client/jsFiles/SEAWebClientCommunication.js +++ b/client/jsFiles/SEAWebClientCommunication.js @@ -111,8 +111,8 @@ function handleUpdateMessage(src, message) { instrument.style.width = 'auto' device.style.width = 'auto' instrument.innerHTML = message.instrument - device.innerHTML = message.device - // console.log('ID', initCommands); + // device.innerHTML = message.device + console.log('ID', initCommands); nextInitCommand(); break; // console-update-message: Confirms a command. @@ -306,7 +306,7 @@ function reqJSONPOST(s, url, parameters, successHandler, errorHandler) { var xhr = typeof XMLHttpRequest != 'undefined' ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'); if (debugCommunication) { - console.log("%cto server (reqJSON): %s", + console.log("%cto server (reqJSONPOST): %s", "color:white;background:lightgreen", url); } xhr.open('post', url, true); @@ -406,8 +406,11 @@ function successHandler(s, message) { begin = timeRange[0] - timeRange[1]; select.value = begin; // Server-request for variable-list.*/ - reqJSONPOST(0, "http://" + hostPort + "/getvars", "time=" + timeRange[1] + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id=" - + clientID, successHandler, errorHandler); + reqJSONPOST(0, "http://" + hostPort + "/getvars", + "time=" + timeRange[1] + + window['clientTags'] + + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + + "&id=" + clientID, successHandler, errorHandler); break; // Response to a "getvars"-server-request. case "var_list": @@ -427,6 +430,7 @@ function successHandler(s, message) { nextInitCommand(); }*/ // graphs.receivedVars(message.blocks); + document.getElementById("device").innerHTML = message.device graphs.initGraphs(message.blocks); nextInitCommand(); break; diff --git a/client/jsFiles/SEAWebClientGraphics.js b/client/jsFiles/SEAWebClientGraphics.js index 49618e7..6a80407 100644 --- a/client/jsFiles/SEAWebClientGraphics.js +++ b/client/jsFiles/SEAWebClientGraphics.js @@ -371,10 +371,9 @@ function loadExportPopup(){ */ function exportCallback(selectedVariables, startDateTimeMs, endDateTimeMs, nan, binning=null){ - let binningParam = "None"; - if (binning !== null) - binningParam = binning - let exportURL = "http://" + hostPort + "/export?time=" + startDateTimeMs/1000 + "," + endDateTimeMs/1000 + "&variables=" + selectedVariables + "&nan=" + nan + "&interval=" + binningParam + "&id=" + clientID + if (binning === null || binning == "None") + binning = ""; + let exportURL = "http://" + hostPort + "/export?time=" + startDateTimeMs/1000 + "," + endDateTimeMs/1000 + "&variables=" + selectedVariables + "&nan=" + nan + "&interval=" + binning + "&id=" + clientID let a = document.createElement('a'); a.href = exportURL a.download = true @@ -417,7 +416,7 @@ let graphs = (function (){ let minTime, maxTime; // the queried time range let lastTime = 0; // time of most recent data point - let resolution = undefined; // current window resolution (ms/pixel) + // let resolution = undefined; // current window resolution (ms/pixel) let activateUpdateTimeout = undefined; // timeout for the activateUpdates function let updateAutoTimeout = undefined; // timeout for the updateAuto function (used in onZoomCompleteCallback) @@ -542,11 +541,12 @@ let graphs = (function (){ varlist = vars_array[gindex]; let graph_elm = graph_elm_array[gindex]; - timeDeltaAxis = maxTime - minTime - setResolution(timeDeltaAxis) - - AJAX("http://" + hostPort + "/graph?time=" + minTime/1000 + "," + maxTime/1000 + "&variables=" + varlist + "&interval=" + resolution + "&id=" + clientID).getJSON().then(function(data){ + resolution = getResolution((maxTime - minTime) / 1000) + AJAX("http://" + hostPort + "/graph?time=" + minTime/1000 + "," + maxTime/1000 + + "&variables=" + varlist + + "&interval=" + resolution + + "&id=" + clientID).getJSON().then(function(data){ //console.log('Graph', block, data) let graph = new Graph(gindex, graph_elm, "Time", block.unit, block.tag, type); graph_array[gindex] = graph; @@ -777,10 +777,12 @@ let graphs = (function (){ max = max/1000; } - timeDelta = currentMaxTime - currentMinTime - setResolution(timeDelta) + resolution = getResolution((currentMaxTime - currentMinTime) / 1000) - AJAX("http://" + hostPort + "/graph?time=" + min + ","+max+"&variables=" + variables() + "&interval=" + resolution + "&id=" + clientID).getJSON().then(function(data){ + AJAX("http://" + hostPort + "/graph?time=" + min + ","+max + +"&variables=" + variables() + + "&interval=" + resolution + + "&id=" + clientID).getJSON().then(function(data){ for(let key in data.graph){ let pdata = []; for(let e of data.graph[key]){ @@ -893,10 +895,10 @@ let graphs = (function (){ * Sets the resolution of the viewing window in milliseconds * @param {*} timeDelta - The difference between the maximum time and the minimum time of the window */ - function setResolution(timeDelta){ - resolution = Math.ceil((timeDelta / container.getBoundingClientRect().width)) + function getResolution(timeDelta){ + return Math.ceil((timeDelta / container.getBoundingClientRect().width)) } - + /** * The callback to be called when the user click on the "Jump" button of the date selector * Gets the vars + device name for the selected date+time, then rebuilds the graphs @@ -915,7 +917,11 @@ let graphs = (function (){ msRightTimestampGetVars = dateTimestampMs + timeValueMs; msRightTimestampGetGraph = dateTimestampMs + 24*60*60*1000; - AJAX("http://" + hostPort + "/getvars").postForm("time=" + msRightTimestampGetVars/1000 + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id="+ clientID).then(function(data){ + AJAX("http://" + hostPort + "/getvars").postForm( + "time=" + msRightTimestampGetVars/1000 + + window['clientTags'] + + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + + "&id="+ clientID).then(function(data){ blocks = data.blocks; document.getElementById("device").innerHTML = data.device maxTime = msRightTimestampGetGraph; @@ -989,7 +995,11 @@ let graphs = (function (){ window["wideGraphs"] = false; // will have no effect if hideRightPart is true adjustGrid(); - AJAX("http://" + hostPort + "/getvars").postForm("time=" + msRightTimestamp/1000 + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id="+ clientID).then(function(data){ + AJAX("http://" + hostPort + "/getvars").postForm( + "time=" + msRightTimestamp/1000 + "&userconfiguration=" + + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + + window['clientTags'] + + "&id="+ clientID).then(function(data){ currentMaxTime = msRightTimestamp + 60000; currentMinTime = msLeftTimestamp; @@ -1291,7 +1301,11 @@ let graphs = (function (){ function applySettingsCallback(userConfiguration){ cursorLine(null); - AJAX("http://" + hostPort + "/getvars").postForm("time=" + currentMaxTime/1000 + "&userconfiguration=" + JSON.stringify(userConfiguration) + "&id="+ clientID).then(function(data){ + AJAX("http://" + hostPort + "/getvars").postForm( + "time=" + currentMaxTime/1000 + + "&userconfiguration=" + JSON.stringify(userConfiguration) + + window['clientTags'] + + "&id="+ clientID).then(function(data){ blocks = data.blocks; document.getElementById("device").innerHTML = data.device maxTime = currentMaxTime; diff --git a/client/jsFiles/SEAWebClientMain.js b/client/jsFiles/SEAWebClientMain.js index 195baa8..b54fabd 100644 --- a/client/jsFiles/SEAWebClientMain.js +++ b/client/jsFiles/SEAWebClientMain.js @@ -91,6 +91,17 @@ new Settings() .treat("hideRightPart", "hr", to_bool, false) //used to completely disable the right part .treat("wideGraphs", "wg", to_bool, false) //used to toggle the size of the graphs part .treat("showAsync", "sa", to_bool, false) + .treat("device", "dev", 0, "*") + .treat("server", "srv", 0, "*") + .treat("instrument", "ins", 0, "") + +if (window['instrument']) { + window['clientTags'] = "&instrument=" + window["instrument"]; +} else { + window['clientTags'] = "&stream=" + window["server"] + "&device=" + window["device"]; +} + +console.log('TAGS', window['clientTags']); function loadFirstBlocks() { if (debug_main_daniel) { @@ -196,9 +207,9 @@ window.onload = function() { // var homeButton = document.getElementById("home-icon"); // TODO : uncomment this code with the right URL to navigate to when the way to select the instrument will be decided. - // homeButton.onclick = function () { - // window.location = "http://" + location.hostname + ":8800/"; - // }; + homeButton.onclick = function () { + window.location = "/select"; + }; buildUpdateConnection(); // if (location.hash) { // console.log("hash in url", location.hash); diff --git a/colors.py b/colors.py index c800b13..0ca8e7d 100644 --- a/colors.py +++ b/colors.py @@ -18,7 +18,7 @@ def assign_colors_to_curves(blocks): auto_curves = [] for curve in block["curves"]: - col = curve["color"].strip() + col = curve.get("color", "").strip() c = ColorMap.to_code(col) if c < 0: valid = ColorMap.check_hex(col) diff --git a/config/generic.ini b/config/generic.ini index d9a4e87..15e7f7f 100644 --- a/config/generic.ini +++ b/config/generic.ini @@ -20,4 +20,15 @@ T_sorb.target=- T_still=unit:K,color:orange dil=- lev=unit:%,color:brown -lev.n2=*,unit:%,color:black +lev.n2=unit:%,color:black +hefill=- +ln2fill=- +hepump=- +hemot=- +nv=- +nv.flow=unit:ln/min +nv.flowtarget=unit:ln/min +nv.flowp=unit:ln/min +stickrot=unit:deg +tcoil1=*_coil,unit:K +tcoil2=*_coil,unit:K \ No newline at end of file diff --git a/config/influx.ini b/config/influx.ini index 638a404..c60d100 100644 --- a/config/influx.ini +++ b/config/influx.ini @@ -1,4 +1,5 @@ [INFLUX] -url=http://localhost:8086 +url=http://linse-a:8086 org=linse +bucket=curve-test token=zqDbTcMv9UizfdTj15Fx_6vBetkM5mXN56EE9CiDaFsh7O2FFWZ2X4VwAAmdyqZr3HbpIr5ixRju07-oQmxpXw== \ No newline at end of file diff --git a/influxdb.py b/influxdb.py index 47809b0..eedd4ef 100644 --- a/influxdb.py +++ b/influxdb.py @@ -17,6 +17,7 @@ class InfluxDB: def __init__(self): config = ConfigParser() + config.optionxform = str config.read("./config/influx.ini") self._client = InfluxDBClient(url=config["INFLUX"]["url"], token=config["INFLUX"]["token"], org=config["INFLUX"]["org"]) @@ -83,35 +84,6 @@ class InfluxDataGetter: self._db = db # ----- PUBLIC METHODS - - def get_available_variables_at_time(self, time, chart_configs = None, user_config = None): - """ - Gets the available variables (those that we can have a value for since the device has been installed on the instrument) at the given point in time. - Here, a variable means : SECOP module name + parameter. By default, this method returns the parameters "value" and "target", unless the config files used in chart_configs or user_config indicates other directives. - - Parameters : - time (int) : the unix timestamps in seconds of the point in time to get the variables at. - chart_configs ([ChartConfig] | None) : an array of objects, each holding a configuration file for the chart. Configurations are applied in the order of the list. - user_config ({(str):{"cat":(str), "color":(str), "unit":(str)}} | None) : the Python dict representing the user configuration, applied at the end. The key is . - - Returns : - [{"tag":(str), "unit":(str), "curves":[{"name":(str), "label":(str), "color":(str)}]}] : a list of dictionnaries, each one representing - a block of curves with their name, their label and their color to display, grouped by their category if given or unit (in tag). - """ - - all_setup_info = self._get_all_setup_info_as_dict(time) - - available_variables = self._extract_variables(all_setup_info) - if not chart_configs == None: - for chart_config in chart_configs: - available_variables = self._filter_params_with_config(available_variables, chart_config) - if not user_config == None: - available_variables = self._filter_params_with_user_config(available_variables, user_config) - available_variables = self._remove_variables_params_not_displayed(available_variables) - available_variables = self._remove_variables_params_wihout_param_float_and_split(available_variables, time) - res = self._group_variables_by_cat_unit(available_variables) - - return res def get_curves_in_timerange(self, variables, time, interval = None): """ @@ -199,18 +171,17 @@ class InfluxDataGetter: parameter = "value" if len(var_param) == 1 else var_param[1] # we need to rename the "_time" column to simply "time" in case we want binning because of the comparison done later in the "binned points with same timestamp" process. - # chr(34) is the double quote char, because we cannot escape them in a f string query = f""" from(bucket: "{self._bucket}") |> range(start: {times[0]}, stop: {times[1] + 1}) |> filter(fn : (r) => r._measurement == "{variable_name_for_query}") |> filter(fn : (r) => r._field == "{parameter+"_float"}") - {"|> aggregateWindow(every: duration(v: "+ str(self._seconds_to_nanoseconds(interval))+"), fn: last, createEmpty:false, timeDst:"+chr(34)+"binning_time"+chr(34)+")" if interval != "None" else ""} + {f'|> aggregateWindow(every: duration(v: {self._seconds_to_nanoseconds(interval)}), fn: last, createEmpty:false, timeDst:"binning_time")' if interval != 'None' else ''} |> map(fn: (r) => ({{r with relative: ( float(v: uint(v: {"r.binning_time" if interval != "None" else "r._time"}) - uint(v:{self._seconds_to_nanoseconds(times[0])})) / 1000000000.0 )}})) |> map(fn: (r) => ({{r with timestamp: float(v: uint(v: {"r.binning_time" if interval != "None" else "r._time"})) / 1000000000.0}})) - {"|> rename(columns: {_time:"+chr(34)+"time"+chr(34)+"})" if interval != "None" else ""} + {'|> rename(columns: {_time:"time"})' if interval != 'None' else ''} |> drop(columns:["_start", "_stop", "_field"]) - |> pivot(rowKey:["relative", "timestamp", "expired"{", "+chr(34)+"time"+chr(34) if interval != "None" else ""}], columnKey: ["_measurement"], valueColumn: "_value") + |> pivot(rowKey:["relative", "timestamp", "expired"{', "time"' if interval != "None" else ''}], columnKey: ["_measurement"], valueColumn: "_value") """ data_frame = self._db.query_data_frame(query) @@ -322,75 +293,6 @@ class InfluxDataGetter: # ----- PRIVATE METHODS - def _get_all_setup_info_as_dict(self, time): - """ - Gets the value of the field setup_info in the measurements nicos/se_main, nicos/se_stick, nicos/se_addons as an array of Python dicts. - Takes the last setup_info dict (for each measurement) known at time. - - Parameters - time (int) : the unix timestamps in seconds of the point in time to get the variables at. - - Returns : - [{(str):((str), {...})}]: an array of the parsed "setup_info dict" of each measurements. The key is the secop_module prefixed with "se_", and the value is a tuple with its first value - being the type of Secop device for this module, and the value is too big to give its signature. Some tuple examples can be found under graphs/setup_info_examples. - - """ - measurements = ["nicos/se_main", "nicos/se_stick", "nicos/se_addons"] - res = [] - for measurement in measurements: - query = f""" - from(bucket: "{self._bucket}") - |> range(start: 0, stop: {time + 1}) - |> filter(fn: (r) => r._measurement == "{measurement}") - |> filter(fn: (r) => r._field == "setup_info") - |> last() - |> yield(name: "res") - """ - tables = self._db.query(query) - for table in tables: - for record in table.records: - res.append(ast.literal_eval(record.get_value())) - return res - - def _extract_variables(self, all_setup_info_dict): - """ - Extracts relevant information out of the setup_info dict for each available variable in measurements nicos/se_main, nicos/se_stick, nicos/se_addons - - Parameters : - all_setup_info_dict ([{(str):((str), {...})}]) : an array of the parsed "setup_info dict" of each measurements. The key is the secop_module prefixed with "se_", and the value is a tuple with its first value - being the type of Secop device for this module, and the value is too big to give its signature. Some tuple examples can be found under graphs/setup_info_examples. - - Returns : - [{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}] : an array of dictionnaries, each containing the Influx name of the corresponding variable out of the setup_info dict, - the label to display in the Web GUI, and a dictionnary of parameters (including value), which consist of dictionnares with the category ("*" for value and target, "-" else), the color (empty for the moment) - and the unit ("1" if not available or empty), indexed by the name of the parameter. - - """ - available_varirables = [] - added_names = [] - for setup_info_dict in all_setup_info_dict: - for (_, content) in setup_info_dict.items(): - if content[0] != "nicos.devices.secop.devices.SecopDevice": - name = self._transform_secop_module_name_to_influx(content[1]["secop_module"]) - if name not in added_names: - value_unit = "1" if (not "unit" in content[1].keys() or content[1]["unit"] == "") else content[1]["unit"] - variable = { - "name":name, - "label":content[1]["secop_module"], - "params":{"value":{"cat":"*", "color":"", "unit":value_unit}} # main value, shown by default - } - - for param_name, param_content in content[1]["params_cfg"].items(): - param_unit = "1" if (not "unit" in param_content.keys() or param_content["unit"] == "") else param_content["unit"] - variable["params"][param_name] = { - "cat":"*" if param_name == "target" else "-", # target is also shown by default, not the other parameters - "color":"", - "unit":param_unit - } - available_varirables.append(variable) - added_names.append(name) - return available_varirables - def _transform_secop_module_name_to_influx(self, secop_module_name): """ Transforms the name of the variable available in the setup_info dict into the Influx name. diff --git a/influxgraph.py b/influxgraph.py index 5e8f4c9..7ad9a10 100644 --- a/influxgraph.py +++ b/influxgraph.py @@ -1,19 +1,23 @@ -import time +from time import time as current_time import logging import json import io import uuid -from influxdb import InfluxDB, InfluxDataGetter +from configparser import ConfigParser +from math import ceil +from sehistory.influx import InfluxDBWrapper from colors import assign_colors_to_curves from chart_config import ChartConfig from base import Instrument, get_abs_time from secop import SecopClient, SecopInstrument +def split_tags(tags): + return {k: v.split(',') for k, v in tags.items()} + + class InfluxGraph: - """ - Class implementing the logic of the different routes that are called by - the client to retrieve graph data with InfluxDB. + """Class implementing the logic of the different routes that are called by the client to retrieve graph data with InfluxDB. Global constants : HISTORICAL (int) : value that represents the "historical" visualization mode, meaning that the @@ -29,96 +33,99 @@ class InfluxGraph: livemode (int) : the type of visualization the user is currently in. Can be HISTORICAL, ACTUAL or LIVE. end_query (int) : the unix timestamp in seconds of the most recent requested point in time of the last query or update. - lastvalues ({(str):((int), (float))}) : a dictionnary where the keys are the variable names, and the values + last_values ({(str):((int), (float))}) : a dictionnary where the keys are the variable names, and the values are tuples, where the first value is the unix timestamp of the most recent value known for this variable, and the second value its corresponding value - variables ({(str):(str)}) : a dictionnary of the current available variables requested by the client. + variables ({(str):(str)}) : a dictionary of the current available variables requested by the client. The key is the InfluxDB name of the curve, and the value is its label in the GUI. """ HISTORICAL = 0 ACTUAL = 1 LIVE = 2 - def __init__(self, influx_data_getter, instrument): - self.influx_data_getter = influx_data_getter - self.chart_configs = [ChartConfig("./config/generic.ini"), ChartConfig(f"./config/{instrument}.ini")] + def __init__(self, instrument): + self.instrument = instrument + self.db = instrument.db + instrument_name = instrument.title + # self.influx_data_getter = influx_data_getter + self.chart_configs = [ChartConfig("./config/generic.ini")] + try: + self.chart_configs.append(ChartConfig(f"./config/{instrument_name}.ini")) + except KeyError: + pass self.livemode = self.HISTORICAL - self.end_query = 0 - self.lastvalues = {} - self.variables = {} # name:label - - def complete_to_end_and_feed_lastvalues(self, result, endtime): - """ - Completes the data until the last requested point in time by adding the last known y-value at the end point. - Also feeds self.lastvalues. - - Parameters : - result ({(str):[[(int),(float)]]}) : a dictionnary with the variable names as key, and an array of points, - which are a tuple (timestamp, y-value as float) - endtime (int) : the unix timestamp in seconds of the time we want to have data until - """ - for var, c in result.items(): - if c: - lastt, lastx = c[-1] - if lastt < endtime: - c.append((endtime, lastx)) - self.lastvalues[var] = (lastt, lastx) + self.last_values = {} # dict of last known point (