Implemented connection with InfluxDB

This commit is contained in:
l_samenv
2024-07-22 15:51:51 +02:00
parent 0c0662b0b1
commit aa29d89a18
10 changed files with 1017 additions and 5 deletions

126
colors.py Normal file
View File

@ -0,0 +1,126 @@
import json
def assign_colors_to_curves(blocks):
"""
Assign a color to each curve contained in the blocks
Parameters :
blocks ["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 (if found in the database), grouped by their tag (which can be the unit augmented with an index) and their unit
Returns :
["tag":(str),"unit":(str), "curves":[{"name":(str), "label":(str), "color":(str), "original_color":(str)}]] : the same object as the "blocks" parameter, with the color value
updated depending on its availability in the ColorMap, and a new "original_color" value which contains the previous "color" value
"""
color_map = json.load(open("./graphs/color_mapping.json", "r"))
# get last value only
for block in blocks:
color_set = set()
auto_curves = []
for curve in block["curves"]:
if curve["name"] in color_map:
curve["original_color"] = "?"
curve["color"] = ColorMap.to_hex(ColorMap.to_code(color_map[curve["name"]]))
continue
col = curve["color"].strip()
c = ColorMap.to_code(col)
if c < 0:
valid = ColorMap.check_hex(col)
if valid:
curve["original_color"] = col
curve["color"] = valid
else:
auto_curves.append(curve)
curve["original_color"] = col + "?"
else:
color_set.add(c)
curve["original_color"] = col
curve["color"] = ColorMap.to_hex(c)
c = 1 # omit white
for curve in auto_curves:
while c in color_set: c += 1 # find unused color
curve["color"] = ColorMap.to_hex(c)
c += 1
class ColorMap(object):
'''
ColorMap is using official CSS color names, with the exception of Green, as this
is defined differently with X11 colors than in SEA, and used heavily in config files.
Here Green is an alias to Lime (#00FF00) and MidGreen is #008000, which is called Green in CSS.
The function to_code is case insensitive and accepts also names with underscores.
The order is choosen by M. Zolliker for the SEA client, originally only the first 16 were used.
'''
hex_name = (("#FFFFFF","White"), ("#FF0000","Red"), ("#00FF00","Lime"), ("#0000FF","Blue"), ("#FF00FF","Magenta"),
("#FFFF00","Yellow"), ("#00FFFF","Cyan"), ("#000000","Black"), ("#FFA500","Orange"), ("#006400","DarkGreen"),
("#9400D3","DarkViolet"), ("#A52A2A","Brown"), ("#87CEEB","SkyBlue"), ("#808080","Gray"), ("#FF69B4","HotPink"),
("#FFFFE0","LightYellow"), ("#00FF7F","SpringGreen"), ("#000080","Navy"), ("#1E90FF","DodgerBlue"),
("#9ACD32","YellowGreen"), ("#008B8B","DarkCyan"), ("#808000","Olive"), ("#DEB887","BurlyWood"),
("#7B68EE","MediumSlateBlue"), ("#483D8B","DarkSlateBlue"), ("#98FB98","PaleGreen"), ("#FF1493","DeepPink"),
("#FF6347","Tomato"), ("#32CD32","LimeGreen"), ("#DDA0DD","Plum"), ("#7FFF00","Chartreuse"), ("#800080","Purple"),
("#00CED1","DarkTurquoise"), ("#8FBC8F","DarkSeaGreen"), ("#4682B4","SteelBlue"), ("#800000","Maroon"),
("#3CB371","MediumSeaGreen"), ("#FF4500","OrangeRed"), ("#BA55D3","MediumOrchid"), ("#2F4F4F","DarkSlateGray"),
("#CD853F","Peru"), ("#228B22","ForestGreen"), ("#48D1CC","MediumTurquoise"), ("#DC143C","Crimson"),
("#D3D3D3","LightGray"), ("#ADFF2F","GreenYellow"), ("#7FFFD4","Aquamarine"), ("#BC8F8F","RosyBrown"),
("#20B2AA","LightSeaGreen"), ("#C71585","MediumVioletRed"), ("#F0E68C","Khaki"), ("#6495ED","CornflowerBlue"),
("#556B2F","DarkOliveGreen"), ("#CD5C5C","IndianRed "), ("#2E8B57","SeaGreen"), ("#F08080","LightCoral"),
("#8A2BE2","BlueViolet"), ("#AFEEEE","PaleTurquoise"), ("#4169E1","RoyalBlue"), ("#0000CD","MediumBlue"),
("#B8860B","DarkGoldenRod"), ("#00BFFF","DeepSkyBlue"), ("#FFC0CB","Pink"), ("#4B0082","Indigo "), ("#A0522D","Sienna"),
("#FFD700","Gold"), ("#F4A460","SandyBrown"), ("#DAA520","GoldenRod"), ("#DA70D6","Orchid"), ("#E6E6FA","Lavender"),
("#5F9EA0","CadetBlue"), ("#D2691E","Chocolate"), ("#66CDAA","MediumAquaMarine"), ("#6B8E23","OliveDrab"),
("#A9A9A9","DarkGray"), ("#BDB76B","DarkKhaki"), ("#696969","DimGray"), ("#B0C4DE","LightSteelBlue"),
("#191970","MidnightBlue"), ("#FFE4C4","Bisque"), ("#6A5ACD","SlateBlue"), ("#EE82EE","Violet"),
("#8B4513","SaddleBrown"), ("#FF7F50","Coral"), ("#008000","MidGreen"), ("#DB7093","PaleVioletRed"), ("#C0C0C0","Silver"),
("#E0FFFF","LightCyan"), ("#9370DB","MediumPurple"), ("#FF8C00","DarkOrange"), ("#00FA9A","MediumSpringGreen"),
("#E9967A","DarkSalmon"), ("#778899","LightSlateGray"), ("#9932CC","DarkOrchid"), ("#EEE8AA","PaleGoldenRod"),
("#F8F8FF","GhostWhite"), ("#FFA07A","LightSalmon"), ("#ADD8E6","LightBlue"), ("#D8BFD8","Thistle"),
("#FFE4E1","MistyRose"), ("#FFDEAD","NavajoWhite"), ("#40E0D0","Turquoise"), ("#90EE90","LightGreen"),
("#B22222","FireBrick"), ("#008080","Teal"), ("#F0FFF0","HoneyDew"), ("#FFFACD","LemonChiffon"), ("#FFF5EE","SeaShell"),
("#F5F5DC","Beige"), ("#DCDCDC","Gainsboro"), ("#FA8072","Salmon"), ("#8B008B","DarkMagenta"), ("#FFB6C1","LightPink"),
("#708090","SlateGray"), ("#87CEFA","LightSkyBlue"), ("#FFEFD5","PapayaWhip"), ("#D2B48C","Tan"), ("#FFFFF0","Ivory"),
("#F0FFFF","Azure"), ("#F5DEB3","Wheat"), ("#00008B","DarkBlue"), ("#FFDAB9","PeachPuff"), ("#8B0000","DarkRed"),
("#FAF0E6","Linen"), ("#B0E0E6","PowderBlue"), ("#FFE4B5","Moccasin"), ("#F5F5F5","WhiteSmoke"), ("#FFF8DC","Cornsilk"),
("#FFFAFA","Snow"), ("#FFF0F5","LavenderBlush"), ("#FFEBCD","BlanchedAlmond"), ("#F0F8FF","AliceBlue"),
("#FAEBD7","AntiqueWhite"), ("#FDF5E6","OldLace"), ("#FAFAD2","LightGoldenRodYellow"), ("#F5FFFA","MintCream"),
("#FFFAF0","FloralWhite"), ("#7CFC00","LawnGreen"), ("#663399","RebeccaPurple"))
codes = {}
for i, pair in enumerate(hex_name):
codes[pair[0]] = i
low = pair[1].lower()
codes[low] = i
codes[low.replace("gray","grey")] = i
codes["green"] = 2
codes["fuchsia"] = 4
codes["aqua"] = 6
@staticmethod
def to_code(colortext):
try:
return int(colortext)
except ValueError:
return ColorMap.codes.get(colortext.lower().replace("_",""),-1)
@staticmethod
def check_hex(code):
if not code.startswith("#"):
return None
if len(code) == 4: # convert short code to long code
code = code[0:2] + code[1:3] + code[2:4] + code[3]
if len(code) != 7:
return None
try:
int(code[1:]) # we have a valid hex color code
return code
except ValueError:
return None
@staticmethod
def to_hex(code):
try:
return ColorMap.hex_name[code][0]
except IndexError:
return -1

4
config/config.ini Normal file
View File

@ -0,0 +1,4 @@
[INFLUX]
url=http://localhost:8086
org=linse
token=zqDbTcMv9UizfdTj15Fx_6vBetkM5mXN56EE9CiDaFsh7O2FFWZ2X4VwAAmdyqZr3HbpIr5ixRju07-oQmxpXw==

View File

@ -0,0 +1,3 @@
{
"nicos/se_t_plato":"Yellow"
}

18
graphs/lab4.json Normal file
View File

@ -0,0 +1,18 @@
{
"types":
{
"graph":"influx",
"control":"sea"
},
"influx":
{
"bucket_prefix":"nicos-cache-",
"bucket":"nicos-cache-lab4",
"measurement_prefix":"nicos/se_",
"setup_info_prefix":"se_"
},
"sea":{
"address":"samenv:8664",
"logger_dir":"/home/l_samenv/sea/lab4/"
}
}

View File

@ -0,0 +1,133 @@
(
'nicos.devices.secop.devices.SecopMoveable',
{
'secnode': 'se_main',
'description': 'magnetic field, x-axis',
'secop_module': 'mfx',
'params_cfg': {
'pollinterval_': {
'datainfo': {'unit': 's', 'min': 0.1, 'max': 120.0, 'type': 'double'},
'description': 'default poll interval',
'settable': True,
'unit': 's'
},
'target': {
'datainfo': None,
'description': 'target value of the module',
'unit': 'T'
},
'target_min': {
'datainfo': {'unit': 'T', 'min': -0.6, 'max': 0.6, 'type': 'double'},
'description': 'limit for target',
'settable': True,
'unit': 'T'
},
'target_max': {
'datainfo': {'unit': 'T', 'min': -0.6, 'max': 0.6, 'type': 'double'},
'description': 'limit for target',
'settable': True,
'unit': 'T'
},
'ramp': {
'datainfo': {'unit': 'T/min', 'type': 'double'},
'description': 'wanted ramp rate for field',
'settable': True,
'unit': 'T/min'
},
'tolerance': {
'datainfo': {'unit': 'T', 'min': 0.0, 'type': 'double'},
'description': 'tolerance',
'settable': True,
'unit': 'T'
},
'trained': {
'datainfo': {
'type': 'tuple',
'members': [
{'unit': 'T', 'min': -99.0, 'max': 0.0, 'type': 'double'},
{'unit': 'T', 'min': 0.0, 'type': 'double'}
]
},
'description': 'trained field (positive)',
'settable': True
},
'wait_stable_field': {
'datainfo': {'unit': 's', 'min': 0.0, 'type': 'double'},
'description': 'wait time to ensure field is stable',
'settable': True,
'unit': 's'
},
'ramp_tmo': {
'datainfo': {'unit': 's', 'min': 0.0, 'type': 'double'},
'description': 'timeout for field ramp progress',
'settable': True,
'unit': 's'
},
'action': {
'datainfo': {
'type': 'enum',
'members': {'hold': 0, 'run_to_set': 1, 'run_to_zero': 2, 'clamped': 3, 'not_found': 4}
},
'description': 'action',
'settable': True
},
'setpoint': {
'datainfo': {'unit': 'T', 'type': 'double'},
'description': 'field setpoint',
'unit': 'T'
},
'voltage': {
'datainfo': {'unit': 'V', 'type': 'double'},
'description': 'leads voltage',
'unit': 'V'
},
'atob': {
'datainfo': {'unit': 'A/T', 'min': 0.0, 'type': 'double'},
'description': 'field to amp',
'unit': 'A/T'
},
'working_ramp': {
'datainfo': {'unit': 'T/min', 'min': 0.0, 'type': 'double'},
'description': 'effective ramp',
'unit': 'T/min'
},
'i1': {
'datainfo': {'unit': 'A', 'type': 'double'},
'description': 'slave 1 current',
'unit': 'A'
},
'v1': {
'datainfo': {'unit': 'V', 'type': 'double'}
'description': 'slave 1 voltage',
'unit': 'V'
},
'i2': {
'datainfo': {'unit': 'A', 'type': 'double'},
'description': 'slave 2 current', 'unit': 'A'
},
'v2': {
'datainfo': {'unit': 'V', 'type': 'double'},
'description': 'slave 2 voltage',
'unit': 'V'
}
},
'commands_cfg': {
'stop': {
'description': 'keep field at current value',
'datainfo': {'type': 'command'}
}
},
'mixins': [],
'secop_properties': {
'description': 'magnetic field, x-axis',
'implementation':
'frappy_psi.ips_mercury.SimpleField2',
'interface_classes': ['Drivable'],
'features': []
},
'unit': 'T',
'fmtstr': '%g',
'value_datainfo': {'unit': 'T', 'type': 'double'},
'target_datainfo': {'unit': 'T', 'min': -0.6, 'max': 0.6, 'type': 'double'}
}
)

View File

@ -0,0 +1,170 @@
(
'nicos.devices.secop.devices.SecopMoveable',
{
'secnode': 'se_main',
'description': 'channel 1',
'secop_module': 'T_chip',
'params_cfg': {
'pollinterval_': {
'datainfo': {
'unit': 's',
'min': 0.1,
'max': 120.0,
'type':
'double'
},
'description': 'default poll interval',
'settable': True,
'unit': 's'
},
'target': {
'datainfo': None,
'description': 'setpoint',
'unit': 'K'
},
'enabled': {
'datainfo': {
'type': 'bool'
},
'description': 'is this channel enabled?',
'settable': True
},
'range': {
'datainfo': {
'type': 'enum',
'members': {
'2mOhm': 1,
'6.32mOhm': 2,
'20mOhm': 3,
'63.2mOhm': 4,
'200mOhm': 5,
'632mOhm': 6,
'2Ohm': 7,
'6.32Ohm': 8,
'20Ohm': 9,
'63.2Ohm': 10,
'200Ohm': 11,
'632Ohm': 12,
'2kOhm': 13,
'6.32kOhm': 14,
'20kOhm': 15,
'63.2kOhm': 16,
'200kOhm': 17,
'632kOhm': 18,
'2MOhm': 19,
'6.32MOhm': 20,
'20MOhm': 21,
'63.2MOhm': 22
}
},
'description': 'reading range',
'settable': True
},
'minrange': {
'datainfo': {
'type': 'enum',
'members': {'2mOhm': 1, '6.32mOhm': 2, '20mOhm': 3, '63.2mOhm': 4, '200mOhm': 5, '632mOhm': 6, '2Ohm': 7, '6.32Ohm': 8, '20Ohm': 9, '63.2Ohm': 10, '200Ohm': 11, '632Ohm': 12, '2kOhm': 13, '6.32kOhm': 14, '20kOhm': 15, '63.2kOhm': 16, '200kOhm': 17, '632kOhm': 18, '2MOhm': 19, '6.32MOhm': 20, '20MOhm': 21, '63.2MOhm': 22}
},
'description': 'minimum range for software autorange',
'settable': True
},
'autorange': {
'datainfo': {'type': 'bool'},
'description': 'autorange',
'settable': True
},
'iexc': {
'datainfo': {
'type': 'enum',
'members': {'off': 0, '1pA': 1, '3.16pA': 2, '10pA': 3, '31.6pA': 4, '100pA': 5, '316pA': 6, '1nA': 7, '3.16nA': 8, '10nA': 9, '31.6nA': 10, '100nA': 11, '316nA': 12, '1uA': 13, '3.16uA': 14, '10uA': 15, '31.6uA': 16, '100uA': 17, '316uA': 18, '1mA': 19, '3.16mA': 20, '10mA': 21, '31.6mA': 22}
},
'description': 'current excitation',
'settable': True
},
'vexc': {
'datainfo': {
'type': 'enum',
'members': {'off': 0, '2uV': 1, '6.32uV': 2, '20uV': 3, '63.2uV': 4, '200uV': 5, '632uV': 6, '2mV': 7, '6.32mV': 8, '20mV': 9, '63.2mV': 10, '200mV': 11, '632mV': 12}
},
'description': 'voltage excitation',
'settable': True
},
'pause': {
'datainfo': {'unit': 's', 'min': 3.0, 'max': 60.0, 'type': 'double'},
'description': 'pause after channel change',
'settable': True, 'unit': 's'
},
'dwell': {
'datainfo': {'unit': 's', 'min': 1.0, 'max': 200.0, 'type': 'double'},
'description': 'dwell time with autoscan',
'settable': True, 'unit': 's'
},
'filter': {
'datainfo': {'unit': 's', 'min': 1.0, 'max': 200.0, 'type': 'double'},
'description': 'filter time', 'settable': True, 'unit': 's'},
'raw': {
'datainfo': {'unit': 'Ohm', 'type': 'double'},
'description': 'raw reistance value', 'unit': 'Ohm'
},
'tolerance': {
'datainfo': {'unit': 'K', 'min': 0.0, 'type': 'double'},
'description': 'absolute tolerance', 'settable': True, 'unit': 'K'
},
'settling_time':
{'datainfo': {'unit': 'sec', 'min': 0.0, 'type': 'double'},
'description': 'settling time', 'settable': True, 'unit': 'sec'
},
'timeout': {
'datainfo': {'unit': 'sec', 'min': 0.0, 'type': 'double'},
'description': 'timeout', 'settable': True, 'unit': 'sec'
},
'control_active': {
'datainfo': {'type': 'bool'},
'description': 'we are controlling'
},
'minheater': {
'datainfo': {'unit': 'A', 'min': 0.0, 'max': 0.01, 'type': 'double'},
'description': 'minimal heater current', 'settable': True, 'unit': 'A'
},
'htrrng': {
'datainfo': {
'type': 'enum',
'members': {'off': 0, '30uA': 1, '100uA': 2, '300uA': 3, '1mA': 4, '3mA': 5, '10mA': 6, '30mA': 7, '100mA': 8}
},
'description': '', 'settable': True
},
'ctrlpars': {
'datainfo': {
'type': 'struct',
'members': {
'p': {'min': -10.0, 'max': 10.0, 'type': 'double'},
'i': {'min': -10.0, 'max': 10.0, 'type': 'double'},
'd': {'min': -10.0, 'max': 10.0, 'type': 'double'}
}
},
'description': 'PID control parameters', 'settable': True
}
},
'commands_cfg': {
'stop': {
'description': 'set to idle when busy\n\ndoes not stop control!',
'datainfo': {'type': 'command'}
},
'control_off': {
'description': 'switch control off',
'datainfo': {'type': 'command'}
}
},
'mixins': [],
'secop_properties': {
'description': 'channel 1',
'implementation': 'frappy_psi.ls372.TemperatureLoop',
'interface_classes': ['Drivable'],
'features': []
},
'unit': 'K',
'fmtstr': '%g',
'value_datainfo': {'unit': 'K', 'min': 0.0, 'type': 'double'},
'target_datainfo': {'unit': 'K', 'min': 0.0, 'type': 'double'}
}
)

View File

@ -0,0 +1,50 @@
(
'nicos.devices.secop.devices.SecopReadable',
{
'secnode': 'se_main',
'description': 'mix. chamber temperature',
'secop_module': 'T_mix',
'params_cfg': {
'pollinterval_': {
'datainfo': {'unit': 's', 'min': 0.1, 'max': 120.0, 'type': 'double'},
'description': 'default poll interval',
'settable': True,
'unit': 's'
},
'raw': {
'datainfo': {'unit': 'Ohm', 'type': 'double'},
'description': 'raw value',
'unit': 'Ohm'
},
'filter_time': {
'datainfo': {'unit': 'sec', 'min': 1.0, 'max': 200.0, 'type': 'double'},
'description': 'filter time',
'settable': True,
'unit': 'sec'
},
'dwell_time': {
'datainfo': {'unit': 'sec', 'min': 1.0, 'max': 200.0, 'type': 'double'},
'description': 'dwell time',
'settable': True,
'unit': 'sec'
},
'pause_time': {
'datainfo': {'unit': 'sec', 'min': 3.0, 'max': 200.0, 'type': 'double'},
'description': 'pause time',
'settable': True,
'unit': 'sec'
}
},
'commands_cfg': {},
'mixins': [],
'secop_properties': {
'description': 'mix. chamber temperature',
'implementation': 'frappy_psi.triton.TemperatureSensor',
'interface_classes': ['Readable'],
'features': []},
'unit': 'K',
'fmtstr': '%g',
'value_datainfo': {'unit': 'K', 'type': 'double'}
}
)

336
influxdb.py Normal file
View File

@ -0,0 +1,336 @@
from influxdb_client import InfluxDBClient
from configparser import ConfigParser
import ast
from datetime import datetime
class InfluxDB:
"""
Class used to handle the connection with the InfluxDB instance
"""
def __init__(self):
config = ConfigParser()
config.read("./config/config.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)
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:
def __init__(self, db, influx_instrument_config):
self._influx_instrument_config = influx_instrument_config
self._bucket = self._influx_instrument_config["bucket"]
self._db = db
# ----- PUBLIC METHODS
def get_available_variables_at_time(self, time):
"""
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.
Parameters :
time (int) : the unix timestamp in seconds of the given point in time
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 can be the unit augmented with an index) and their unit.
"""
setup_info = self._get_setup_info_as_dict(time)
available_variables = self._extract_variables(setup_info)
available_variables = self._remove_variables_without_value_float(available_variables, time)
available_variables = self._set_variables_with_target(available_variables, time)
res = self._group_variables_by_unit(available_variables)
return res
def get_curves_in_timerange(self, variables, time):
"""
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.
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:
if variable.endswith(".target"):
variable_name_for_query = variable[:-len(".target")]
res[variable] = self._get_curve(variable_name_for_query, True, time)
else:
res[variable] = self._get_curve(variable, False, time)
return res
# ----- PRIVATE METHODS
def _get_setup_info_as_dict(self, time):
"""
Gets the value of the field setup_info in the measurement nicos/se_main as a Python dict.
Parameters
time (int) : the Unix timestamp in seconds we want to look the availability of variables for.
Returns :
{(str):((str), {...})} : the parsed "setup_info dict". 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.
"""
query = f"""
from(bucket: "{self._bucket}")
|> range(start: 0, stop: {time + 1})
|> filter(fn: (r) => r._measurement == "nicos/se_main")
|> filter(fn: (r) => r._field == "setup_info")
|> last()
|> yield(name: "res")
"""
tables = self._db.query(query)
res = ast.literal_eval(tables[0].records[0].get_value())
# TODO : what if 1. there is no record and 2. what if res is get_value() is empty (do we try to look for another way of getting the variables ?)
return res
def _extract_variables(self, setup_info_dict):
"""
Extracts relevant information out of the setup_info dict for each available variable.
Parameters :
setup_info_dict ({(str):((str), {...})}) : the parsed "setup_info dict". 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), "unit":(str), "has_potential_target":(bool)}] : 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, its unit and a boolean value indicating if the variable has a potential target available.
"""
available_varirables = [
{
"name":self._transform_setup_info_variable_name_to_influx(setup_info_variable_name),
"label":content[1]["secop_module"],
"unit":content[1]["unit"],
"has_potential_target": "target_datainfo" in content[1].keys()
}
for (setup_info_variable_name, content) in setup_info_dict.items() if content[0] != "nicos.devices.secop.devices.SecopDevice"
]
return available_varirables
def _transform_setup_info_variable_name_to_influx(self, setup_info_name):
"""
Transforms the name of the variable available in the setup_info dict into the Influx name.
Parameters :
setup_info_name (str) : the name of the variable in the setup_info dict.
Returns :
str : the transformed variable name that matches the Influx names reqauirements
"""
return self._influx_instrument_config["measurement_prefix"] + setup_info_name.lower()[len(self._influx_instrument_config["setup_info_prefix"]):]
def _remove_variables_without_value_float(self, available_variables, time):
"""
Removes some of the previously identified available_variables if they effectively do not have a value_float field in InfluxDB.
Parameters :
available_variables ([{"name":(str), "label":(str), "unit":(str), "has_potential_target":(bool)}]) : 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, its unit and a boolean value indicating if the variable has a potential target available.
time (int): the unix timestamp in seconds at which we want to get the available variables (for the target).
Returns :
[{"name":(str), "label":(str), "unit":(str), "has_potential_target":(bool)}] : an array of dictionnaries (updated), each containing the Influx name of the corresponding variable out of the setup_info dict,
the label to display in the Web GUI, its unit and a boolean value indicating if the variable has a potential target available.
"""
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
if "value_float" in [record.get_value() for record in records]:
res.append(variable)
return res
def _set_variables_with_target(self, available_variables, time):
"""
Determines if the previously identified available_variables have effectively a target or not (meaning it has a target_float field in Influx).
Parameters :
available_variables ([{"name":(str), "label":(str), "unit":(str), "has_potential_target":(bool)}]) : 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, its unit and a boolean value indicating if the variable has a potential target available.
time (int): the unix timestamp in seconds at which we want to get the available variables (for the target).
Returns :
[{"name":(str), "label":(str), "unit":(str), "has_potential_target":(bool)}] : 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, its unit and a boolean value indicating if the variable has a potential target available (updated).
"""
for variable in available_variables:
if variable["has_potential_target"]:
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
if not "target_float" in [record.get_value() for record in records]:
variable["has_potential_target"] = False
return available_variables
def _group_variables_by_unit(self, available_variables):
"""Performs a group by unit, while removing useless information and adding target curves.
Parameters :
available_variables ([{"name":(str), "label":(str), "unit":(str), "has_potential_target":(bool)}]) : 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, its unit and a boolean value indicating if the variable has a target available.
Returns :
[{"tag":(str), "unit":(str), "curves":[{"name":(str), }]]
"""
groups = {}
for available_variable in available_variables:
if available_variable["has_potential_target"]:
target_variable = self._get_formatted_target_variable(available_variable)
self._append_variable(groups, target_variable)
self._append_variable(groups, available_variable)
return list(groups.values())
def _get_formatted_target_variable(self, variable):
"""
Formats the variable which has a target to be added to the unit groups, meaning it adds ".target" as the suffix in the Influx name and label, and removes the "has_target" value.
Parameters :
variable ({"name":(str), "label":(str), "unit":(str), "has_potential_target":(bool)}) : a dictionnary containing the Influx name of the corresponding variable out of the setup_info dict,
the label to display in the Web GUI, its unit and a boolean value indicating if the variable has a target available.
Returns :
{"name":(str), "label":(str), "unit":(str)} : a dictionnary containing the Influx name of the corresponding variable out of the setup_info dict (augmented with ".target" suffix),
the label (augmented with ".target" suffix) to display in the Web GUI.
"""
return {
"name":variable["name"]+".target",
"label":variable["name"][len(self._influx_instrument_config["measurement_prefix"]):]+".target",
"unit":variable["unit"]
}
def _append_variable(self, groups, variable):
"""
Appends the variable in the unit group with a tag and a color, and creates the unit key if not available.
Parameters :
groups ({}) : a dictionnary that contains the curves grouped by unit, which will be updated
variable ({"name":(str), "label":(str), "unit":(str)[,"has_potential_target":(bool)]}) : a dictionnary containing the Influx name of the corresponding variable out of the setup_info dict,
the label to display in the Web GUI, its unit and possibly a boolean value indicating if the variable has a target available.
"""
if variable["unit"] not in groups.keys():
groups[variable["unit"]] = {"tag":variable["unit"], "unit":variable["unit"], "curves":[]}
groups[variable["unit"]]["curves"].append({
"name":variable["name"],
"label":variable["label"],
"color":""
})
def _get_curve(self, variable, is_target, time):
"""
Gets the points (curve) within a timerange for the given variable.
Parameters :
variable (str) : the name (Influx) of the variable we want the values of.
is_target (bool) : tells if the given variable is a target, or not (if variable is "nicos/se_t_chip.target", then is_target has to be set to True)
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)]] : an array of pairs (also arrays), the first value being the Unix timestamp in second (x), the seconds being the value (y)
"""
res = []
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 == "{"target_float" if is_target else "value_float"}")
|> keep(columns: ["_time","_value"])
|> yield(name: "res")
"""
tables = self._db.query(query)
for table in tables:
for record in table.records:
t = int(datetime.timestamp(record.get_time()))
value = record.get_value()
try:
value = PrettyFloat(value)
except:
value = None
res.append([t, value])
return self._insert_last_know_value(variable, is_target, res, time)
def _insert_last_know_value(self, variable, is_target, curve, time):
"""
Adds the last known value as the first point in the curve if the last known value is outside the viewing window.
Parameters :
variable (str) : the name (Influx) of the variable we want the values of.
is_target (bool) : tells if the given variable is a target, or not (if variable is "nicos/se_t_chip.target", then is_target has to be set to True)
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 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 == "{"target_float" if is_target else "value_float"}")
|> last()
|> keep(columns: ["_value"])
|> yield(name: "res")
"""
record = self._db.query(query)[0].records[0]
value = record.get_value()
try:
value = PrettyFloat(value)
except:
value = None
curve.insert(0, [time[0], value])
return curve

169
influxgraph.py Normal file
View File

@ -0,0 +1,169 @@
from influxdb import InfluxDB, InfluxDataGetter
import time
import logging
from colors import assign_colors_to_curves
import json
class InfluxGraph:
"""
Class implementing the logic of the different routes that are called by the client to retrieve graph data with InfluxDB.
Global constants :
HISTORICAL (int) : value that represents the "historical" visualisation mode, meaning that the most recent point is not in the visualisation window.
ACTUAL (int) : value that represents the "actual" visualisation mode, meaning that the curves stay in place, but new data is being added to the right (expansion in the future).
LIVE (int) : value that represents the "live" visualisation mode, meaning that the curves move with the most recent point always at the same place.
Attributes :
db (InfluxDB) : the InfluxDB instance that holds the connection with InfluxDB.
influx_data_getter (InfluxDataGetter) : the InfluxDataGetter instance that allows to get data out of InfluxDB.
livemode (int) : the type of visualisation the user is currently in. Can be HISTORICAL, ACTUAL or LIVE.
time ([(int)]) : an array of unix timestamp in seconds, where the first value is the last most recent requested point in time,
and the second value is the current most recent requested point in time.
lastvalues ({(str):((int), (float))}) : a dictionnary where the keys are the variable names, and the values are tuples, where the first
value is the unix timestamp of the most recent value known for this variable, and the second value its corresponding value
"""
HISTORICAL = 0
ACTUAL = 1
LIVE = 2
def __init__(self):
self.db = InfluxDB()
self.influx_data_getter = InfluxDataGetter(self.db, json.load(open("./graphs/lab4.json", "r"))["influx"])
self.livemode = self.HISTORICAL
self.time = [0, 0]
self.lastvalues = {}
def get_abs_time(self, times):
"""
Gets the absolute times for the given pontential relative times. If the given timestamps are less than one year, then the value is relative
and converted into an asbolute timestamps
Parameters :
times([(float)]) : an array of unix timestamps or relative duration (< 1 year) as floats
Returns :
[(float)] : an array of absolute unix timestamps as floats
"""
now = int(time.time() + 0.999)
oneyear = 365 * 24 * 3600
return [t + now if t < oneyear else t for t in times]
def strip_future(self, result):
"""
OLD : strip future points (happens only on dummy test_day)
Removes points more recent that the last requested point in time
Parameters :
result ({(str):[[(int),(float)]]}) : a dictionnary with the variable names as key, 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.
"""
# if self.livemode == self.LIVE:
for c in result.values():
while c:
lastt, lastx = c[-1]
if lastt <= self.time[1]:
break
c.pop()
def complete_to_end(self, result, endtime):
"""
Completes the data until the last requested point in time by adding the last known y-value at the end point.
Parameters :
result ({(str):[[(int),(float)]]}) : a dictionnary with the variable names as key, 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.
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] = (endtime, lastx)
def w_graph(self, variables, time="-1800,0"):
"""
Gets the curves given by variables in the time range "time"
Called when the route /graph is reached.
Parameters :
variables (str) : a comma separataed value 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 if they are lesser than one year.
Returns :
{"type":"graph-draw", "reduced":(bool), "graph":{(str):[[(int),(float)]]}} : a dictionnary with its "graph-draw" type (so it can be processed by the client), a "reduced" value
indicating if the data is reduced or not (meaning the data is sampled to be lighter for data viewing), and a "graph" dictionnary with the variable names as key, 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.
"""
time = [float(t) for t in time.split(',')]
start, end, now = self.get_abs_time(time + [0])
start, end, now = int(start), int(end), int(now)
self.time = [start, end]
self.variables = variables.split(',')
self.livemode = self.ACTUAL if end >= now else self.HISTORICAL
logging.info('LIVE %g %g %d %d', end, now, end >= now, self.livemode)
result = self.influx_data_getter.get_curves_in_timerange(self.variables, self.time)
self.strip_future(result)
self.complete_to_end(result, end)
self.time[0] = self.time[1]
# reduction not yet implemented
return dict(type='graph-draw', reduced=False, graph=result)
def w_gettime(self, time):
"""
Gets the server time for the give time.
Called when the route /gettime is reached.
Parameters :
time (str="-1800,0") : the given point in time represented by a string, which is a comma separated unix timestamp values list (in seconds). They are treated as relative from now if they are lesser than one year.
Returns :
{"type":"time", "time":(int)} : a dictionnary 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
"""
time = [float(t) for t in time.split(',')]
return dict(type='time', time= self.get_abs_time(time))
def w_getvars(self, time):
"""
Gets the curve names available at a given point in time.
Called when the route /getvars is reached.
Parameters :
time (str) : the given point in time represented by a string, which is a comma separated unix timestamp values list (in seconds). They are treated as relative from now if they are lesser than one year.
Returns :
{"type":"var_list", "blocks":[{"tag":(str),"unit":(str), "curves":[{"name":(str), "label":(str), "color":(str), "original_color":(str)}]}]} :
a dictionnary with its "var_list" type (so the data can be processed by the client) and the available curves with the name of the internal variable,
the color to display for this curve, its original color in SEA, grouped by their tag (which is a unit augmented with an index) and their unit ("in blocks")
"""
time = [float(t) for t in time.split(',')]
end_time = int(self.get_abs_time(time)[-1])
blocks = self.influx_data_getter.get_available_variables_at_time(end_time)
assign_colors_to_curves(blocks)
result = dict(type='var_list')
result['blocks'] = blocks
return result
def w_updategraph(self):
"""
TODO : method needed for live data update. See example in seaweb.py : SeaGraph
OLD : update live values - seems not to work
Sets the current visualisation mode to LIVE.
Called when the route /updategraph is reached.
Returns :
{"type":"accept-graph", "live": bool} : a dict with its "accept-graph" type and a "live" value telling if the server could change its visualization mode to live
"""
return dict(type='accept-graph', live=False)
def graphpoll(self):
"""
TODO : method needed for live data update. See example in seaweb.py : SeaGraph
"""
return None

View File

@ -25,9 +25,8 @@ import circularlog
import os
import signal
from influxgraph import InfluxGraph
class InfluxGraph:
"""to be imported"""
try: import simplejson as json
except ImportError: import json
@ -597,7 +596,7 @@ class SeaParams:
self.values = {}
self.consolepos = 0
self.id = uuid.uuid4().hex[0:15]
SeaGraph.__init__(self)
# SeaGraph.__init__(self)
self.queue = [dict(type='id', id=self.id, instrument=instrument.inst_name, device=instrument.device)]
def poll(self):
@ -663,11 +662,15 @@ class SeaParams:
class SeaClient(SeaParams, SeaGraph):
pass
def __init__(self):
SeaParams.__init__(self)
SeaGraph.__init__(self)
class SeaInfluxClient(SeaParams, InfluxGraph):
pass
def __init__(self):
SeaParams.__init__(self)
InfluxGraph.__init__(self)
class DummyClient(SeaGraph):