8 Commits

Author SHA1 Message Date
8fc2c9a1f9 improve command handling 2025-05-13 11:01:35 +02:00
7bb60ddc37 fix bug in select_experiment page 2025-05-13 10:59:59 +02:00
4d60ebff50 remove obsolete stuff + small adjustments 2025-05-13 10:59:11 +02:00
5e1502d918 rename files with spaces in doc 2025-05-13 10:56:34 +02:00
b79616fd8d fix chart config parameters
- add SEA dil pressures
- read config each time when it is used
2025-05-13 10:50:51 +02:00
77035e859d remove some console.log for debug 2025-05-05 14:02:52 +02:00
07bd5407e0 fix simple command (e.g. stop) 2025-05-05 14:01:18 +02:00
fae76f2ae1 lazyPermission: allow to configure starting with writePermission=true 2025-05-05 14:00:35 +02:00
15 changed files with 99 additions and 1074 deletions

View File

@ -93,9 +93,9 @@ class Client(HandlerBase):
result = node.handle_command(command)
if result is not None:
break
if isinstance(result, str):
if isinstance(result, dict):
return dict(type='accept-command', result=result)
return dict(type='accept-command') # TODO: how to handle result is None?
return dict(type='accept-command')
def info(self):
return ["na"]

View File

@ -1,4 +1,5 @@
from configparser import ConfigParser
import logging
class ChartConfig:
@ -16,16 +17,15 @@ class ChartConfig:
Parameters :
path (str) : the path to the configuration file
"""
self.errors = {}
self.variables = {}
cfgp = ConfigParser(interpolation=None)
cfgp.optionxform = str
cfgp.read(path)
section = cfgp["chart"]
try:
section = cfgp["chart"]
except KeyError:
return
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': '*'}
@ -36,25 +36,11 @@ class ChartConfig:
config[argname] = argvalue
else:
if keyword_mode:
self.errors[key] = f"positional arg after keywd arg: {key}={raw_value!r}"
logging.error('positional arg after keywd arg: %s=%r', key, raw_value)
else:
try:
if argvalue:
config[self.KEYS[i]] = argvalue
except Exception as e:
self.errors[key] = f"{e!r} in {key}={raw_value}"
logging.error('%r in %s=%r', e, key, raw_value)
self.variables[key] = config
def get_variable_parameter_config(self, key):
"""
Gets the configuration of the given key in the configuration file (chart section).
Parameters :
key (str) : the key to look for in the chart section (<variable>[.<param>])
Returns :
{"cat":(str), "color":(str), "unit":(str)} | None : a dictionnary representing the different options for the given key 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.
"""
return self.variables.get(key)

View File

@ -48,7 +48,7 @@
}
.row-waiting-for-answer {
background-color: orangered;
background-color: LightGoldenrodYellow;
}
/* ------------------------------ icon-modules ------------------------------ */

View File

@ -215,18 +215,10 @@ function handleUpdate(message, src) {
function updateTarget(component) {
let matches = document.getElementsByName(component.name);
let elem = matches[0]; // Should be the only match
elem.value = component.value;
// elem.value = component.value;
let row = elem.closest('div');
row.classList.remove('row-waiting-for-answer');
let oldValue = ('oldValue' in elem) ? elem.oldValue : elem.value;
if (component.value != elem.value && parseFloat(component.value) != parseFloat(elem.value) && component.value != oldValue) {
if (elem == document.activeElement || oldValue != elem.value) {
row.classList.add('row-waiting-for-answer');
} else {
elem.value = component.value;
}
}
elem.actualValue = component.value;
if(elem.__ctype__ == 'input') {
resizeTextfield(elem);
@ -277,13 +269,12 @@ function updateValue(component) {
for (var j = 0; j < matches.length; j++) {
let elem = matches[j];
let type = elem.__ctype__; // -> Show Dom-Properties
if (type == "rdonly" || type == "none") {
if (type == "rdonly") {
let text = htmlEscape(component.formatted);
if (text) {
elem.innerHTML = text;
}
}
else if (type == "input") {
} else if (type == "input") {
let row = elem.closest('div');
row.classList.remove('row-waiting-for-answer');
elem.actualValue = component.value;
@ -306,10 +297,10 @@ function updateValue(component) {
// elem.value = j + 1;
// }
// }
elem.value = component.value;
console.log('update pushbutton');
console.log('component.name:', component.name);
console.log('element', elem);
} else if (type == "none") {
// pushbutton (e.g. stop command)
let row = elem.closest('div');
row.classList.remove('row-waiting-for-answer');
}
}
}
@ -468,6 +459,11 @@ function successHandler(s, message) {
// Response to a "updategraph"-server-request.
case "accept-graph":
break;
case "accept-command":
if (message.result) {
updateValue(message.result);
}
break;
case "error":
console.log("%cError-Message received!", "color:white;background:red");
console.log(message);

View File

@ -342,7 +342,7 @@ function loadGraphicsMenu(panel){
menuGraphicsPopup.addEntry(aboutTopRightHandCornerCrossHelpEntry);
// let graphicsMenuControl = new Control("res/menu_white_wide.png", "res/menu_white_wide.png", "Menu", () => {
let graphicsMenuControl = new Control("res/icon_menu_graphics.png", "icon_menu_graphics.png.png", "Menu", () => {
let graphicsMenuControl = new Control("res/icon_menu_graphics.png", "res/icon_menu_graphics.png", "Menu", () => {
datesPopup.hide();
exportPopup.hide();
curvesSettingsPopup.hide();

View File

@ -1,4 +1,4 @@
var writePermission = true;
var writePermission = false;
var showParams = false;
var showConsole = false;
var prompt = false // True while a prompt is opened.
@ -223,7 +223,11 @@ function createValue (component) {
value.classList.add('col-right-value-with-write-permission');
}
value.setAttribute('name', component.name);
value.__ctype__ = 'rdonly';
if (component.type == 'pushbutton') {
value.__ctype__ = 'none';
} else {
value.__ctype__ = 'rdonly';
}
return value;
}
@ -501,369 +505,3 @@ function appendToGridElement(s, title, type, content) {
gridelements[s].innerHTML = "";
gridelements[s].appendChild(gridContainer);
}
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
// %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
/* ---------------------------------------------------------------------------------- */
// obsolete...
// function createPushbutton_row(component) {
// // Creates row-element containing a push button
// var name = component.name;
// var command = component.command;
// var left = createTitle(component);
// left.id = component.name;
// left.name = component.title;
// var right = createInputElement(component);
// right.classList.add("push-button");
// row = appendToContent(left, right);
// right.onclick = function () {
// if (writePermission) {
// var row = left.parentNode;
// right.style.backgroundColor = "orangered";
// // Request for command
// sendCommand(s, command);
// } else {
// prompt = true;
// alertify.confirm("", "You are connected with <b>" + clientTitle
// + "</b>. <br>"
// + "Are you sure you want to modify things here?",
// function () {
// // User decided to proceed.
// writePermission = true;
// writePermissionTimeout = setTimeout(function () {
// writePermission = false;
// }, 3600000);
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// // Request for command
// sendCommand(s, command);
// prompt = false;
// }, function () {
// // User decided to cancel
// prompt = false;
// });
// }
// }
// row.classList.add("row");
// return row;
// }
// function create_input_row(component) {
// // Creates row-element containing input-item.
// var name = component.name;
// var command = component.command;
// var left = createTitle(component);
// var input = createInputElement(component, 'input', 'input-text');
// input.type = "text";
// input.style.width = "100px";
// input.addEventListener("focus", function(evt) {
// let elm = evt.target;
// setTimeout(function(){elm.setSelectionRange(0, elm.value.length);},0);
// });
// input.onkeydown = function (e) {
// if (e.which === 27 || e.key == "Escape") {
// // User decided to cancel
// input.value = intput.oldValue;
// resizeTextfield(input);
// var row = left.parentNode;
// row.style.backgroundColor = "white";
// }
// }
// input.onfocus = function () {
// input.oldValue = input.value;
// if (isTouchDevice)
// setTimeout(function () {
// posTextfield(s, left);
// }, 1);
// }
// input.onblur = function () {
// if (prompt) {
// return false;
// }
// var row = left.parentNode;
// var value = input.value;
// let oldValue = 'oldValue' in input ? input.oldValue : value;
// if (!('actualValue' in input)) input.actualValue = oldValue;
// actualValue = input.actualValue;
// if (value == actualValue || value == oldValue ||
// parseFloat(value) == parseFloat(actualValue) || parseFloat(value) == parseFloat(oldValue)) {
// input.value = actualValue;
// // nothing to do.
// row.style.backgroundColor = "white";
// return false;
// }
// // User changed value and moved focus to other object.
// alertify.confirm("", "You changed a field without pressing the return key.<br>"
// + "Hint: press ESC for leaving a field unchanged.<b>"
// + "You are connected with <b>" + clientTitle + "</b>.<br>"
// + "Are you sure you want to change the value of<br><b>"
// + name + "</b> from <b>" + actualValue
// + "</b> to <b>" + value + "</b>?", function () {
// // User decided to proceed.
// writePermission = true;
// writePermissionTimeout = setTimeout(function () {
// writePermission = false;
// }, 3600000);
// row.style.backgroundColor = "orangered";
// // Request for command
// sendCommand(s, command + " " + value);
// resizeTextfield(input);
// prompt = false;
// }, function () {
// // User decided to cancel
// input.value = input.actualValue;
// resizeTextfield(input);
// row.style.backgroundColor = "white";
// prompt = false;
// });
// }
// var form = document.createElement('form');
// form.onsubmit = function (e) {
// e.preventDefault();
// if (writePermission) {
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// // Request for command
// input.actualValue = input.value;
// sendCommand(s, name + " " + input.value);
// input.blur();
// } else {
// var value = input.value
// prompt = true;
// alertify.confirm("", "You are connected with <b>" + clientTitle
// + "</b>. <br>"
// + "Are you sure you want to modify things here?",
// function () {
// // User decided to proceed.
// writePermission = true;
// writePermissionTimeout = setTimeout(function () {
// writePermission = false;
// }, 3600000);
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// input.actualValue = value;
// // Request for command
// sendCommand(s, command + " " + value);
// resizeTextfield(input);
// prompt = false;
// }, function () {
// // User decided to cancel
// input.value = input.oldValue;
// resizeTextfield(input);
// prompt = false;
// });
// }
// };
// form.appendChild(input);
// var right = createInputElement(component);
// right.appendChild(form);
// return appendToContent(left, right);
// function posTextfield(s, left) {
// if (debug_group_daniel) {
// console.log("%cfunction: posTextfield", "color:white;background:salmon");
// }
// // var content = swiper[s].slides[swiper[s].activeIndex].childNodes[1];
// // var row = left.parentNode;
// // content.scrollTop = row.offsetTop - 30;
// // ---------------------> Not working anymore since swiper was removed!!!
// }
// }
// function createCheckbox_row(component) {
// // Creates row-element containing checkbox-item
// var command = component.command;
// var left = createTitle(component);
// var input = createInputElement(component, 'input', 'parameter-checkbox');
// input.type = "checkbox";
// input.onkeyup = function (e) {
// if (e.keyCode === 32) {
// handleCheckbox();
// }
// }
// var label = document.createElement('label');
// label.for = input;
// label.classList.add("parameter-label");
// label.onclick = function () {
// handleCheckbox();
// }
// function handleCheckbox() {
// if (writePermission) {
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// if (input.checked) {
// var value = "0";
// input.checked = false;
// } else {
// var value = "1";
// input.checked = true;
// }
// // Request for command
// sendCommand(s, command + " " + value);
// } else {
// alertify.confirm("", "You are connected with <b>" + clientTitle
// + "</b>. <br>"
// + "Are you sure you want to modify things here?",
// function () {
// // User decided to proceed.
// writePermission = true;
// writePermissionTimeout = setTimeout(function () {
// writePermission = false;
// }, 3600000);
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// if (input.checked) {
// var value = "0";
// input.checked = false;
// } else {
// var value = "1";
// input.checked = true;
// }
// // Request for command
// sendCommand(s, command + " " + value);
// }, function () {
// // User decided to cancel
// });
// }
// };
// var right = document.createElement('span');
// right.classList.add("col-right");
// right.appendChild(input);
// right.appendChild(label);
// return appendToContent(left, right);
// }
// function createEnum_row(component) {
// // Creates row-element containing dropdown-selection.
// var name = component.name;
// var command = component.command;
// var buttons = component.enum_names;
// var left = createTitle(component);
// var select = createInputElement(component, 'select', 'select-params');
// select.onfocus = function () {
// select.oldIndex = select.selectedIndex;
// }
// select.oninput = function () {
// if (writePermission && component.title != "device config") {
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// // Request for command
// sendCommand(s, command + " " + this.value);
// } else {
// alertify.confirm("", "You are connected with <b>" + clientTitle
// + "</b>. <br>"
// + "Are you sure you want to modify things here?",
// function () {
// // User decided to proceed.
// writePermission = true;
// writePermissionTimeout = setTimeout(function () {
// writePermission = false;
// }, 3600000);
// var row = left.parentNode;
// row.style.backgroundColor = "orangered";
// // Request for command
// sendCommand(s, command + " " + select.value);
// }, function () {
// // User decided to cancel
// select.value = select.options[select.oldIndex].value
// });
// }
// };
// for (var i = 0; i < buttons.length; i++) {
// var option = document.createElement('option');
// option.type = "enum";
// option.classList.add("option-params");
// option.value = buttons[i].value;
// option.appendChild(document.createTextNode(buttons[i].title));
// select.add(option);
// }
// select.style.display = "none";
// var right = document.createElement('span');
// right.classList.add("col-right");
// right.appendChild(select);
// return appendToContent(left, right);
// }
// ...obsolete
/* ---------------------------------------------------------------------------------- */
// The following two functions seem unreachable
/* ########## Is this function used? How? Where? ########## */
// function create_group_row(component) {
// // Creates row-element containing link.
// var title = component.title;
// var row = document.createElement('row');
// row.id = component.name;
// row.name = title;
// row.classList.add("row");
// row.tabIndex = "0";
// console.log('createGroupRow');
// row.onclick = function () {
// var slideNames = getSlideNames();
// slideNames[0] = component.name;
// document.title = "SEA "+ clientTitle + " " + slideNames.join(" ");
// history.pushState({func: "gotoGroups", funarg: slideNames.join("%20")}, document.title, "#" + slideNames.join("%20"));
// getGroup(s, component.name);
// }
// if (title === "console" || title === "device config") {
// row.classList.add("row");
// row.innerHTML = "console";
// }
// row.innerHTML = title;
// return row;
// }
/* ########## Is this function used? How? Where? ########## */
// function create_rdonly_row(component) {
// // Creates row-element containing link AND read-only-item.
// var link = component.link;
// if (!link) // simple rdonly
// return appendToContent(createTitle(component),
// createInputElement(component));
// // with link
// var left = document.createElement('a');
// left.innerHTML = component.title;
// console.log('ccc')
// left.id = component.name;
// row = appendToContent(left, createInputElement(component));
// row.onclick = function () {
// this.style.backgroundColor = "orangered";
// left.click();
// }
// if (link.charAt(0) == ':') {
// left.href = "http://" + location.hostname + link + "/";
// } else {
// left.href = link;
// }
// row.classList.add("row");
// return row;
// }

View File

@ -75,6 +75,8 @@ new Settings()
.treat("stream", "stream", 0, "")
.treat("instrument", "instrument", 0, "")
.treat("timerange", "time", 0, "-1800,0")
.treat("lazyPermission", "wr", to_bool, true);
if (window.instrument) {
window.clientTags = "&instrument=" + window.instrument;
@ -183,35 +185,24 @@ window.onload = function() {
adjustGrid();
}
icon_lock_container.onclick = function(){
function changeWritePermission(flag) {
let array_icon_edit = document.getElementsByClassName('icon-edit');
let array_pushbutton = document.getElementsByClassName('push-button');
let array_col_right_value = document.getElementsByClassName('col-right-value');
if (writePermission == false) {
alertify.prompt( "WRITE PERMISSION", "Current device: <b>"+ window.device + "</b><p>Please confirm the instrument:", ""
, function(evt, value) {
// User decided to proceed
if (window.instrument.toUpperCase() == value.toUpperCase()) {
writePermission = true;
icon_lock_container.innerHTML = '<img class = "icon-main icon-lock" src="res/icon_lock_open.png">';
for(i = 0; i < array_icon_edit.length; i++) {
array_icon_edit[i].classList.remove('icon-edit-hidden');
}
for(i = 0; i < array_pushbutton.length; i++) {
array_pushbutton[i].classList.add('push-button-active');
}
for (let i = 0; i < array_col_right_value.length; i++) {
array_col_right_value[i].classList.add('col-right-value-with-write-permission');
}
}
writePermission = flag;
if (writePermission) {
icon_lock_container.innerHTML = '<img class = "icon-main icon-lock" src="res/icon_lock_open.png">';
for(i = 0; i < array_icon_edit.length; i++) {
array_icon_edit[i].classList.remove('icon-edit-hidden');
}
for(i = 0; i < array_pushbutton.length; i++) {
array_pushbutton[i].classList.add('push-button-active');
}
for (let i = 0; i < array_col_right_value.length; i++) {
array_col_right_value[i].classList.add('col-right-value-with-write-permission');
}
, function() {
// User decided to cancel
prompt = false;
});
} else {
writePermission = false;
icon_lock_container.innerHTML = '<img class = "icon-main icon-lock" src="res/icon_lock_closed.png">';
for(i = 0; i < array_icon_edit.length; i++) {
array_icon_edit[i].classList.add('icon-edit-hidden');
@ -225,6 +216,33 @@ window.onload = function() {
}
}
if (window.lazyPermission) {
changeWritePermission(true);
}
icon_lock_container.onclick = function(){
if (writePermission == false) {
if (window.lazyPermission) {
changeWritePermission(true);
} else {
alertify.prompt( "WRITE PERMISSION", "Current device: <b>"+ window.device + "</b><p>Please confirm the instrument:", ""
, function(evt, value) {
// User decided to proceed
if (window.instrument.toUpperCase() == value.toUpperCase()) {
changeWritePermission(true);
}
}
, function() {
// User decided to cancel
prompt = false;
});
}
} else {
changeWritePermission(false);
}
}
var homeButton = document.getElementById("home-icon");
homeButton.onclick = function () {

View File

@ -1 +1,9 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
{"name":"",
"short_name":"",
"icons":[
{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},
{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],
"theme_color":"#ffffff",
"background_color":"#ffffff",
"display":"fullscreen"}

View File

@ -19,6 +19,13 @@ T_sorb=unit:K,color:dark_violet
T_sorb.target=-
T_still=unit:K,color:orange
dil=-
dil.G1=unit:mbar
dil.G2=unit:mbar
dil.G3=unit:mbar
dil.P1=unit:mbar
dil.P2=unit:mbar
dil.v6pos=unit:%
dil.V12A=unit:%
lev=unit:%,color:brown
lev.n2=unit:%,color:black
hefill=-

View File

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 146 KiB

View File

@ -1,624 +0,0 @@
from influxdb_client import InfluxDBClient
from configparser import ConfigParser
import ast
from datetime import datetime
from pandas import DataFrame as df, merge_ordered
from numpy import NaN
MEASURMENT_PREFIX = "nicos/se_"
BUCKET_PREFIX = "nicos-cache-"
class InfluxDB:
"""
Class used to handle the connection with the InfluxDB instance
Attributes :
_client (InfluxDBClient) : the InfluxDB client
"""
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"])
def disconnet(self):
"""
Disconnects from the InfluxDB instance
"""
self._client.close()
def query(self, query_str):
"""
Executes the query on the InfluxDB instance
Parameters :
query_str (string) : the Flux query to execute
Returns :
TableList : an InfluxDB list of the tables returned by the query
"""
return self._client.query_api().query(query_str)
def query_data_frame(self, query_str):
"""
Executes the query on the InfluxDB instance and gets the response as a pandas DataFrame
Parameters :
query_str (string) : the Flux query to execute
Returns :
DataFrame : the query response as a DataFrame
"""
return self._client.query_api().query_data_frame(query_str)
class PrettyFloat(float):
"""saves bandwidth when converting to JSON
a lot of numbers originally have a fixed (low) number of decimal digits
as the binary representation is not exact, it might happen, that a
lot of superfluous digits are transmitted:
str(1/10*3) == '0.30000000000000004'
str(PrettyFloat(1/10*3)) == '0.3'
"""
def __repr__(self):
return '%.15g' % self
class InfluxDataGetter:
"""
Class used to get data from InfluxDB.
Attributes :
_bucket (str) : the name of the InfluxDB bucket to query (used for all queries)
_db (InfluxDB) : the InfluxDB instance of the database to query
"""
def __init__(self, db, instrument_name):
"""
Parameters :
db (InfluxDB) : the InfluxDB instance of the database to query
instrument_name (str) : the name of the instrument from which the data will be got
"""
self._bucket = BUCKET_PREFIX + instrument_name
self._db = db
# ----- PUBLIC METHODS
def get_curves_in_timerange(self, variables, time, interval = None):
"""
Gets the curves for the given variables within a timerange.
Parameters :
variables ([(str)]) : an array of variable names (Influx) to get the curves for
time ([int]) : the timerange we want the values in. It consists of two values which are Unix timestamps in seconds, first included, second excluded.
interval (int|None) : the interval (resolution) of the values to get (in nanoseconds)
Returns :
{(str):[[(int), (float)]]} : a dictionnary of curves. The key is the name of the influx variable, and the value is an array of pairs (also arrays), the first value being the Unix timestamp in second (x), the seconds being the value (y).
"""
res = {}
for variable in variables:
var_param = variable.split(".")
variable_name_for_query = var_param[0]
parameter = "value" if len(var_param) == 1 else var_param[1]
curve = self._get_curve(variable_name_for_query, parameter, time, interval)
res[variable] = curve
return res
def poll_last_values(self, variables, lastvalues, end_time):
"""
Polls the lastest values for the given variables since their last known point to end_time.
Parameters :
variables ([(str)]) : an array of variable names (Influx) to get the last known values for
lastvalues ({(str):((float), (float))}) : a dictionnary of tuples, first value being the floating Unix timestamp in seconds (precision = ms) of the last known value for the curve, and the second value being the associated value, indexed by the curve name
end_time (int) : the Unix timestamp in seconds of the last point in time to include the values in
Returns :
{(str):[[(int), (float)]]} : a dictionnary of points. The key is the name of the influx variable, and the value is an array of pairs (also array), the first value being the Unix timestamp in second (x), the seconds being the value (y).
"""
res = {}
for variable in variables:
var_param = variable.split(".")
variable_name_for_query = var_param[0]
parameter = "value" if len(var_param) == 1 else var_param[1]
start_time = int(lastvalues[variable][0]) if variable in lastvalues.keys() else None #if the call to poll_last_values is more recent than getgraph, we trigger only one value, which is the last one available from 0 to end_time
points = []
if start_time == None or start_time < end_time: # start_time might be more recent than end_time if getgraph is called between graphpoll and pol_last_values (influxgraph.py)
points = self._get_last_values(variable_name_for_query,parameter,start_time, end_time)
if len(points) > 0 :
res[variable] = points
return res
def get_device_name(self, time):
"""
Gets the device name available at time with stick and addons
Parameters :
time (int) : the Unix timestamp in seconds of the time we want the device name
Returns :
str : the device name
"""
components = self._get_device_name_components(time)
return "/".join(components)
def get_curves_data_frame(self, variables, times, interval, variables_name_label_map=None):
"""
Gets the curves for the given variables within a timerange times, as a pandas dataframe.
All curves are on a single common time axis.
The first column is called "relative", and consists of floating seconds, relative to the beginning of the query.
The "timestamp" column (absolute floating UNIX timestamps in seconds, precise to the nanosecond) is the last one.
If a curve does not have a point at a given point, the last known value for this curve is used.
If a curve gets expired, it is filled with NaN value for the corresponding expired time windows.
The first value (for each variable) is the last known value before the time interval.
Parameters :
variables ([(str)]) : an array of variable names (Influx) to get the curves for.
times ([int]) : the timerange we want the values in. It consists of two values which are Unix timestamps in seconds, first included, second excluded.
interval (int) : the interval (resolution) of the values to get (in seconds). Allows data binning.
variables_name_label_map ({(str):(str)} | None) : a dictionnary containing curve labels, indexed by INFLUX names. The corresponding label will be used in the TSV header for each variable if found, else the influx name.
Returns :
pandas.DataFrame : the curves in a single pandas DataFrame
"""
variables_info = {}
for variable in variables:
var_param = variable.split(".")
variable_name_for_query = var_param[0]
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.
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"}")
{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:"time"})' if interval != 'None' else ''}
|> drop(columns:["_start", "_stop", "_field"])
|> pivot(rowKey:["relative", "timestamp", "expired"{', "time"' if interval != "None" else ''}], columnKey: ["_measurement"], valueColumn: "_value")
"""
data_frame = self._db.query_data_frame(query)
# If there is a binning asked, there can be two points with the same timestamp/relative value, because points with expired=True and expired=False can be found in the same interval, leading to two rows, one with last True, and one with last False.
# For each identified couple, we look at the real timestamp of the points used to feed this interval. We then keep the value which is the more recent in this interval.
if interval != "None" and not data_frame.empty:
# we first identify the expired points
expired_rows = data_frame.loc[data_frame["expired"] == "True"]
# we use itertuples to preserve the pandas dtypes, so comparisons can be done. "tuple" is a Python named tuple
for expired_point_tuple in expired_rows.itertuples():
# Then, we identify if there is a valid point with the same relative time as the current expired point
corresponding_valid_point = data_frame.loc[(data_frame["expired"] == "False") & (data_frame["relative"] == expired_point_tuple.relative)]
# we make sure that there is only one corresponding valid point, even if in theory, there will never be more than one corresponding valid point
if not corresponding_valid_point.empty and len(corresponding_valid_point.index) == 1:
# if we did not rename "_time" to "time" sooner in the query, "_time" would have been renamed to a positionnal name (because it starts with a _, see itertuples() doc), making confusion while reading the code
if corresponding_valid_point.iloc[0]["time"] > expired_point_tuple.time:
data_frame.drop(expired_point_tuple.Index, inplace=True)
else:
data_frame.drop(corresponding_valid_point.index, inplace=True)
# we do not need the "time" column anymore
data_frame.drop(["time"], axis=1, inplace=True)
# Needed for last known value
query_last_known = f"""
from(bucket: "{self._bucket}")
|> range(start: 0, stop: {times[0] + 1})
|> filter(fn : (r) => r._measurement == "{variable_name_for_query}")
|> filter(fn : (r) => r._field == "{parameter+"_float"}")
|> last()
|> map(fn: (r) => ({{r with relative: 0.0}}))
|> map(fn: (r) => ({{r with timestamp: float(v: uint(v: r._time)) / 1000000000.0}}))
|> drop(columns:["_start", "_stop", "_field"])
|> pivot(rowKey:["relative", "timestamp", "expired"], columnKey: ["_measurement"], valueColumn: "_value")
"""
data_frame_last_known = self._db.query_data_frame(query_last_known)
row_to_insert = None
for index, row in data_frame_last_known.iterrows():
try: #needed because row_to_insert == None is not possible
if row_to_insert.empty or row["timestamp"] > row_to_insert["timestamp"]:
row_to_insert = row
except:
row_to_insert = row
try: #row_to_insert might be None
if not row_to_insert.empty :
row_to_insert["timestamp"] = float(times[0])
if data_frame.empty:
data_frame = row_to_insert.to_frame().T
else:
data_frame.loc[-1] = row_to_insert
except:
pass
if data_frame.empty:
continue
variable_df_column_name = variables_name_label_map.get(variable, variable) if not variables_name_label_map == None else variable
data_frame.rename(columns={variable_name_for_query : variable_df_column_name}, inplace=True)
data_frame.drop(["result", "table"], axis=1, inplace=True)
data_frame.sort_values(by=["timestamp"], inplace=True)
data_frame.reset_index()
variables_info[variable_df_column_name] = {}
variables_info[variable_df_column_name]["expired_ranges"] = []
# Identify time windows for which the curve is expired
for index, row in data_frame.iterrows():
if row["expired"] == "True":
data_frame.loc[index, variable_df_column_name] = NaN
variables_info[variable_df_column_name]["expired_ranges"].append([row["timestamp"]])
elif row["expired"] == "False":
if len(variables_info[variable_df_column_name]["expired_ranges"]) > 0 and len(variables_info[variable_df_column_name]["expired_ranges"][-1]) == 1:
variables_info[variable_df_column_name]["expired_ranges"][-1].append(row["timestamp"])
data_frame.reset_index()
data_frame.drop(["expired"], axis=1, inplace=True)
variables_info[variable_df_column_name]["df"] = data_frame
res = None
non_empty_variables = list(variables_info.keys())
# Merge single curve dataframes to a global one
if len(non_empty_variables) == 0:
return df()
elif len(non_empty_variables) == 1:
res = variables_info[non_empty_variables[0]]["df"]
else :
for i in range(0, len(non_empty_variables)):
if i == 1:
res = merge_ordered(variables_info[non_empty_variables[0]]["df"], variables_info[non_empty_variables[1]]["df"], on=["timestamp", "relative"], suffixes=(None, None))
elif i > 1:
res = merge_ordered(res, variables_info[non_empty_variables[i]]["df"], on=["timestamp", "relative"], suffixes=(None, None))
# Forward fill missing values, then set data points to NaN for those which are expired
if len(non_empty_variables) > 1:
res.ffill(inplace=True)
for variable, info in variables_info.items():
for expired_range in info["expired_ranges"]:
res.loc[(res["timestamp"] >= expired_range[0]) & ((res["timestamp"] < expired_range[1]) if len(expired_range) == 2 else True), variable] = NaN
# Change order of columns
cols = res.columns.tolist()
cols = [cols[0]] + cols[2:] + [cols[1]]
res = res[cols]
return res
# ----- PRIVATE METHODS
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.
Lowers the secop_module_name and adds "nicos/se_" as prefix
Parameters :
secop_module_name (str) : the secop module name of the variable in the setup_info dict.
Returns :
str : the transformed variable name that matches the Influx names reqauirements
"""
return MEASURMENT_PREFIX + secop_module_name.lower()
def _filter_params_with_config(self, available_variables, chart_config):
"""
Updates (cat, color, unit) the parameters of each variable according to the user_config object.
Parameters:
available_variables ([{"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 dictionnaries with the category, the color and the unit, indexed by the name of the parameter.
chart_config (ChartConfig) : the object holding a configuration file for the chart.
Returns :
[{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}] : the available_variables parameter, updated
"""
for variable in available_variables:
params = list(variable["params"].keys())
for param_key in params:
key = variable["label"] if param_key == "value" else variable["label"]+"."+param_key
param_config = chart_config.get_variable_parameter_config(key)
if param_config != None :
for key, value in param_config.items():
variable["params"][param_key][key] = value
return available_variables
def _filter_params_with_user_config(self, available_variables, user_config):
"""
Updates (cat, color, unit) the parameters of each variable according to the user_config object.
Parameters:
available_variables ([{"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 dictionnaries with the category, the color and the unit, indexed by the name of the parameter.
user_config ({(str):{"cat":(str), "color":(str), "unit":(str)}}) : the Python dict representing the user configuration. The key is <secop_module.parameter>.
Returns :
[{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}] : the available_variables parameter, updated
"""
for variable in available_variables:
params = list(variable["params"].keys())
for param_key in params:
key = variable["label"] if param_key == "value" else variable["label"]+"."+param_key
param_config = user_config[key] if key in user_config.keys() else None
if param_config != None :
for key, value in param_config.items():
variable["params"][param_key][key] = value
return available_variables
def _remove_variables_params_not_displayed(self, available_variables):
"""
Removes the parameters of each variable if their category is "-".
Parameters:
available_variables ([{"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 dictionnaries with the category, the color and the unit, indexed by the name of the parameter.
Returns :
[{"name":(str), "label":(str), "params":{(str):{"cat":(str), "color":(str), "unit":(str)}}}] : the available_variables parameter, updated
"""
for variable in available_variables:
params = list(variable["params"].keys())
for param_key in params:
if variable["params"][param_key]["cat"] == "-":
del variable["params"][param_key]
return available_variables
def _remove_variables_params_wihout_param_float_and_split(self, available_variables, time):
"""
For each variable, removes the parameters if the Influx database does not contain <param>.float field, and split the parameters to the corresponding output format.
Parameters:
available_variables ([{"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, the color and the unit, indexed by the name of the parameter.
time (int) : the unix timestamp in seconds of the point in time to get the variables at. Used to have an upper limit in the query.
Returns :
[{"name":(str), "label":(str), "cat":(str), "color":(str), "unit":(str)}] : an array of dictionnaries, each containing the name of the variable[.<param>],
the label to display in the Web GUI, its category, its color and its unit.
"""
res = []
for variable in available_variables:
query = f"""
import "influxdata/influxdb/schema"
schema.measurementFieldKeys(bucket: "{self._bucket}", measurement: "{variable["name"]}", start:0, stop: {time + 1})
|> yield(name: "res")
"""
records = self._db.query(query)[0].records
fields = [record.get_value() for record in records]
for param in variable["params"].keys():
if param+"_float" in fields:
curve = {
"name": variable["name"] if param == "value" else variable["name"]+"."+param,
"label": variable["label"] if param == "value" else variable["label"]+"."+param,
"cat": variable["params"][param]["cat"],
"color": variable["params"][param]["color"],
"unit": variable["params"][param]["unit"],
}
if param == 'target':
curve['period'] = 0
res.append(curve)
return res
def _group_variables_by_cat_unit(self, available_variables):
"""
Performs a group by cat if specified (different than "*"), or by unit instead for the available variables
Parameters :
available_variables ([{"name":(str), "label":(str), "cat":(str), "color":(str), "unit":(str)}]) : an array of dictionnaries, each containing the name of the variable[.<param>],
the label to display in the Web GUI, its category, its color and its unit.
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 tag, which is the unit or the category if given and not "unit".
"""
groups = {}
for available_variable in available_variables:
unit = available_variable.pop("unit")
key = available_variable.pop("cat").replace("*", unit)
if key not in groups.keys():
groups[key] = {"tag":key, "unit": unit, "curves":[]}
groups[key]["curves"].append(available_variable)
return list(groups.values())
def _get_curve(self, variable, parameter, time, interval=None):
"""
Gets the points (curve) within a timerange for the given variable and parameter.
Parameters :
variable (str) : the name (Influx) of the variable we want the values of.
parameter (str) : the parameter of the variable to get the values from
time ([(int)]) : the timerange we want the values in. It consists of two values which are Unix timestamps in seconds, first included, second excluded.
interval (int) : the interval (resolution) of the values to get (in milliseconds)
Returns :
[[(int), (float)]] : an array of pairs (also arrays), the first value being the Unix timestamp in second (x), the seconds being the value (y)
"""
raw = []
query = f"""
from(bucket: "{self._bucket}")
|> range(start: {time[0]}, stop: {time[1] + 1})
|> filter(fn : (r) => r._measurement == "{variable}")
|> filter(fn : (r) => r._field == "{parameter+"_float"}")
{"|> aggregateWindow(every: duration(v:"+str(self._milliseconds_to_nanoseconds(interval))+"), fn: last, createEmpty:false, timeDst:"+chr(34)+"binning_time"+chr(34)+")" if interval else ""}
|> keep(columns: ["_time","_value","expired"{", "+chr(34)+"binning_time"+chr(34) if interval else ""}])
|> yield(name: "res")
"""
tables = self._db.query(query)
for table in tables:
for record in table.records:
t = round(datetime.timestamp(record["binning_time"] if interval else record.get_time()), 3) # t is the real timestamp if no interval is given, or the binned timestamp if interval
value = record.get_value()
try:
value = PrettyFloat(value)
except:
value = None
point = [t, value, record["expired"]]
if interval: # t is the binning time, we need to add the real time of the point that was used in this interval
point.append(record.get_time())
raw.append(point)
if interval:
indexes_to_delete = []
expired_points = {i:point for i,point in enumerate(raw) if point[2] == "True"} #we need to keep track of the indexes of the expired point in the list
for expired_point_index, expired_point in expired_points.items():
for i, point in enumerate(raw):
if point[2] == "False" and expired_point[0] == point[0]: # if the current point is expired and has the same binning time as the current expired point
if point[3] > expired_point[3]: # comparison on the real timestamp used.
indexes_to_delete.insert(0, expired_point_index)
else:
indexes_to_delete.insert(0,i)
sorted(indexes_to_delete, reverse=True) #we have to make sure that the list is sorted in reverse to then delete at the given indexes
for index in indexes_to_delete:
del raw[index]
sorted_raw = sorted(raw, key=lambda pair: pair[0]) #expired=True and expired=False are in two Influx tables, so they need to be synchronized
res = []
for pair in sorted_raw:
if pair[2] == "True":
res.append([pair[0], pair[1]]) # So the user can know precisely when a curve is expired
res.append([pair[0], None]) # So chartJS will cut the curve from this point (which is expired)
else:
res.append([pair[0], pair[1]])
return self._insert_last_known_value(variable, parameter, res, time)
def _insert_last_known_value(self, variable, parameter, curve, time):
"""
Adds the last known value as the first point in the curve if the last known value is outside the viewing window, for the given variable and parameter.
The point is added only if it is not expired.
Parameters :
variable (str) : the name (Influx) of the variable we want the values of.
parameter (str) : the parameter of the variable to get the values from
curve ([[(int), (float)]]) : an array of pairs (arrays), the first value being the Unix timestamp in second (x), the seconds being the value (y)
time ([(int)]) : the timerange we want the values in. It consists of two values which are Unix timestamps in seconds, first included, second excluded.
Returns :
[[(int), (float)]] : the curve of the parameter, updated with a potential new first point
"""
if len(curve) == 0 or curve[0][0] != time[0]:
query = f"""
from(bucket: "{self._bucket}")
|> range(start: 0, stop: {time[0]+1})
|> filter(fn : (r) => r._measurement == "{variable}")
|> filter(fn : (r) => r._field == "{parameter+"_float"}")
|> last()
|> keep(columns: ["_time", "_value", "expired"])
|> yield(name: "res")
"""
tables = self._db.query(query)
pair_to_insert = []
for table in tables:
for record in table.records:
t = round(datetime.timestamp(record.get_time()), 3)
value = None
if record["expired"] == "False":
value = record.get_value()
try:
value = PrettyFloat(value)
except:
value = None
if len(pair_to_insert) == 0 or t >= pair_to_insert[0]:
pair_to_insert = [t, value]
if len(pair_to_insert)==2 and pair_to_insert[1] != None:
curve.insert(0, [time[0], pair_to_insert[1]])
return curve
def _get_last_values(self, variable, parameter, start_time, end_time):
"""
Gets the lastest values for the given variable and parameter that are in [start_time, end_time].
The process is the same as _get_curve.
Parameters :
variable (str) : the name (Influx) of the variable we want the last value of.
parameter (str) : the parameter of the variable to get the values from
start_time (int|None) : the start of time range (Unix timestamp in seconds) to include the values in
end_time (int) : the end of time range (Unix timestamp in seconds) to include the values in
Returns :
[[(int), (float)]] : an array of points (also arrays). The first value is the Unix timestamp in second (x), the seconds is the value (y)
"""
raw = []
query = f"""
from(bucket: "{self._bucket}")
|> range(start: {start_time if start_time != None else 0}, stop: {end_time+1})
|> filter(fn : (r) => r._measurement == "{variable}")
|> filter(fn : (r) => r._field == "{parameter+ "_float"}")
{"|> last()" if start_time == None else ""}
|> keep(columns: ["_time","_value", "expired"])
|> yield(name: "res")
"""
# this loop might be simplified, but it has to be kept to catch the case when there is unavailable data
tables = self._db.query(query)
for table in tables:
for record in table.records:
t = round(datetime.timestamp(record.get_time()), 3)
value = record.get_value()
try:
value = PrettyFloat(value)
except:
value = None
raw.append([t, value, record["expired"]])
sorted_raw = sorted(raw, key=lambda pair: pair[0])
res = []
for pair in sorted_raw:
if pair[2] == "True":
res.append([pair[0], pair[1]])
res.append([pair[0], None])
else:
res.append([pair[0], pair[1]])
return res
def _get_device_name_components(self, time):
"""
Gets the components of the device name, first in the main name, then stick, then addons.
Parameters :
time (int) : the Unix timestamp in seconds of the time we want the device name
Returns :
[str] : an array of string, each one being a component of the device name
"""
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 == "value")
|> last()
"""
tables = self._db.query(query)
for table in tables:
for record in table.records:
name = ast.literal_eval(record.get_value())
if name != None and name != '':
res.append(ast.literal_eval(record.get_value()))
return res
def _seconds_to_nanoseconds(self, seconds):
return seconds * 1000000000
def _milliseconds_to_nanoseconds(self, milliseconds):
return milliseconds * 1000000

View File

@ -58,14 +58,11 @@ class InfluxGraph(HandlerBase):
self.server = server
self.db = server.db
# self.influx_data_getter = influx_data_getter
self.chart_configs = [ChartConfig("./config/generic.ini")]
self.chart_configs = ["./config/generic.ini"]
self.instrument = instrument
self.device_name = device_name
if instrument: # TODO: should it not be better to have inifiles per device?
try:
self.chart_configs.append(ChartConfig(f"./config/{instrument}.ini"))
except KeyError:
pass
self.chart_configs.append(f"./config/{instrument}.ini")
self.livemode = self.HISTORICAL
self.last_values = {} # dict <variable> of last known point (<time>, <value>)
self.last_time = {} # dict <stream> of last received time
@ -204,12 +201,11 @@ class InfluxGraph(HandlerBase):
config = {}
if chart_configs:
for chart_config in chart_configs:
for key, cfg in chart_config.variables.items():
for key, cfg in ChartConfig(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):

View File

@ -150,17 +150,17 @@ class SecopInteractor(SecopClient):
if module not in self.modules:
return None
logging.info('SENDCOMMAND %r', command)
try:
if is_param:
if is_param:
try:
entry = self.setParameterFromString(module, parameter, strvalue)
item = {'name': f'{module}:{parameter}', 'value': str(entry), 'formatted': entry.formatted()}
self.updates[module, parameter] = item
result = True
else:
result = self.execCommandFromString(module, parameter, strvalue)[0]
except Exception as e:
print(f"{e!r} converting {strvalue} to {self.modules[module]['parameters'][parameter]['datatype']}")
return result
except Exception as e:
print(f"{e!r} converting {strvalue} to {self.modules[module]['parameters'][parameter]['datatype']}")
self.updates[module, parameter] = item
return True
# called a command
formatted = self.execCommandFromString(module, parameter, strvalue)[0] # ignore qualifiers
return {'name': f'{module}:{parameter}', 'value': str(result), 'formatted': formatted}
def get_updates(self):
updates, self.updates = self.updates, {}

View File

@ -380,7 +380,7 @@ a {
showtitle = 1
else:
daterange = begdate if begdate == enddate else f'{begdate}...{enddate}'
if end > now - ONEMONTH:
if end < now - ONEMONTH:
if showtitle == 1:
title('older than 30 days')
showtitle = 2