major rework
using influxdb structure independed of nicos cache
This commit is contained in:
8
base.py
8
base.py
@ -6,9 +6,11 @@ ONEYEAR = 366 * 24 * 3600
|
|||||||
|
|
||||||
|
|
||||||
def get_abs_time(times):
|
def get_abs_time(times):
|
||||||
"""
|
"""Gets the absolute times for the given potential relative 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
|
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 :
|
Parameters :
|
||||||
times([(float)]) : an array of unix timestamps or relative duration (< 1 year) as floats
|
times([(float)]) : an array of unix timestamps or relative duration (< 1 year) as floats
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
from configparser import ConfigParser
|
from configparser import ConfigParser
|
||||||
|
|
||||||
|
|
||||||
class ChartConfig:
|
class ChartConfig:
|
||||||
"""
|
"""
|
||||||
Class that holds the chart section of a configuration file (for an instrument).
|
Class that holds the chart section of a configuration file (for an instrument).
|
||||||
@ -6,14 +8,42 @@ class ChartConfig:
|
|||||||
Attributes :
|
Attributes :
|
||||||
chart_config (Section) : the Section corresponding to the "chart" section in the given configuration file
|
chart_config (Section) : the Section corresponding to the "chart" section in the given configuration file
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
KEYS = ["cat", "color", "unit", "label"]
|
||||||
|
|
||||||
def __init__(self, path):
|
def __init__(self, path):
|
||||||
"""
|
"""
|
||||||
Parameters :
|
Parameters :
|
||||||
path (str) : the path to the configuration file
|
path (str) : the path to the configuration file
|
||||||
"""
|
"""
|
||||||
|
self.errors = {}
|
||||||
|
self.variables = {}
|
||||||
cfgp = ConfigParser(interpolation=None)
|
cfgp = ConfigParser(interpolation=None)
|
||||||
|
cfgp.optionxform = str
|
||||||
cfgp.read(path)
|
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):
|
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,
|
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.
|
or if there is a syntax problem for the given key.
|
||||||
"""
|
"""
|
||||||
config = {}
|
return self.variables.get(key)
|
||||||
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
|
|
||||||
|
@ -111,8 +111,8 @@ function handleUpdateMessage(src, message) {
|
|||||||
instrument.style.width = 'auto'
|
instrument.style.width = 'auto'
|
||||||
device.style.width = 'auto'
|
device.style.width = 'auto'
|
||||||
instrument.innerHTML = message.instrument
|
instrument.innerHTML = message.instrument
|
||||||
device.innerHTML = message.device
|
// device.innerHTML = message.device
|
||||||
// console.log('ID', initCommands);
|
console.log('ID', initCommands);
|
||||||
nextInitCommand();
|
nextInitCommand();
|
||||||
break;
|
break;
|
||||||
// console-update-message: Confirms a command.
|
// console-update-message: Confirms a command.
|
||||||
@ -306,7 +306,7 @@ function reqJSONPOST(s, url, parameters, successHandler, errorHandler) {
|
|||||||
var xhr = typeof XMLHttpRequest != 'undefined' ? new XMLHttpRequest()
|
var xhr = typeof XMLHttpRequest != 'undefined' ? new XMLHttpRequest()
|
||||||
: new ActiveXObject('Microsoft.XMLHTTP');
|
: new ActiveXObject('Microsoft.XMLHTTP');
|
||||||
if (debugCommunication) {
|
if (debugCommunication) {
|
||||||
console.log("%cto server (reqJSON): %s",
|
console.log("%cto server (reqJSONPOST): %s",
|
||||||
"color:white;background:lightgreen", url);
|
"color:white;background:lightgreen", url);
|
||||||
}
|
}
|
||||||
xhr.open('post', url, true);
|
xhr.open('post', url, true);
|
||||||
@ -406,8 +406,11 @@ function successHandler(s, message) {
|
|||||||
begin = timeRange[0] - timeRange[1];
|
begin = timeRange[0] - timeRange[1];
|
||||||
select.value = begin;
|
select.value = begin;
|
||||||
// Server-request for variable-list.*/
|
// Server-request for variable-list.*/
|
||||||
reqJSONPOST(0, "http://" + hostPort + "/getvars", "time=" + timeRange[1] + "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage()) + "&id="
|
reqJSONPOST(0, "http://" + hostPort + "/getvars",
|
||||||
+ clientID, successHandler, errorHandler);
|
"time=" + timeRange[1]
|
||||||
|
+ window['clientTags']
|
||||||
|
+ "&userconfiguration=" + JSON.stringify(getFormattedUserConfigurationFromLocalStorage())
|
||||||
|
+ "&id=" + clientID, successHandler, errorHandler);
|
||||||
break;
|
break;
|
||||||
// Response to a "getvars"-server-request.
|
// Response to a "getvars"-server-request.
|
||||||
case "var_list":
|
case "var_list":
|
||||||
@ -427,6 +430,7 @@ function successHandler(s, message) {
|
|||||||
nextInitCommand();
|
nextInitCommand();
|
||||||
}*/
|
}*/
|
||||||
// graphs.receivedVars(message.blocks);
|
// graphs.receivedVars(message.blocks);
|
||||||
|
document.getElementById("device").innerHTML = message.device
|
||||||
graphs.initGraphs(message.blocks);
|
graphs.initGraphs(message.blocks);
|
||||||
nextInitCommand();
|
nextInitCommand();
|
||||||
break;
|
break;
|
||||||
|
@ -371,10 +371,9 @@ function loadExportPopup(){
|
|||||||
*/
|
*/
|
||||||
function exportCallback(selectedVariables, startDateTimeMs, endDateTimeMs, nan, binning=null){
|
function exportCallback(selectedVariables, startDateTimeMs, endDateTimeMs, nan, binning=null){
|
||||||
|
|
||||||
let binningParam = "None";
|
if (binning === null || binning == "None")
|
||||||
if (binning !== null)
|
binning = "";
|
||||||
binningParam = binning
|
let exportURL = "http://" + hostPort + "/export?time=" + startDateTimeMs/1000 + "," + endDateTimeMs/1000 + "&variables=" + selectedVariables + "&nan=" + nan + "&interval=" + binning + "&id=" + clientID
|
||||||
let exportURL = "http://" + hostPort + "/export?time=" + startDateTimeMs/1000 + "," + endDateTimeMs/1000 + "&variables=" + selectedVariables + "&nan=" + nan + "&interval=" + binningParam + "&id=" + clientID
|
|
||||||
let a = document.createElement('a');
|
let a = document.createElement('a');
|
||||||
a.href = exportURL
|
a.href = exportURL
|
||||||
a.download = true
|
a.download = true
|
||||||
@ -417,7 +416,7 @@ let graphs = (function (){
|
|||||||
let minTime, maxTime; // the queried time range
|
let minTime, maxTime; // the queried time range
|
||||||
let lastTime = 0; // time of most recent data point
|
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 activateUpdateTimeout = undefined; // timeout for the activateUpdates function
|
||||||
let updateAutoTimeout = undefined; // timeout for the updateAuto function (used in onZoomCompleteCallback)
|
let updateAutoTimeout = undefined; // timeout for the updateAuto function (used in onZoomCompleteCallback)
|
||||||
@ -542,11 +541,12 @@ let graphs = (function (){
|
|||||||
varlist = vars_array[gindex];
|
varlist = vars_array[gindex];
|
||||||
let graph_elm = graph_elm_array[gindex];
|
let graph_elm = graph_elm_array[gindex];
|
||||||
|
|
||||||
timeDeltaAxis = maxTime - minTime
|
resolution = getResolution((maxTime - minTime) / 1000)
|
||||||
setResolution(timeDeltaAxis)
|
|
||||||
|
|
||||||
AJAX("http://" + hostPort + "/graph?time=" + minTime/1000 + "," + maxTime/1000 + "&variables=" + varlist + "&interval=" + resolution + "&id=" + clientID).getJSON().then(function(data){
|
|
||||||
|
|
||||||
|
AJAX("http://" + hostPort + "/graph?time=" + minTime/1000 + "," + maxTime/1000
|
||||||
|
+ "&variables=" + varlist
|
||||||
|
+ "&interval=" + resolution
|
||||||
|
+ "&id=" + clientID).getJSON().then(function(data){
|
||||||
//console.log('Graph', block, data)
|
//console.log('Graph', block, data)
|
||||||
let graph = new Graph(gindex, graph_elm, "Time", block.unit, block.tag, type);
|
let graph = new Graph(gindex, graph_elm, "Time", block.unit, block.tag, type);
|
||||||
graph_array[gindex] = graph;
|
graph_array[gindex] = graph;
|
||||||
@ -777,10 +777,12 @@ let graphs = (function (){
|
|||||||
max = max/1000;
|
max = max/1000;
|
||||||
}
|
}
|
||||||
|
|
||||||
timeDelta = currentMaxTime - currentMinTime
|
resolution = getResolution((currentMaxTime - currentMinTime) / 1000)
|
||||||
setResolution(timeDelta)
|
|
||||||
|
|
||||||
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){
|
for(let key in data.graph){
|
||||||
let pdata = [];
|
let pdata = [];
|
||||||
for(let e of data.graph[key]){
|
for(let e of data.graph[key]){
|
||||||
@ -893,10 +895,10 @@ let graphs = (function (){
|
|||||||
* Sets the resolution of the viewing window in milliseconds
|
* Sets the resolution of the viewing window in milliseconds
|
||||||
* @param {*} timeDelta - The difference between the maximum time and the minimum time of the window
|
* @param {*} timeDelta - The difference between the maximum time and the minimum time of the window
|
||||||
*/
|
*/
|
||||||
function setResolution(timeDelta){
|
function getResolution(timeDelta){
|
||||||
resolution = Math.ceil((timeDelta / container.getBoundingClientRect().width))
|
return Math.ceil((timeDelta / container.getBoundingClientRect().width))
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The callback to be called when the user click on the "Jump" button of the date selector
|
* 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
|
* Gets the vars + device name for the selected date+time, then rebuilds the graphs
|
||||||
@ -915,7 +917,11 @@ let graphs = (function (){
|
|||||||
msRightTimestampGetVars = dateTimestampMs + timeValueMs;
|
msRightTimestampGetVars = dateTimestampMs + timeValueMs;
|
||||||
msRightTimestampGetGraph = dateTimestampMs + 24*60*60*1000;
|
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;
|
blocks = data.blocks;
|
||||||
document.getElementById("device").innerHTML = data.device
|
document.getElementById("device").innerHTML = data.device
|
||||||
maxTime = msRightTimestampGetGraph;
|
maxTime = msRightTimestampGetGraph;
|
||||||
@ -989,7 +995,11 @@ let graphs = (function (){
|
|||||||
window["wideGraphs"] = false; // will have no effect if hideRightPart is true
|
window["wideGraphs"] = false; // will have no effect if hideRightPart is true
|
||||||
adjustGrid();
|
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;
|
currentMaxTime = msRightTimestamp + 60000;
|
||||||
currentMinTime = msLeftTimestamp;
|
currentMinTime = msLeftTimestamp;
|
||||||
|
|
||||||
@ -1291,7 +1301,11 @@ let graphs = (function (){
|
|||||||
function applySettingsCallback(userConfiguration){
|
function applySettingsCallback(userConfiguration){
|
||||||
cursorLine(null);
|
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;
|
blocks = data.blocks;
|
||||||
document.getElementById("device").innerHTML = data.device
|
document.getElementById("device").innerHTML = data.device
|
||||||
maxTime = currentMaxTime;
|
maxTime = currentMaxTime;
|
||||||
|
@ -91,6 +91,17 @@ new Settings()
|
|||||||
.treat("hideRightPart", "hr", to_bool, false) //used to completely disable the right part
|
.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("wideGraphs", "wg", to_bool, false) //used to toggle the size of the graphs part
|
||||||
.treat("showAsync", "sa", to_bool, false)
|
.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() {
|
function loadFirstBlocks() {
|
||||||
if (debug_main_daniel) {
|
if (debug_main_daniel) {
|
||||||
@ -196,9 +207,9 @@ window.onload = function() {
|
|||||||
// var homeButton = document.getElementById("home-icon");
|
// 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.
|
// TODO : uncomment this code with the right URL to navigate to when the way to select the instrument will be decided.
|
||||||
// homeButton.onclick = function () {
|
homeButton.onclick = function () {
|
||||||
// window.location = "http://" + location.hostname + ":8800/";
|
window.location = "/select";
|
||||||
// };
|
};
|
||||||
buildUpdateConnection();
|
buildUpdateConnection();
|
||||||
// if (location.hash) {
|
// if (location.hash) {
|
||||||
// console.log("hash in url", location.hash);
|
// console.log("hash in url", location.hash);
|
||||||
|
@ -18,7 +18,7 @@ def assign_colors_to_curves(blocks):
|
|||||||
auto_curves = []
|
auto_curves = []
|
||||||
for curve in block["curves"]:
|
for curve in block["curves"]:
|
||||||
|
|
||||||
col = curve["color"].strip()
|
col = curve.get("color", "").strip()
|
||||||
c = ColorMap.to_code(col)
|
c = ColorMap.to_code(col)
|
||||||
if c < 0:
|
if c < 0:
|
||||||
valid = ColorMap.check_hex(col)
|
valid = ColorMap.check_hex(col)
|
||||||
|
@ -20,4 +20,15 @@ T_sorb.target=-
|
|||||||
T_still=unit:K,color:orange
|
T_still=unit:K,color:orange
|
||||||
dil=-
|
dil=-
|
||||||
lev=unit:%,color:brown
|
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
|
@ -1,4 +1,5 @@
|
|||||||
[INFLUX]
|
[INFLUX]
|
||||||
url=http://localhost:8086
|
url=http://linse-a:8086
|
||||||
org=linse
|
org=linse
|
||||||
|
bucket=curve-test
|
||||||
token=zqDbTcMv9UizfdTj15Fx_6vBetkM5mXN56EE9CiDaFsh7O2FFWZ2X4VwAAmdyqZr3HbpIr5ixRju07-oQmxpXw==
|
token=zqDbTcMv9UizfdTj15Fx_6vBetkM5mXN56EE9CiDaFsh7O2FFWZ2X4VwAAmdyqZr3HbpIr5ixRju07-oQmxpXw==
|
106
influxdb.py
106
influxdb.py
@ -17,6 +17,7 @@ class InfluxDB:
|
|||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
config = ConfigParser()
|
config = ConfigParser()
|
||||||
|
config.optionxform = str
|
||||||
config.read("./config/influx.ini")
|
config.read("./config/influx.ini")
|
||||||
self._client = InfluxDBClient(url=config["INFLUX"]["url"], token=config["INFLUX"]["token"],
|
self._client = InfluxDBClient(url=config["INFLUX"]["url"], token=config["INFLUX"]["token"],
|
||||||
org=config["INFLUX"]["org"])
|
org=config["INFLUX"]["org"])
|
||||||
@ -83,35 +84,6 @@ class InfluxDataGetter:
|
|||||||
self._db = db
|
self._db = db
|
||||||
|
|
||||||
# ----- PUBLIC METHODS
|
# ----- 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 <secop_module.parameter>.
|
|
||||||
|
|
||||||
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):
|
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]
|
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.
|
# 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"""
|
query = f"""
|
||||||
from(bucket: "{self._bucket}")
|
from(bucket: "{self._bucket}")
|
||||||
|> range(start: {times[0]}, stop: {times[1] + 1})
|
|> range(start: {times[0]}, stop: {times[1] + 1})
|
||||||
|> filter(fn : (r) => r._measurement == "{variable_name_for_query}")
|
|> filter(fn : (r) => r._measurement == "{variable_name_for_query}")
|
||||||
|> filter(fn : (r) => r._field == "{parameter+"_float"}")
|
|> 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 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}}))
|
|> 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"])
|
|> 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)
|
data_frame = self._db.query_data_frame(query)
|
||||||
|
|
||||||
@ -322,75 +293,6 @@ class InfluxDataGetter:
|
|||||||
|
|
||||||
# ----- PRIVATE METHODS
|
# ----- 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):
|
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.
|
Transforms the name of the variable available in the setup_info dict into the Influx name.
|
||||||
|
348
influxgraph.py
348
influxgraph.py
@ -1,19 +1,23 @@
|
|||||||
import time
|
from time import time as current_time
|
||||||
import logging
|
import logging
|
||||||
import json
|
import json
|
||||||
import io
|
import io
|
||||||
import uuid
|
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 colors import assign_colors_to_curves
|
||||||
from chart_config import ChartConfig
|
from chart_config import ChartConfig
|
||||||
from base import Instrument, get_abs_time
|
from base import Instrument, get_abs_time
|
||||||
from secop import SecopClient, SecopInstrument
|
from secop import SecopClient, SecopInstrument
|
||||||
|
|
||||||
|
|
||||||
|
def split_tags(tags):
|
||||||
|
return {k: v.split(',') for k, v in tags.items()}
|
||||||
|
|
||||||
|
|
||||||
class InfluxGraph:
|
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 :
|
Global constants :
|
||||||
HISTORICAL (int) : value that represents the "historical" visualization mode, meaning that the
|
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.
|
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
|
end_query (int) : the unix timestamp in seconds of the most recent requested point in time of the last query
|
||||||
or update.
|
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,
|
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
|
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.
|
The key is the InfluxDB name of the curve, and the value is its label in the GUI.
|
||||||
"""
|
"""
|
||||||
HISTORICAL = 0
|
HISTORICAL = 0
|
||||||
ACTUAL = 1
|
ACTUAL = 1
|
||||||
LIVE = 2
|
LIVE = 2
|
||||||
|
|
||||||
def __init__(self, influx_data_getter, instrument):
|
def __init__(self, instrument):
|
||||||
self.influx_data_getter = influx_data_getter
|
self.instrument = instrument
|
||||||
self.chart_configs = [ChartConfig("./config/generic.ini"), ChartConfig(f"./config/{instrument}.ini")]
|
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.livemode = self.HISTORICAL
|
||||||
self.end_query = 0
|
self.last_values = {} # dict <variable> of last known point (<time>, <value>)
|
||||||
self.lastvalues = {}
|
self.last_time = {} # dict <stream> of last received time
|
||||||
self.variables = {} # name:label
|
self.last_minute = 0
|
||||||
|
self.last_update = 0 # time of last call with a result
|
||||||
def complete_to_end_and_feed_lastvalues(self, result, endtime):
|
self.tags = {} # tags for query (determines device and/or server)
|
||||||
"""
|
|
||||||
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)
|
|
||||||
|
|
||||||
def w_graph(self, variables, time="-1800,0", interval=None):
|
def w_graph(self, variables, time="-1800,0", interval=None):
|
||||||
"""
|
"""Get the curves given by variables in the time range "time"
|
||||||
Gets the curves given by variables in the time range "time", spaced by "interval" if given (binning/resolution)
|
|
||||||
|
spaced by "interval" if given (binning/resolution)
|
||||||
Called when the route /graph is reached.
|
Called when the route /graph is reached.
|
||||||
|
|
||||||
Parameters :
|
Parameters :
|
||||||
variables (str) : a comma separataed value string of variable names (influx names) to retrieve
|
variables (str) : a comma separated string of variable names (influx names) to retrieve
|
||||||
time (str) : a commma separated value string (range) of seconds. They are treated as relative from now
|
time (str) : a commma separated value string (range) of seconds.
|
||||||
if they are lesser than one year.
|
values < one year are treated as relative from now.
|
||||||
interval (str) : the interval (resolution) of the values to get (string in milliseconds)
|
interval (str) : the interval (resolution) of the values to get (string in seconds)
|
||||||
|
|
||||||
Returns :
|
Returns :
|
||||||
{"type":"graph-draw", "graph":{(str):[[(int),(float)]]}} : a dictionnary with its "graph-draw" type
|
{"type":"graph-draw", "graph":{(str):[[(int),(float)]]}} : a dictionary with its "graph-draw" type
|
||||||
(so it can be processed by the client), and a "graph" dictionnary with the variable names as key,
|
(so it can be processed by the client), and a "graph" dictionary with the variable names as key,
|
||||||
and an array of points as a tuple (timestamp, y-value as float)
|
and an array of points as a tuple (timestamp, y-value as float)
|
||||||
"""
|
"""
|
||||||
time = [float(t) for t in time.split(',')]
|
start, end, now = get_abs_time([float(t) for t in time.split(',')] + [0])
|
||||||
start, end, now = get_abs_time(time + [0])
|
start, end, now = int(start), ceil(end), ceil(now)
|
||||||
start, end, now = int(start), int(end), int(now)
|
|
||||||
queried_time_range = [start, end]
|
|
||||||
queried_variables = variables.split(',')
|
queried_variables = variables.split(',')
|
||||||
self.livemode = self.ACTUAL if end+10 >= now else self.HISTORICAL
|
self.livemode = self.ACTUAL if end+10 >= now else self.HISTORICAL
|
||||||
logging.info('LIVE %g %g %d %d', end, now, end >= now, self.livemode)
|
logging.info('LIVE %g %g %d %d', end, now, end >= now, self.livemode)
|
||||||
if interval : interval = int(interval)
|
if interval:
|
||||||
|
interval = float(interval)
|
||||||
|
result = self.db.curves(start, end, queried_variables, merge='_measurement',
|
||||||
|
interval=interval or None, **self.tags)
|
||||||
|
self.update_last(result)
|
||||||
|
self.db.complete(result, self.last_time, 'stream')
|
||||||
|
self.last_minute = now // 60
|
||||||
|
return dict(type='graph-draw', graph={k: v for k, v in result.items()})
|
||||||
|
|
||||||
result = self.influx_data_getter.get_curves_in_timerange(queried_variables, queried_time_range, interval)
|
def update_last(self, curve_dict):
|
||||||
self.complete_to_end_and_feed_lastvalues(result, min(end, now))
|
"""update last values per variable and last time per stream"""
|
||||||
self.end_query = end
|
for key, curve in curve_dict.items():
|
||||||
|
stream = curve.tags.get('stream')
|
||||||
return dict(type='graph-draw', graph=result)
|
tlast, value = curve[-1]
|
||||||
|
self.last_values[key] = curve[-1]
|
||||||
|
self.last_time[stream] = max(self.last_time.get(stream, 0), tlast)
|
||||||
|
|
||||||
def w_gettime(self, time):
|
def w_gettime(self, time):
|
||||||
"""
|
"""Get the server time for the given time(range).
|
||||||
Gets the server time for the give time.
|
|
||||||
Called when the route /gettime is reached.
|
Called when the route /gettime is reached.
|
||||||
|
|
||||||
Parameters :
|
Parameters :
|
||||||
time (str="-1800,0") : the given point in time represented by a string, which is a comma separated unix
|
time (str="-1800,0") : the given point in time represented by a string,
|
||||||
timestamp values list (in seconds). They are treated as relative from now if they are lesser than one year.
|
which is a comma separated unix timestamp values list (in seconds).
|
||||||
|
values < one year are treated as relative from now.
|
||||||
|
|
||||||
Returns :
|
Returns :
|
||||||
{"type":"time", "time":(int)} : a dictionnary with its "time" type (so the data can be processed by the
|
{"type":"time", "time":(int)} : a dictionary with its "time" type (so the data can be processed by the
|
||||||
client) and the server unix timestamp in seconds corresponding to the time asked by the client
|
client) and the server unix timestamp in seconds corresponding to the time asked by the client
|
||||||
"""
|
"""
|
||||||
time = [float(t) for t in time.split(',')]
|
return dict(type='time', time=get_abs_time(
|
||||||
return dict(type='time', time=get_abs_time(time))
|
[float(t) for t in time.split(',')]))
|
||||||
|
|
||||||
def w_getvars(self, time, userconfiguration = None):
|
def w_getvars(self, time, userconfiguration=None, instrument=None, **tags):
|
||||||
"""
|
"""Get the curve names available at a given point in time
|
||||||
Gets the curve names available at a given point in time, with a possible user configuration on the client side.
|
|
||||||
|
with a possible user configuration on the client side.
|
||||||
Called when the route /getvars is reached.
|
Called when the route /getvars is reached.
|
||||||
|
|
||||||
Parameters :
|
Parameters :
|
||||||
time (str) : the given point in time represented by a string, which is a unix timestamp in seconds.
|
time (str) : the given point in time represented by a string, which is a unix timestamp in seconds.
|
||||||
It is treated as relative from now if it is lesser than one year.
|
values < one year are treated as relative from now.
|
||||||
|
Might also be a comma separated time range.
|
||||||
userconfiguration (str|None) : the JSON string representing the user configuration
|
userconfiguration (str|None) : the JSON string representing the user configuration
|
||||||
|
|
||||||
Returns :
|
Returns :
|
||||||
@ -130,25 +137,118 @@ class InfluxGraph:
|
|||||||
category or unit if absent) and their unit (in "blocks")
|
category or unit if absent) and their unit (in "blocks")
|
||||||
"""
|
"""
|
||||||
|
|
||||||
time = [float(t) for t in time.split(',')]
|
time = get_abs_time([float(t) for t in time.split(',')])
|
||||||
end_time = int(get_abs_time(time)[-1])
|
start_time = int(time[0])
|
||||||
|
end_time = int(time[-1])
|
||||||
if not userconfiguration == None : userconfiguration = json.loads(userconfiguration)
|
if userconfiguration is not None:
|
||||||
|
userconfiguration = json.loads(userconfiguration)
|
||||||
blocks = self.influx_data_getter.get_available_variables_at_time(end_time, self.chart_configs, userconfiguration)
|
|
||||||
device_name = self.influx_data_getter.get_device_name(end_time)
|
|
||||||
# updates the self.variables attribute to keep track of the available variables
|
|
||||||
self.variables = {variable["name"]:variable["label"] for block in blocks for variable in block["curves"]}
|
|
||||||
|
|
||||||
|
self.tags = split_tags(tags)
|
||||||
|
if instrument:
|
||||||
|
tags['stream'] = list(self.db.get_streams(instrument))
|
||||||
|
print('GETAV', self.tags)
|
||||||
|
blocks = self.get_available_variables(start_time, end_time, self.chart_configs, userconfiguration)
|
||||||
|
device_name = tags.get('device', '<unknown>')
|
||||||
|
# initialize self.last_values to keep track of the available variables
|
||||||
|
self.last_values = {var["name"]: [0, None] for block in blocks for var in block["curves"]}
|
||||||
assign_colors_to_curves(blocks)
|
assign_colors_to_curves(blocks)
|
||||||
result = dict(type='var_list')
|
result = dict(type='var_list')
|
||||||
result['blocks'] = blocks
|
result['blocks'] = blocks
|
||||||
result['device'] = device_name
|
result['device'] = device_name
|
||||||
|
print('DEVICE', device_name, tags)
|
||||||
|
for block in blocks:
|
||||||
|
print(block['tag'], [c['name'] for c in block['curves']])
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def w_updategraph(self):
|
def get_available_variables(self, start_time, end_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 :
|
||||||
|
start_time, send_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 <secop_module.parameter>.
|
||||||
|
|
||||||
|
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).
|
||||||
"""
|
"""
|
||||||
Sets the current visualisation mode to LIVE if not in HISTORICAL mode.
|
if start_time == end_time:
|
||||||
|
start_time = end_time - 3600
|
||||||
|
result = self.db.curves(start_time, end_time, _measurement=None, _field='float', **self.tags)
|
||||||
|
assert all(c.key_names[0] == '_measurement' for c in result.values())
|
||||||
|
variables = {k[0] for k in result}
|
||||||
|
config = {}
|
||||||
|
if chart_configs:
|
||||||
|
for chart_config in chart_configs:
|
||||||
|
for key, cfg in chart_config.variables.items():
|
||||||
|
config.setdefault(key, {}).update(cfg)
|
||||||
|
if user_config:
|
||||||
|
for key, cfg in user_config.items():
|
||||||
|
config.setdefault(key, {}).update(cfg)
|
||||||
|
|
||||||
|
groups = {}
|
||||||
|
|
||||||
|
def add_to_groups(name, cat=None, unit='1', color='', label=None):
|
||||||
|
if cat == '-':
|
||||||
|
return
|
||||||
|
if name.endswith('.value'):
|
||||||
|
if not cat:
|
||||||
|
cat = '*'
|
||||||
|
if not label:
|
||||||
|
label = name[:-6]
|
||||||
|
elif name.endswith('.target'):
|
||||||
|
if not cat:
|
||||||
|
cat = '*'
|
||||||
|
elif not cat:
|
||||||
|
return
|
||||||
|
tag = cat.replace('*', unit)
|
||||||
|
grp = groups.get(tag)
|
||||||
|
if grp is None:
|
||||||
|
curves = []
|
||||||
|
groups[tag] = {'tag': cat.replace('*', unit), 'unit': unit, 'curves': curves}
|
||||||
|
else:
|
||||||
|
curves = grp['curves']
|
||||||
|
curves.append({'name': name, 'unit': unit, 'label': label or name})
|
||||||
|
|
||||||
|
# treat variables in config first (in their order!)
|
||||||
|
result = {}
|
||||||
|
for key, cfg in config.items():
|
||||||
|
cat = cfg.pop('cat', None)
|
||||||
|
unit = cfg.get('unit', '1')
|
||||||
|
if '.' in key:
|
||||||
|
if key in variables:
|
||||||
|
add_to_groups(key, cat, **cfg)
|
||||||
|
variables.discard(key)
|
||||||
|
else:
|
||||||
|
var = f'{key}.value'
|
||||||
|
if var in variables:
|
||||||
|
label = cfg.pop('label', None) or key
|
||||||
|
add_to_groups(var, cat, label=label, **cfg)
|
||||||
|
variables.discard(var)
|
||||||
|
var = f'{key}.target'
|
||||||
|
if var in variables:
|
||||||
|
cfg.pop('color', None)
|
||||||
|
add_to_groups(var, cat, **cfg)
|
||||||
|
variables.discard(var)
|
||||||
|
for var in variables:
|
||||||
|
add_to_groups(var)
|
||||||
|
return list(groups.values())
|
||||||
|
|
||||||
|
def w_updategraph(self):
|
||||||
|
"""Set the current visualisation mode to LIVE if not in HISTORICAL mode.
|
||||||
|
|
||||||
Called when the route /updategraph is reached.
|
Called when the route /updategraph is reached.
|
||||||
Returns :
|
Returns :
|
||||||
{"type":"accept-graph", "live": bool} : a dict with its "accept-graph" type and a "live"
|
{"type":"accept-graph", "live": bool} : a dict with its "accept-graph" type and a "live"
|
||||||
@ -161,7 +261,7 @@ class InfluxGraph:
|
|||||||
self.livemode = self.LIVE
|
self.livemode = self.LIVE
|
||||||
return dict(type='accept-graph', live=True)
|
return dict(type='accept-graph', live=True)
|
||||||
|
|
||||||
def w_export(self, variables, time, nan, interval):
|
def w_export(self, variables, time, nan, interval, timeoffset=None):
|
||||||
"""
|
"""
|
||||||
Returns the bytes of a dataframe with the curves given by variables in the time range "time"
|
Returns the bytes of a dataframe with the curves given by variables in the time range "time"
|
||||||
Called when the route /export is reached.
|
Called when the route /export is reached.
|
||||||
@ -176,19 +276,15 @@ class InfluxGraph:
|
|||||||
io.BytesIO : an BytesIO object containing the dataframe to retrieve
|
io.BytesIO : an BytesIO object containing the dataframe to retrieve
|
||||||
"""
|
"""
|
||||||
|
|
||||||
time = [float(t) for t in time.split(',')]
|
start, end = get_abs_time([float(t) for t in time.split(',')])
|
||||||
start, end = get_abs_time(time)
|
start, end = int(start), ceil(end)
|
||||||
start, end = int(start), int(end)
|
|
||||||
|
|
||||||
queried_variables = variables.split(',')
|
queried_variables = variables.split(',')
|
||||||
if interval != "None" : interval = int(interval)
|
interval = float(interval) if interval else None
|
||||||
|
timeoffset = None if timeoffset == 'now' else (timeoffset or 0)
|
||||||
df = self.influx_data_getter.get_curves_data_frame(queried_variables, [start, end], interval, self.variables)
|
result = self.db.export(start, end, queried_variables, timeoffset=timeoffset, none=nan, interval=interval,
|
||||||
|
**self.tags)
|
||||||
mem = io.BytesIO()
|
return io.BytesIO(result.encode('utf-8'))
|
||||||
df.to_csv(mem, sep="\t", index=False, float_format="%.15g", na_rep=nan)
|
|
||||||
mem.seek(0)
|
|
||||||
return mem
|
|
||||||
|
|
||||||
def graphpoll(self):
|
def graphpoll(self):
|
||||||
"""
|
"""
|
||||||
@ -199,45 +295,65 @@ class InfluxGraph:
|
|||||||
|
|
||||||
Returns :
|
Returns :
|
||||||
{"type":"graph-update", "time":(int), "graph":{(str):[[(int),(float)]]}} | None :
|
{"type":"graph-update", "time":(int), "graph":{(str):[[(int),(float)]]}} | None :
|
||||||
a dictionnary with its "graph-update" type
|
a dictionary with its "graph-update" type
|
||||||
(so it can be processed by the client), and a "graph" dictionnary with the variable names as key,
|
(so it can be processed by the client), and a "graph" dictionary with the variable names as key,
|
||||||
and an array of points, which are an array containing the timestamp
|
and an array of points, which are an array containing the timestamp
|
||||||
as their first value, and the y-value in float as their second one
|
as their first value, and the y-value in float as their second one
|
||||||
"""
|
"""
|
||||||
if self.livemode != self.LIVE:
|
if self.livemode != self.LIVE:
|
||||||
return None
|
return None
|
||||||
now, = get_abs_time([0])
|
now = current_time()
|
||||||
|
if now < int(self.last_update) + 1.5:
|
||||||
result = self.influx_data_getter.poll_last_values(list(self.variables.keys()), self.lastvalues, now)
|
# the server is only waiting after a None return
|
||||||
for variable, values in list(result.items()):
|
# this avoids to many queries with expected empty result
|
||||||
tlast = self.lastvalues.get(variable, (0,))[0]
|
return None
|
||||||
# removes points older than the last known point
|
last_time = int(min(self.last_time.values()))
|
||||||
# (queries are in seconds and might return points already displayed)
|
# if len(self.last_time) > 1:
|
||||||
while values and values[0][0] <= tlast:
|
# print('time_poll_jitter', max(self.last_time.values()) - min(self.last_time.values()))
|
||||||
values.pop(0)
|
prev_minute, self.last_minute = self.last_minute, now // 60
|
||||||
if values and values[-1][0] > tlast:
|
fullminute = prev_minute != self.last_minute
|
||||||
self.lastvalues[variable] = values[-1]
|
add_prev = 3600 if fullminute else 0
|
||||||
|
result = self.db.curves(last_time, None, list(self.last_values),
|
||||||
|
merge='_measurement', add_prev=add_prev, **self.tags)
|
||||||
|
to_remove = {}
|
||||||
|
for key, curve in result.items():
|
||||||
|
tlast = self.last_values.get(key, [0])[0]
|
||||||
|
# remove points older than the last known point. this might happen for different reasons:
|
||||||
|
# - queries are rounded to seconds
|
||||||
|
# - clocks of different streams might not be synched
|
||||||
|
l = len(curve)
|
||||||
|
for i, row in enumerate(curve):
|
||||||
|
if row[0] > tlast:
|
||||||
|
del curve[:i]
|
||||||
|
break
|
||||||
else:
|
else:
|
||||||
del result[variable]
|
if not fullminute:
|
||||||
if int(now / 60) != int(self.end_query / 60):
|
to_remove[key] = l
|
||||||
# Update unchanged values every plain minute
|
self.update_last(result)
|
||||||
for var, (_, lastx) in self.lastvalues.items():
|
if fullminute:
|
||||||
if var not in result:
|
self.db.complete(result, self.last_time, 'stream')
|
||||||
result[var] = [(now, lastx)]
|
for key, length in to_remove.items():
|
||||||
self.end_query = now
|
curve = result[key]
|
||||||
|
if len(curve) > l:
|
||||||
|
del curve[:l]
|
||||||
|
else:
|
||||||
|
if fullminute:
|
||||||
|
print('R', key)
|
||||||
|
result.pop(key)
|
||||||
|
# print('poll', sum(len(c) for c in result.values()), self.last_time)
|
||||||
if len(result) > 0:
|
if len(result) > 0:
|
||||||
return dict(type='graph-update', time=now, graph=result)
|
self.last_update = now
|
||||||
|
return dict(type='graph-update', time=last_time, graph=result)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
class InfluxInstrument(Instrument):
|
class InfluxInstrument(Instrument):
|
||||||
|
|
||||||
def __init__(self, instr_name, inst_config=None):
|
def __init__(self, instr_name, inst_config=None):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self.db = InfluxDB()
|
self.db = InfluxDB()
|
||||||
self.influx_data_getter = InfluxDataGetter(self.db, instr_name)
|
# self.influx_data_getter = InfluxDataGetter(self.db, instr_name)
|
||||||
self.title = instr_name
|
self.title = instr_name
|
||||||
self.device = self.influx_data_getter.get_device_name(int(time.time()))
|
self.device = self.influx_data_getter.get_device_name(int(current_time()))
|
||||||
|
|
||||||
def new_client(self):
|
def new_client(self):
|
||||||
return self.register(InfluxClient(self))
|
return self.register(InfluxClient(self))
|
||||||
@ -268,7 +384,7 @@ class InfluxParams:
|
|||||||
class InfluxClient(InfluxParams, InfluxGraph):
|
class InfluxClient(InfluxParams, InfluxGraph):
|
||||||
def __init__(self, instrument):
|
def __init__(self, instrument):
|
||||||
InfluxParams.__init__(self)
|
InfluxParams.__init__(self)
|
||||||
InfluxGraph.__init__(self, instrument.influx_data_getter, instrument.title)
|
InfluxGraph.__init__(self, instrument)
|
||||||
|
|
||||||
def poll(self):
|
def poll(self):
|
||||||
messages = self.queue
|
messages = self.queue
|
||||||
@ -282,7 +398,7 @@ class InfluxClient(InfluxParams, InfluxGraph):
|
|||||||
class SecopInfluxClient(SecopClient, InfluxGraph):
|
class SecopInfluxClient(SecopClient, InfluxGraph):
|
||||||
def __init__(self, instrument):
|
def __init__(self, instrument):
|
||||||
SecopClient.__init__(self, instrument)
|
SecopClient.__init__(self, instrument)
|
||||||
InfluxGraph.__init__(self, instrument.influx_data_getter, instrument.title)
|
InfluxGraph.__init__(self, instrument)
|
||||||
|
|
||||||
def poll(self):
|
def poll(self):
|
||||||
messages = super().poll()
|
messages = super().poll()
|
||||||
@ -296,10 +412,20 @@ class SecopInfluxInstrument(SecopInstrument):
|
|||||||
|
|
||||||
def __init__(self, inst_name, instrument_config):
|
def __init__(self, inst_name, instrument_config):
|
||||||
super().__init__(inst_name, instrument_config)
|
super().__init__(inst_name, instrument_config)
|
||||||
self.db = InfluxDB()
|
config = ConfigParser()
|
||||||
self.influx_data_getter = InfluxDataGetter(self.db, inst_name)
|
config.optionxform = str
|
||||||
self.device = self.influx_data_getter.get_device_name(int(time.time()))
|
config.read("./config/influx.ini")
|
||||||
|
section = config["INFLUX"]
|
||||||
|
self.db = InfluxDBWrapper('linse-c')
|
||||||
|
# self.db = InfluxDBWrapper(uri=section["url"], token=section["token"],
|
||||||
|
# org=section["org"], bucket=section['bucket'])
|
||||||
|
# self.influx_data_getter = InfluxDataGetter(self.db, inst_name)
|
||||||
|
# self.device = self.influx_data_getter.get_device_name(int(current_time()))
|
||||||
|
|
||||||
def new_client(self):
|
def new_client(self):
|
||||||
return self.register(SecopInfluxClient(self))
|
return self.register(SecopInfluxClient(self))
|
||||||
|
|
||||||
|
def get_stream_tags(self, timestamp=None):
|
||||||
|
return self.db.get_streams(None, timestamp)
|
||||||
|
|
||||||
|
|
||||||
|
2
secop.py
2
secop.py
@ -136,7 +136,7 @@ class SecopInstrument(Instrument):
|
|||||||
# test_day = instrument_config.get('test_day', None)
|
# test_day = instrument_config.get('test_day', None)
|
||||||
# self.test_day = [int(x) for x in test_day.split('-')] if test_day else None
|
# self.test_day = [int(x) for x in test_day.split('-')] if test_day else None
|
||||||
self.title = inst_name
|
self.title = inst_name
|
||||||
self.device = 'UNDEFINED'
|
self.device = ''
|
||||||
self.nodes = []
|
self.nodes = []
|
||||||
self.node_map = {}
|
self.node_map = {}
|
||||||
for host_port in host_ports.split(','):
|
for host_port in host_ports.split(','):
|
||||||
|
35
webserver.py
35
webserver.py
@ -165,8 +165,13 @@ def reply():
|
|||||||
logging.error('%s', traceback.format_exc())
|
logging.error('%s', traceback.format_exc())
|
||||||
circularlog.log()
|
circularlog.log()
|
||||||
msg = dict(type='error', request=path[1:], error=repr(e))
|
msg = dict(type='error', request=path[1:], error=repr(e))
|
||||||
logging.info('REPLY %s %r', path, msg)
|
jsonmsg = json.dumps(msg)
|
||||||
resp = flask.Response(json.dumps(msg), mimetype='application/json')
|
if len(jsonmsg) < 120:
|
||||||
|
logging.info('REPLY %s %s', path, jsonmsg)
|
||||||
|
else:
|
||||||
|
logging.info('REPLY %s %s...', path, jsonmsg[:80])
|
||||||
|
logging.debug('REPLY %s %r', path, jsonmsg)
|
||||||
|
resp = flask.Response(jsonmsg, mimetype='application/json')
|
||||||
resp.headers['Access-Control-Allow-Origin'] = '*'
|
resp.headers['Access-Control-Allow-Origin'] = '*'
|
||||||
return resp
|
return resp
|
||||||
|
|
||||||
@ -206,10 +211,34 @@ def replace_by_empty(file):
|
|||||||
|
|
||||||
|
|
||||||
@app.route('/')
|
@app.route('/')
|
||||||
def default():
|
def main():
|
||||||
return general_file('SEAWebClient.html')
|
return general_file('SEAWebClient.html')
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/select')
|
||||||
|
def default():
|
||||||
|
out = ['''<html><body><table>
|
||||||
|
<style>
|
||||||
|
th {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<tr><th>Instrument</th><th colspan=99>Devices</th></tr>''']
|
||||||
|
result = {}
|
||||||
|
for stream, tags in instrument.get_stream_tags().items():
|
||||||
|
ins = tags.get('instrument', '0')
|
||||||
|
result.setdefault(ins, []).append((stream, tags.get('device')))
|
||||||
|
bare_streams = result.pop('0', [])
|
||||||
|
for ins, streams in result.items():
|
||||||
|
out.append(f'<tr><td><a href="/?ins={ins}">{ins}</a></td>')
|
||||||
|
out.extend(f'<td>{d or s}</td>' for s, d in streams)
|
||||||
|
out.append('</tr>')
|
||||||
|
for stream, device in bare_streams:
|
||||||
|
out.append(f'<tr><td><a href="/?srv={stream}">{stream}</a></td><td>{device}</td><tr>')
|
||||||
|
out.extend(['</table></body?</html>', ''])
|
||||||
|
return '\n'.join(out)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/<file>')
|
@app.route('/<file>')
|
||||||
def general_file(file):
|
def general_file(file):
|
||||||
subdir = "client/"
|
subdir = "client/"
|
||||||
|
Reference in New Issue
Block a user