history, as of 2022-02-01

Change-Id: I725a57546df8f7c9e5ffe04eb98872f2a609efe4
This commit is contained in:
2022-02-02 09:52:01 +01:00
parent 8253fe471b
commit 05d0cfb193
3 changed files with 130 additions and 148 deletions

215
secop/historywriter.py Normal file
View File

@ -0,0 +1,215 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Markus Zolliker <markus.zolliker@psi.ch>
# *****************************************************************************
import time
from os.path import join
from frappyhistory.writer import Writer # pylint: disable=import-error
from secop.lib import clamp, formatExtendedTraceback
from secop.datatypes import IntRange, FloatRange, ScaledInteger,\
EnumType, BoolType, StringType, TupleOf, StructOf, ArrayOf, TextType
def make_cvt_list(dt, tail):
"""create conversion list
list of tuple (<conversion function>, <tail>, <curve options>)
tail is a postfix to be appended in case of tuples and structs
"""
if isinstance(dt, (IntRange, BoolType)):
return [(int, {'key': tail})]
if isinstance(dt, EnumType):
return [(int, {'key': tail, 'enum': dt.export_datatype()['members']})]
if isinstance(dt, (FloatRange, ScaledInteger)):
opts = {'key': tail}
if dt.unit:
opts['unit'] = dt.unit
opts['stepped'] = True
return [(dt.import_value, opts)]
if isinstance(dt, StringType):
opts = {'key': tail, 'kind': 'STR'}
if isinstance(dt, TextType):
opts['category'] = 'no'
else:
opts['category'] = 'string'
return [(lambda x: x, opts)]
if isinstance(dt, TupleOf):
result = []
for index, elmtype in enumerate(dt.members):
for fun, opts in make_cvt_list(elmtype, '%s.%s' % (tail, index)):
def conv(value, key=index, func=fun):
return func(value[key])
result.append((conv, opts))
return result
if isinstance(dt, ArrayOf):
result = []
for index in range(dt.maxlen):
for fun, opts in make_cvt_list(dt.members, '%s.%s' % (tail, index)):
opts['category'] = 'no'
def conv(value, key=index, func=fun):
return func(value[key])
result.append((conv, opts))
return result
if isinstance(dt, StructOf):
result = []
for subkey, elmtype in dt.members.items():
for fun, opts in make_cvt_list(elmtype, '%s.%s' % (tail, subkey)):
def conv(value, key=subkey, func=fun):
return func(value.get(key)) # None for missing struct key, should not be needed
result.append((conv, opts))
return result
return [] # other types (BlobType) are ignored: too much data, probably not used
class FrappyAbstractHistoryWriter:
"""abstract writer
doc only
"""
def put_def(self, key, kind='NUM', category='minor', **opts):
"""define or overwrite a new curve named <key> with options from dict <opts>
:param key: the key for the curve
:param kind: 'NUM' (default) for numeric values, 'STR' for strings
:param category: 'major' or 'minor': importance of curve
:param opts: a dict containing some of the following options
- label: a label for the curve in the chart
- unit: the physical unit
- group: grouping of the curves in charts (unit by default)
- stepped: lines in charts should be drawn as stepped line. Only applicable when kind='NUM'
True by default.
"""
raise NotImplementedError()
def put(self, timestamp, key, value):
"""add a data point
:param timestamp: the timestamp. must not decrease!
:param key: the curve name
:param value: the value to be stored (number or string), None indicates un undefined value
"""
raise NotImplementedError()
def close(self, timestamp):
"""close the writer
:param timestamp:
indicate to the writer that all values are getting undefined after <timestamp>
"""
raise NotImplementedError()
class FrappyHistory(Writer):
def __init__(self, history_path, modules, logger):
minsteps = {} # generalConfig.mintimesteps
super().__init__(history_path, logger, minsteps=minsteps)
self.__init_time = time.time()
self.__last_time = self.__init_time
# bad_timestep = set()
for modname, modobj in modules.items():
# if isinstance(modobj, HasComlog):
# modobj.enableComlog(join(history_path, 'comlog'))
for pname, pobj in modobj.parameters.items():
key = '%s:%s' % (modname, pname)
dt = pobj.datatype
cvt_list = make_cvt_list(dt, key)
given_opts = pobj.history
if isinstance(given_opts, dict):
given_opts = [given_opts] * len(cvt_list)
if pname == 'value':
for _, opts in cvt_list:
opts['key'] = opts['key'].replace(':value', '')
if pname == 'status':
# default labels '<modname>:status' and '<modname>:status_text'
for lbl, (_, opts) in zip([key, key + '_text'], cvt_list):
opts['label'] = lbl
label_set = set()
cvt_filtered = []
for given, (cvt_func, opts) in zip(given_opts, cvt_list):
result = dict(opts, **given)
stepped = result.pop('stepped', None)
if opts.get('stepped'): # True on floats
if pobj.readonly or stepped is False:
result['stepped'] = False
cat = given.get('category')
if cat is None:
if not pobj.export:
continue
cat = result.get('category')
if cat is None:
if pname in ('value', 'target'):
result['category'] = 'major'
elif pobj.readonly:
result['category'] = 'minor'
else:
result['category'] = 'param'
if cat == 'no':
continue
label = result.pop('label', None)
if label and label not in label_set:
result['label'] = label
label_set.add(label)
cvt_filtered.append((cvt_func, result))
# if result.get('timestep', 1) < minstep:
# bad_timestep.add('%s:%s' % (modname, pname))
self.put_def(**result)
if cvt_filtered:
def callback(value, p=pobj, history=self, cvt=cvt_filtered):
if self.__init_time:
t = self.__init_time # on initialisation, use the same timestamp for all
else:
t = p.timestamp
if t:
# make sure time stamp is not decreasing, as a potentially decreasing
# value might bring the reader software into trouble
t = clamp(self.__last_time, t, time.time())
else:
t = time.time()
history.__last_time = t
if pobj.readerror: # error update
for _, opts in cvt:
self.put(t, opts['key'], None)
else:
for fun, opts in cvt:
self.put(t, opts['key'], fun(value))
modobj.valueCallbacks[pname].append(callback)
modobj.errorCallbacks[pname].append(callback)
# if bad_timestep:
# if minstep < 1:
# logger.error('timestep < generalConfig.mintimestep')
# else:
# logger.error('timestep < 1, generalConfig.mintimestep not given?')
# logger.error('parameters: %s', ', '.join(bad_timestep))
#
self.__init_time = None
def close(self):
super().close(max(self.__last_time, time.time()))

View File

@ -26,7 +26,7 @@
import inspect
from secop.datatypes import BoolType, CommandType, DataType, \
DataTypeType, EnumType, IntRange, NoneOr, OrType, \
DataTypeType, EnumType, IntRange, NoneOr, OrType, FloatRange, \
StringType, StructOf, TextType, TupleOf, ValueType, ArrayOf
from secop.errors import BadValueError, ProgrammingError
from secop.properties import HasProperties, Property
@ -94,6 +94,11 @@ class Accessible(HasProperties):
return '%s(%s)' % (self.__class__.__name__, ', '.join(props))
historyStruct = StructOf(category=StringType(), label=StringType(), group=StringType(),
stepped=OrType(BoolType(), StringType()), timestep=FloatRange(0, 1),
record_unchanged=BoolType())
class Parameter(Accessible):
"""defines a parameter
@ -163,52 +168,35 @@ class Parameter(Accessible):
default None: write if given in config''', NoneOr(BoolType()),
export=False, default=None, settable=False)
history_category = Property(
'''[custom] category for history
major: should be shown by default in a history chart, default for value and target
minor: to be shown optionally in a history chart, default for other parameters
no: history is not saved. default for TextType and ArrayOf
history = Property(
'''[custom] options for history
category is ignored (forced to no) for BlobType
for structured types, this is an array of options, to be applied in the order
of the created elements.
For structured types, the category may be a comma separated list, overwriting the
default for the first or all curves.
If it does not contain a comma, it applies for all curves
list of options:
category
- major: should be shown by default in a history chart, default for value and target
- minor: to be shown optionally in a history chart, default for other parameters
- no: history is not saved. default for TextType and ArrayOf
category is ignored (forced to no) for BlobType
label
default: <modname>:<parname> or <modname> for main value
group:
default: unit
stepped:
whether a curve has to be drawn stepped or connected.
default: True when readonly=False, else False
timestep:
the desired time step for the curve storage. maximum and default value is 1 sec
''',
NoneOr(StringType()), export=True, default=None, settable=False)
history_label = Property(
'''[custom] label for history
default: <modname>:<parname> or <modname> for main value
For structured types, the label may be a comma separated list, overwriting the
default for the first or all curves.
If it does not contain a comma, it applies for the first curve only.
''',
NoneOr(StringType()), export=True, default=None, settable=False)
history_group = Property(
'''[custom] group for history
default: unit
For structured types, the group may be a comma separated list, overwriting the
default for the first or all curves. If it does not contain a comma, it is
applies for all curves.
''',
NoneOr(StringType()), export=True, default=None, settable=False)
history_stepped = Property(
'''[custom] stepped curve
Whether a curve has to be drawn stepped or connected.
default: True when readonly=False, else False
Applicable to FloatRange and ScaledInteger only, other types are stepped by definition.
For structured types, stepped may be a list, overwriting the default for the
first or all curves. If not, applieas to all curves.
''',
OrType(BoolType(), ArrayOf(BoolType())), export=True, default=False, settable=False)
OrType(historyStruct, ArrayOf(historyStruct)), export=True, default={}, settable=False)
# used on the instance copy only
value = None

View File

@ -114,6 +114,7 @@ class Server:
self.log.warning('ambiguous sections in %s: %r' % (cfgfiles, tuple(ambiguous_sections)))
self._cfgfiles = cfgfiles
self._pidfile = os.path.join(cfg['piddir'], name + '.pid')
self.close_callbacks = []
def loadCfgFile(self, cfgfile):
if not cfgfile.endswith('.cfg'):
@ -210,8 +211,7 @@ class Server:
self.interface.serve_forever()
except KeyboardInterrupt as e:
self._restart = False
self.dispatcher.close()
self.interface.server_close()
self.close()
if self._restart:
self.restart_hook()
self.log.info('restart')
@ -227,6 +227,12 @@ class Server:
self._restart = False
self.interface.shutdown()
def close(self):
self.dispatcher.close()
self.interface.server_close()
for cb in self.close_callbacks:
cb()
def _processCfg(self):
errors = []
opts = dict(self.node_cfg)
@ -338,8 +344,13 @@ class Server:
self.log.info('all modules and pollers started')
history_path = os.environ.get('FRAPPY_HISTORY')
if history_path:
from secop_psi.historywriter import add_writer # pylint: disable=import-outside-toplevel
add_writer(history_path, self)
try:
from secop.historywriter import FrappyHistory # pylint: disable=import-outside-toplevel
history = FrappyHistory(history_path, self.modules, self.log.getChild('history'))
self.close_callbacks.append(history.close)
except ImportError:
raise
self.log.warning('FRAPPY_HISTORY is defined, but frappyhistory package not available')
# TODO: if ever somebody wants to implement an other history writer:
# - a general config file /etc/secp/secop.conf or <frappy repo>/etc/secop.conf
# might be introduced, which contains the log, pid and cfg directory path and