#  -*- coding: utf-8 -*-
# *****************************************************************************
# NICOS, the Networked Instrument Control System of the MLZ
# Copyright (c) 2009-2022 by the NICOS contributors (see AUTHORS)
#
# 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:
#   <NAME> <<EMAIL>>
#
# *****************************************************************************

"""Base device classes for usage in NICOS."""

import inspect
import re
from time import time as currenttime

import numpy

from nicos import session
from nicos.core import status
from nicos.core.constants import MASTER, POLLER, SIMULATION, SLAVE
from nicos.core.errors import AccessError, CacheLockError, \
    CommunicationError, ConfigurationError, InvalidValueError, LimitError, \
    ModeError, MoveError, NicosError, PositionError, ProgrammingError, \
    UsageError
from nicos.core.mixins import DeviceMixinMeta, HasLimits, HasOffset, \
    HasPrecision, HasTimeout, IsController
from nicos.core.params import INFO_CATEGORIES, Attach, Override, Param, \
    Value, anytype, dictof, floatrange, listof, nicosdev, none_or, oneof, \
    setof, tupleof
from nicos.core.utils import formatStatus, multiStatus, multiStop, multiWait, \
    statusString, usermethod
from nicos.protocols.cache import FLAG_NO_STORE
from nicos.utils import getVersions, loggers, number_types, parseDateString

ALLOWED_CATEGORIES = {v[0] for v in INFO_CATEGORIES}


def requires(**access):
    """Decorator to implement user access control.

    The access is checked based on the keywords given.  Currently, the
    keywords with meaning are:

    * ``'level'``: gives the minimum required user access level and can
      have the values ``GUEST``, ``USER`` or ``ADMIN`` as defined in the
      :mod:`nicos.core.utils` module.
    * ``'mode'``: gives the required exection mode ("master", "slave",
      "maintenance", "simulation").
    * ``'passcode'``: only usable in the interactive console: gives a
      passcode that the user has to type back.

    A special keyword is ``'helpmsg'``; if the access check fails, this gives
    a message that is appended to the error message.

    The wrapper function calls `.Session.checkAccess` to verify the
    requirements.  If the check fails, `.AccessError` is raised.
    """
    def decorator(func):
        def new_func(*args, **kwds):
            try:
                session.checkAccess(access)
            except AccessError as err:
                msg = 'cannot do %s: %s' % (func.__name__, err)
                if 'helpmsg' in access:
                    msg += ' (%s)' % access['helpmsg']
                if args and isinstance(args[0], Device):
                    raise AccessError(args[0], msg) from None
                raise AccessError(msg) from None
            return func(*args, **kwds)
        new_func.__name__ = func.__name__
        new_func.__doc__ = func.__doc__
        new_func.real_func = func
        return new_func
    return decorator


class DeviceMeta(DeviceMixinMeta):
    """
    A metaclass that automatically adds properties for the class' parameters,
    and determines a list of user methods.

    It also merges these attributes defined in the class with those defined in
    all base classes:

    * attached_devices
    * parameters
    * parameter_overrides
    * methods (autogenerated)
    """

    def __new__(mcs, name, bases, attrs):
        # check base class ordering: mixins before others
        seen_nonmixin = False
        for base in bases:
            if not isinstance(base, DeviceMeta) and seen_nonmixin:
                raise ProgrammingError('%s.%s device: mixin class %s must come'
                                       ' before all non-mixins in the base'
                                       ' class list' % (attrs['__module__'],
                                                        name, base.__name__))
            if isinstance(base, DeviceMeta):
                seen_nonmixin = True
        newtype = DeviceMixinMeta.__new__(mcs, name, bases, attrs)

        # to debug MRO problems you could use this line
        # print('MRO:', newtype, newtype.mro())
        for adevname, entry in list(newtype.attached_devices.items()):
            # adev names are always lowercased
            if adevname != adevname.lower():
                raise ProgrammingError('%r device: attached device name %r is '
                                       'not all-lowercase' % (name, adevname))
            if not isinstance(entry, Attach):
                raise ProgrammingError("Please use 'Attach' to define an "
                                       "attached device.")

        # create parameter properties
        for param, info in newtype.parameters.items():
            # parameter names are always lowercased (enforce this)
            if param != param.lower():
                raise ProgrammingError('%r device: parameter name %r is not '
                                       'all-lowercase' % (name, param))
            if not isinstance(info, Param):
                raise ProgrammingError('%r device %r parameter info should be '
                                       'a Param object' % (name, param))
            if param in ('value', 'status'):
                raise ProgrammingError('%r device: parameter names "value" '
                                       'and "status" are reserved and cannot '
                                       'be used' % (name, ))

            # process overrides
            override = newtype.parameter_overrides.get(param)
            if override:
                info = newtype.parameters[param] = override.apply(info)

            # create the getter method
            if not info.volatile:
                def getter(self, param=param):
                    if param not in self._params:
                        self._initParam(param)
                    if self._cache and param != 'name':  # no renaming !
                        value = self._cache.get(self, param, Ellipsis)
                        if value is not Ellipsis:
                            self._params[param] = value
                            return value
                    return self._params[param]
            else:
                rmethod = getattr(newtype, 'doRead' + param.title(), None)
                if rmethod is None:
                    raise ProgrammingError('%r device %r parameter is marked '
                                           'as "volatile=True", but has no '
                                           'doRead%s method' %
                                           (name, param, param.title()))

                def getter(self, param=param, rmethod=rmethod):
                    if self._mode == SIMULATION:
                        return self._initParam(param)
                    value = rmethod(self)
                    if value == self._params.get(param, Ellipsis):
                        return value
                    if self._cache:
                        self._cache.put(self, param, value)
                    self._params[param] = value
                    return value

            # create the setter method
            if not info.settable:
                def setter(self, value, param=param):
                    raise ConfigurationError(
                        self, 'the %s parameter can only be changed in the '
                        'setup file' % param)
            else:
                wmethod = getattr(newtype, 'doWrite' + param.title(), None)
                umethod = getattr(newtype, 'doUpdate' + param.title(), None)

                def setter(self, value, param=param, wmethod=wmethod,
                           umethod=umethod, chatty=info.chatty):
                    value = self._validateType(value, param)
                    if getattr(self, 'requires', None):  # for Moveables
                        try:
                            session.checkAccess(self.requires)
                        except AccessError as err:
                            raise AccessError(self, 'cannot set parameter: %s'
                                              % err) from None
                    if self._mode == SLAVE:
                        raise ModeError('setting parameter %s not possible in '
                                        'slave mode' % param)
                    elif self._mode == SIMULATION:
                        if wmethod and not self._sim_intercept:
                            rv = wmethod(self, value)
                            if rv is not None:
                                value = rv
                        if umethod:
                            umethod(self, value)
                        if chatty:
                            oldvalue = self._params[param]
                            if value != oldvalue:
                                valuestr = self.formatParam(param, value)
                                oldvstr = self.formatParam(param, oldvalue)
                                self.log.info('%s set to %s (was %s)',
                                              param, valuestr, oldvstr)
                        self._params[param] = value
                        return
                    oldvalue = getattr(self, param)
                    if wmethod:
                        # allow doWrite to override the value
                        rv = wmethod(self, value)
                        if rv is not None:
                            value = rv
                    if umethod:
                        umethod(self, value)
                    if chatty:
                        if value != oldvalue:
                            valuestr = self.formatParam(param, value)
                            oldvstr = self.formatParam(param, oldvalue)
                            self.log.info('%s set to %s (was %s)',
                                          param, valuestr, oldvstr)
                    self._params[param] = value
                    if self._cache:
                        self._cache.put(self, param, value)

            # create a property and attach to the new device class
            setattr(newtype, param,
                    property(getter, setter, doc=info.formatDoc()))
        del newtype.parameter_overrides
        if 'parameter_overrides' in attrs:
            del attrs['parameter_overrides']
        if 'valuetype' in attrs:
            newtype.valuetype = staticmethod(attrs['valuetype'])

        # check attribute names for the attached devices (created by
        # Device.__init__)
        for adevname, entry in newtype.attached_devices.items():
            pname = '_attached_%s' % adevname
            if newtype.parameters and (pname in newtype.parameters):
                raise ProgrammingError('%r device: attached device helper '
                                       'property %r collides with parameter' %
                                       (name, pname))
            if pname in attrs:
                raise ProgrammingError('%r device: attached device helper '
                                       'property %r collides with attribute' %
                                       (name, pname))

        return newtype


class Device(metaclass=DeviceMeta):
    """
    An object that has a list of parameters that are read from the
    configuration and have default values.

    Subclasses *can* implement:

    * doPreinit()
    * doInit()
    * doShutdown()
    * doVersion()
    * doInfo()
    """

    __mergedattrs__ = ['parameters', 'parameter_overrides', 'attached_devices',
                       'methods']

    # A dictionary mapping device names to classes (or lists of classes) that
    # describe this device's attached (subordinate) devices.
    attached_devices = {}

    # A dictionary mapping parameter names to parameter descriptions, given as
    # Param objects.
    parameters = {
        'name':        Param('The name of the device',
                             type=str, settable=False, userparam=False),
        'classes':     Param('Names of device class and all its base classes',
                             type=listof(str), settable=False, userparam=False),
        'description': Param('A description of the device', type=str,
                             settable=True),
        'visibility':  Param('Selects in which context the device should be '
                             'shown/included',
                             type=setof('metadata', 'namespace', 'devlist'),
                             default=['metadata', 'namespace', 'devlist'],
                             settable=False),
        # Don't allow setting loglevel to > INFO, as it could be confusing.
        'loglevel':    Param('The logging level of the device',
                             type=oneof('debug', 'info'),
                             default='info', settable=True, preinit=True),
    }

    _ownparams = {'name'}

    # A dictionary mapping parameter names to Override objects that override
    # specific properties of parameters found in base classes.
    parameter_overrides = {}

    # Set this to True on devices that are only created for a time, and whose
    # name can be reused.
    temporary = False

    # Set this to True on devices that directly access hardware, and therefore
    # should have their actions simulated.
    hardware_access = True

    # This is set by NICOS to indicate that do-methods should be intercepted
    # and their result simulated.  Combines hardware_access and device mode
    # at runtime.
    _sim_intercept = True

    # Autogenerated inventory of the class' user methods.
    methods = {}

    # Loop delay defaults.  To be set to low values in the test suite.
    _base_loop_delay = 0.1
    _long_loop_delay = 0.5

    def __init__(self, name, **config):
        # register self in device registry
        if not self.temporary:
            if name in session.devices:
                raise ProgrammingError('device with name %s already exists' %
                                       name)
            session.devices[name] = self
            session.device_case_map[name.lower()] = name

        self._name = name
        # _config: device configuration (all parameter names lower-case)
        self._config = {name.lower(): value for (name, value) in config.items()}
        # _params: parameter values from config
        self._params = {'name': name}
        # _infoparams: cached list of parameters to get on info()
        self._infoparams = []
        # _adevs: "attached" device instances
        self._adevs = {}
        # superdevs: reverse adevs for dependency tracking
        self._sdevs = set()
        # keep an explicit record of controllers
        self._controllers = set()
        # execution mode
        self._mode = session.mode

        # initialize a logger for the device
        self.__dict__['log'] = session.getLogger(name)

        try:
            # initialize device
            self.init()
        except BaseException as err:
            try:
                self.shutdown()
            except Exception:
                self.log.warning('could not shutdown after creation failed',
                                 exc=1)
            raise err

    attribute_whitelist = {
        'valuetype',  # for all devices
        'arraydesc',  # for image producers
        'autodevices',  # for HasAutoDevices
    }

    def __setattr__(self, name, value):
        # disallow modification of any public attributes that are not
        # parameters or otherwise properties
        if name[0] != '_' and name not in self.attribute_whitelist:
            obj = getattr(self.__class__, name, Ellipsis)
            if obj is Ellipsis:
                raise UsageError(self, 'device has no parameter %s, use '
                                 'ListParams(%s) to show all' % (name, self))
            elif inspect.isroutine(obj):
                raise UsageError(self, '%s cannot be assigned; it is a device '
                                 'method' % name)
            elif not isinstance(obj, property):
                # this should also be forbidden at some point, but for now just
                # emit a warning to make sure it doesn't break the world
                self.log.warning('Setting a non-parameter attribute %s.%s: '
                                 'this is deprecated and will be an error '
                                 'in a future version.', self, name)
        object.__setattr__(self, name, value)

    def __str__(self):
        return self._name

    def __repr__(self):
        if not self.description:
            return '<device %s (a %s.%s)>' % (self._name,
                                              self.__class__.__module__,
                                              self.__class__.__name__)
        return '<device %s "%s" (a %s.%s)>' % (self._name,
                                               self.description,
                                               self.__class__.__module__,
                                               self.__class__.__name__)

    def __reduce__(self):
        # Used for pickling the device e.g. when sending between daemon and GUI
        return (str, (self._name,))

    def doReadName(self):
        return self._name

    def doReadClasses(self):
        return [c.__module__ + '.' + c.__name__ for c in self.__class__.__mro__]

    def doUpdateLoglevel(self, value):
        if session.sessiontype == POLLER:
            # suppress debug/info messages from ordinary devices in the poller
            self.log.setLevel(loggers.WARNING)
        else:
            self.log.setLevel(loggers.loglevels[value])

    def _attachDevices(self):
        """Validate and create attached devices."""
        for aname, entry in sorted(self.attached_devices.items()):
            if not isinstance(entry, Attach):
                raise ProgrammingError(self, 'attached device entry for %r is '
                                       'invalid; the value should be a '
                                       'nicos.core.Attach object' % aname)
            value = self._config.pop(aname, None)
            devlist = []
            class_needed = entry.devclass
            if self._mode == SIMULATION:
                # need to relax this instance check for simulation mode;
                # aliases are not yet set correctly when the devices are
                # created
                class_needed = object

            for i, devname in enumerate(entry.check(self, aname, value)):
                if (devname is None) or \
                   (devname not in session.configured_devices and entry.missingok):
                    devlist.append(None)
                    continue
                try:
                    dev = session.getDevice(devname, class_needed, source=self)
                except UsageError:
                    raise ConfigurationError(
                        self, 'device %r item %d has wrong type (should be %s)' %
                        (aname, i + 1, entry.devclass.__name__)) from None
                devlist.append(dev)

            for dev in devlist:
                if dev is not None:
                    dev._sdevs.add(self._name)
                    if isinstance(self, IsController):
                        dev._controllers.add(self._name)
            self.__dict__['_attached_%s' % aname] = self._adevs[aname] = \
                devlist[0] if entry.single else devlist

    def init(self):
        """Initialize the object; this is called by the NICOS system when the
        device instance has been created.

        This method first initializes all attached devices (creating them if
        necessary), then initializes parameters.

        .. XXX expand parameter init procedure

        .. method:: doPreinit(mode)

           This method, if present, is called before parameters are initialized
           (except for parameters that have the ``preinit`` property set to
           true).

           This allows to initialize a hardware connection if it is necessary
           for the various ``doRead...()`` methods of other parameters that
           read the current parameter value from the hardware.

        .. method:: doInit(mode)

           This method, if present, is called after all parameters have been
           initialized.  It is the correct place to set up additional
           attributes, or to perform initial (read-only!) communication with
           the hardware.

        .. note:: ``doPreinit()`` and ``doInit()`` are called regardless of the
           current execution mode.  This means that if one of these methods
           does hardware access, it must be done only if ``mode != SIMULATION``.
        """
        self._cache = None
        self._subscriptions = []

        self._attachDevices()

        self._cache = self._getCache()
        lastconfig = None
        if self._cache:
            lastconfig = self._cache.get('_lastconfig_', self._name, None)
            old_classes = self._cache.get(self, 'classes')
            new_classes = self.doReadClasses()
            if old_classes != new_classes and self._mode == MASTER:
                self._cache.put(self, 'classes', new_classes)

        def _init_param(param, paraminfo):
            param = param.lower()
            # mandatory parameters must be in config, regardless of cache
            if paraminfo.mandatory and param not in self._config:
                raise ConfigurationError(self, 'missing configuration '
                                         'parameter %r' % param)
            # Ellipsis representing "no value" since None is a valid value for
            # some parameters
            value = Ellipsis
            # try to get from cache
            if self._cache:
                value = self._cache.get(self, param, Ellipsis)
                if param == 'name':  # clean up legacy, wrong values
                    self._cache.put(self, 'name', self._name)
                    value = self._name
                if value is not Ellipsis:
                    try:
                        value = self._validateType(value, param, paraminfo)
                    except ConfigurationError as e:
                        self.log.warning('value of %s from cache (%r) is '
                                         'invalid: %r using default handling '
                                         'instead.', param, value, e)
                        value = Ellipsis
            if value is not Ellipsis:
                if param in self._ownparams:
                    self._params[param] = value
                    return
                if param in self._config:
                    cfgvalue = self._validateType(self._config[param], param)
                    if cfgvalue != value:
                        valuestr = self.formatParam(param, value)
                        cfgvstr = self.formatParam(param, cfgvalue)
                        prefercache = paraminfo.prefercache
                        if prefercache is None:
                            prefercache = paraminfo.settable
                        if lastconfig and lastconfig.get(param) != cfgvalue:
                            self.log.warning(
                                "value of '%s' from cache (%s) differs from "
                                'configured value (%s), using configured '
                                'since it was changed in the setup file',
                                param, valuestr, cfgvstr)
                            value = cfgvalue
                            self._cache.put(self, param, value)
                        elif prefercache:
                            self.log.warning(
                                "value of '%s' from cache (%s) differs from "
                                'configured value (%s), using cached',
                                param, valuestr, cfgvstr)
                        else:
                            self.log.warning(
                                "value of '%s' from cache (%s) differs from "
                                'configured value (%s), using configured',
                                param, valuestr, cfgvstr)
                            value = cfgvalue
                            self._cache.put(self, param, value)
                elif not paraminfo.settable and paraminfo.prefercache is False:
                    # parameter is in cache, but not in config: if it is not
                    # settable and has a default, use that (since most probably
                    # the default is intended to be used but has changed)
                    defvalue = paraminfo.default
                    if defvalue != value:
                        defvalue = self._validateType(defvalue, param,
                                                      paraminfo)
                    if defvalue != value:
                        valuestr = self.formatParam(param, value)
                        defvstr = self.formatParam(param, paraminfo.default)
                        self.log.warning(
                            "value of '%s' from cache (%s) differs from "
                            'default value (%s), using default',
                            param, valuestr, defvstr)
                        value = paraminfo.default
                        self._cache.put(self, param, value)
                umethod = getattr(self, 'doUpdate' + param.title(), None)
                if umethod:
                    umethod(value)
                self._params[param] = value
            else:
                self._initParam(param, paraminfo)
                notfromcache.append(param)

            if param in self._config and paraminfo.internal:
                self.log.warning(
                    "'%s' is configured in a setup file although "
                    "declared as internal parameter",
                    param
                )

            if paraminfo.category is not None:
                if paraminfo.category not in ALLOWED_CATEGORIES:
                    self.log.error('parameter %s uses illegal category %r!',
                                   param, paraminfo.category)
                else:
                    self._infoparams.append((paraminfo.category, param,
                                             paraminfo.unit))
            # end of _init_param()

        notfromcache = []
        later = []

        for param, paraminfo in self.parameters.items():
            if paraminfo.preinit:
                _init_param(param, paraminfo)
            else:
                later.append((param, paraminfo))

        if hasattr(self, 'doPreinit'):
            self.doPreinit(self._mode)

        for param, paraminfo in later:
            _init_param(param, paraminfo)

        # warn about parameters that weren't present in cache
        if self._cache and notfromcache:
            self.log.info('these parameters were not present in cache: %s',
                          ', '.join(notfromcache))

        self._infoparams.sort()

        # subscribe to parameter value updates, if a doUpdate method exists
        if self._cache:
            for param in self.parameters:
                umethod = getattr(self, 'doUpdate' + param.title(), None)
                if umethod:
                    def updateparam(key, value, time, umethod=umethod):
                        umethod(value)
                    self._cache.addCallback(self, param, updateparam)
                    self._subscriptions.append((param, updateparam))

        if self._cache:
            self._cache.put('_lastconfig_', self._name, self._config)

        # call custom initialization
        if hasattr(self, 'doInit'):
            self.doInit(self._mode)

    def _getCache(self):
        """Indirection needed by the Cache client itself."""
        return session.cache

    def _validateType(self, value, param, paraminfo=None):
        """Validate and coerce the value of a parameter to the correct type.

        If the value can't be coerced, a ConfigurationError is raised.
        """
        paraminfo = paraminfo or self._getParamConfig(param)
        try:
            value = paraminfo.type(value)
        except (ValueError, TypeError) as err:
            raise ConfigurationError(
                self, '%r is an invalid value for parameter %s: %s' % (
                    value, param, err)) from err
        return value

    def _initParam(self, param, paraminfo=None):
        """Get an initial value for the parameter, called when the cache
        doesn't contain such a value.

        If present, a doReadParam method is called.  Otherwise, the value comes
        from either the setup file or the device-specific default value.
        """
        paraminfo = paraminfo or self.parameters[param]
        rmethod = getattr(self, 'doRead' + param.title(), None)
        umethod = getattr(self, 'doUpdate' + param.title(), None)
        done = False
        # try to read from the hardware (only in non-simulation mode)
        if not self._sim_intercept and rmethod:
            try:
                value = rmethod()
            except NicosError:
                self.log.warning('could not read initial value for parameter '
                                 '%s from device', param)
            else:
                done = True
        if not done and param in self._params:
            # happens when called from a param getter, not from init()
            value = self._params[param]
        elif not done:
            value = self._config.get(param, paraminfo.default)
        value = self._validateType(value, param, paraminfo)
        if self._cache:  # will not be there in simulation mode
            self._cache.put(self, param, value)
        # always call update methods, they should be working for simulation
        if umethod:
            umethod(value)
        self._params[param] = value
        return value

    def _setROParam(self, param, value):
        """Set an otherwise read-only parameter.

        This is useful for parameters that change at runtime, but indirectly,
        such as "last filenumber".
        """
        value = self._validateType(value, param)
        self._params[param] = value
        if self._cache:
            self._cache.put(self, param, value)

    def _getParamConfig(self, param):
        """Return the entry for the parameter from self.parameters.

        This should be used when alias resolution is desired, since it is
        overridden for DeviceAliases.
        """
        return self.parameters[param]

    def _getFromCache(self, name, func, maxage=None):
        """Get *name* from the cache, or call *func* if outdated/not present.

        If the *maxage* parameter is set, do not allow the value to be older
        than that amount of seconds.
        """
        if not self._cache:
            return func()
        val = Ellipsis
        if maxage != 0:
            val = self._cache.get(
                self, name, Ellipsis,
                mintime=currenttime() - maxage if maxage is not None else 0)
        if val is Ellipsis:
            defmaxage = getattr(self, 'maxage', None)
            val = func(defmaxage if maxage is None else maxage)
            self._cache.put(self, name, val, currenttime(), defmaxage)
        return val

    def formatParam(self, param, value, use_repr=True):
        """Format a parameter value according to its fmtstr."""
        if isinstance(value, list):
            value = tuple(value)
        fmtstr = self._getParamConfig(param).fmtstr
        if fmtstr == '%r' and not use_repr:
            fmtstr = '%s'
        if fmtstr == 'main':
            if isinstance(value, tuple):
                fmtstr = '(' + ', '.join((self.fmtstr,) * len(value)) + ')'
            else:
                fmtstr = self.fmtstr
        try:
            ret = fmtstr % value
        except (TypeError, ValueError):
            ret = repr(value)
        return ret

    def _setMode(self, mode):
        """Set a new execution mode."""
        self._mode = mode
        if mode == SIMULATION:
            # switching to simulation mode: remove cache entirely
            # and rely on saved _params and values
            self._cache = None

    def history(self, name='value', fromtime=None, totime=None):
        """Return a history of the parameter *name* (can also be ``'value'`` or
        ``'status'``).

        *fromtime* and *totime* can be used to limit the time window.  They can
        be:

        * positive numbers: interpreted as UNIX timestamps
        * negative numbers: interpreted as hours back from now
        * strings: in one of the formats 'HH:MM', 'HH:MM:SS',
          'YYYY-MM-DD', 'YYYY-MM-DD HH:MM' or 'YYYY-MM-DD HH:MM:SS'

        Default is to query the values of the last hour.
        """
        if not self._cache:
            # no cache is configured for this setup
            return []
        else:
            if fromtime is None:
                fromtime = -1
            if isinstance(fromtime, str):
                fromtime = parseDateString(fromtime)
            elif fromtime < 0:
                fromtime = currenttime() + fromtime * 3600
            if totime is None:
                totime = currenttime()
            elif isinstance(totime, str):
                totime = parseDateString(totime, enddate=True)
            elif totime < 0:
                totime = currenttime() + totime * 3600
            return self._cache.history(self, name, fromtime, totime)

    def info(self):
        """Return "device information" as an iterable of tuples ``(name,
        raw_value, formatted_value, unit, category)``.

        This "device information" is put into data files and should therefore
        include any parameters that will be essential to record the current
        status of the instrument.

        The default implementation already collects all parameters whose
        ``category`` property is set.

        .. method:: doInfo()

           This method can add more device information by returning it as a
           sequence of tuples.
        """
        ret = []
        if hasattr(self, 'doInfo') and self._mode != SIMULATION:
            ret.extend(self.doInfo())
        selfunit = getattr(self, 'unit', '')
        for category, name, unit in self._infoparams:
            try:
                parvalue = self._getFromCache(
                    name, lambda _maxage=None: getattr(self, name))
            except Exception as err:
                self.log.warning('error getting %s parameter', name, exc=err)
                continue
            parunit = (unit or '').replace('main', selfunit)
            ret.append((name, parvalue,
                        self.formatParam(name, parvalue, use_repr=False),
                        parunit, category))
        return ret

    def shutdown(self):
        """Shut down the device.  This method is called by the NICOS system when
        the device is destroyed, manually or because the current setup is
        unloaded.

        .. method:: doShutdown()

           This method is called, if present, but not in simulation mode.  It
           should perform cleanup, for example closing connections to hardware.
        """
        self.log.debug('shutting down device')
        caught_exc = None
        if self._mode != SIMULATION:
            # do not execute shutdown actions when simulating

            # remove subscriptions to parameter value updates
            if self._cache:
                for param, func in self._subscriptions:
                    self._cache.removeCallback(self, param, func)

            # execute custom shutdown actions
            if hasattr(self, 'doShutdown'):
                try:
                    self.doShutdown()
                except Exception as err:
                    caught_exc = err

        for adev in self._adevs.values():
            if isinstance(adev, list):
                for real_adev in adev:
                    real_adev._sdevs.discard(self._name)
                    real_adev._controllers.discard(self._name)
            elif adev is not None:
                adev._sdevs.discard(self._name)
                adev._controllers.discard(self._name)
        session.devices.pop(self._name, None)
        session.device_case_map.pop(self._name.lower(), None)
        session.explicit_devices.discard(self._name)
        # re-raise the doShutdown error
        if caught_exc is not None:
            raise caught_exc

    @usermethod
    def version(self):
        """Return a list of versions for this device.

        These are tuples (component, version) where a "component" can be the
        name of a Python module, or an external dependency (like a TACO
        server).

        The base implementation already collects VCS revision information
        available from all Python modules involved in the class inheritance
        chain of the device class.

        .. method:: doVersion()

           This method is called if present, and should return a list of
           (component, version) tuples that are added to the version info.
        """
        versions = getVersions(self)
        if not self._sim_intercept and hasattr(self, 'doVersion'):
            versions.extend(self.doVersion())
        return versions

    def _cachelock_acquire(self, timeout=3):
        """Acquire an exclusive lock for using this device from the cache.  This
        can be used if read access to the device needs to be locked (write
        access is locked anyway, since only one NICOS session can be the master
        session at a time).
        """
        if not self._cache:
            return
        start = currenttime()
        while True:
            try:
                self._cache.lock(self._name)
            except CacheLockError:
                if currenttime() > start + timeout:
                    raise CommunicationError(
                        self, 'device locked in cache') from None
                session.delay(self._base_loop_delay * 3)
            else:
                break

    def _cachelock_release(self):
        """Release the exclusive cache lock for this device.

        Always use like this::

           self._cachelock_acquire()
           try:
               ...  # do locked operations
           finally:
               self._cachelock_release()
        """
        if not self._cache:
            return
        try:
            self._cache.unlock(self._name)
        except CacheLockError:
            raise CommunicationError(
                self, 'device locked by other instance') from None

    def _pollParam(self, name, with_ttl=0):
        """Read a parameter from the hardware and put its value into the cache.

        This is intended to be used from :meth:`doPoll` methods, so that they
        don't have to implement parameter polling themselves.  For readable
        devices, if *with_ttl* is > 0, the cached value gets the TTL of the
        device value, determined by :attr:`maxage`, multiplied by *with_ttl*.
        """
        value = getattr(self, 'doRead' + name.title())()
        if with_ttl:
            self._cache.put(self, name, value, currenttime(),
                            getattr(self, 'maxage', 0) * with_ttl)
        else:
            self._cache.put(self, name, value)

    def pollParams(self, volatile_only=True, blocking=False, with_ttl=0,
                   param_list=None):
        """Poll all parameters (normally only volatile ones)."""
        if param_list is None:
            param_list = list(self.parameters)
        param_list = [param for param in param_list if
                      self.parameters[param].volatile or
                      (not volatile_only and
                       hasattr(self, 'doRead' + param.title()))]
        if blocking:
            for param in param_list:
                self._pollParam(param, with_ttl)
        else:
            self._cache.put_raw('poller/%s/pollparams' % self.name, param_list,
                                flag=FLAG_NO_STORE)


class Readable(Device):
    """
    Base class for all readable devices.

    Subclasses *need* to implement:

    * doRead(maxage)

    Subclasses *can* implement:

    * doReset()
    * doPoll(n, maxage)

    Subclasses *can* override:

    * doStatus(maxage)
    * valueInfo()
    """

    parameters = {
        'fmtstr':       Param('Format string for the device value', type=str,
                              default='%.3f', settable=True),
        'unit':         Param('Unit of the device main value', type=str,
                              mandatory=True, settable=True),
        'maxage':       Param('Maximum age of cached value and status (zero '
                              'to never use cached values, or None to cache '
                              'them indefinitely)', unit='s', fmtstr='%.1f',
                              default=12, settable=True,
                              type=none_or(floatrange(0, 24 * 3600))),
        'pollinterval': Param('Polling interval for value and status (or None '
                              'to disable polling)', unit='s', fmtstr='%.1f',
                              default=5, settable=True,
                              type=none_or(floatrange(0.5, 24 * 3600))),
        'warnlimits':   Param('Range in which the device value should be '
                              'in normal operation; warnings may be triggered '
                              'when it is outside', settable=True, chatty=True,
                              unit='main', fmtstr='main',
                              type=none_or(tupleof(anytype, anytype))),
    }

    def init(self):
        self._info_errcount = 0
        # value in simulation mode
        self._sim_intercept = self._mode == SIMULATION and self.hardware_access
        self._sim_old_value = None
        self._sim_value = 0  # no way to configure a useful default...
        self._sim_min = None
        self._sim_max = None
        self._sim_started = None
        self._sim_preset = {}
        Device.init(self)

    def _sim_getMinMax(self):
        """Return info about the value range this device had in a simulation.

        The return value is a list of tuples ``(value name, last value, minimum
        value, maximum value)``.  By default this has one entry, where "value
        name" is the device name.
        """
        if self._sim_min is not None:
            return [(self.name, self.format(self._sim_value),
                     self.format(self._sim_min), self.format(self._sim_max))]
        else:
            return []

    def _sim_setValue(self, pos):
        self._sim_old_value = self._sim_value
        self._sim_value = pos
        if isinstance(pos, number_types):
            if self._sim_min is None:
                self._sim_min = pos
            self._sim_min = min(pos, self._sim_min)
            if self._sim_max is None:
                self._sim_max = pos
            self._sim_max = max(pos, self._sim_max)

    def _setMode(self, mode):
        sim_intercept = mode == SIMULATION and self.hardware_access
        if sim_intercept:
            # save the last known value
            try:
                self._sim_value = self.read()  # cached value is ok here
                self.log.debug('last value before simulation mode is %r',
                               self._sim_value)
            except Exception as err:
                self.log.warning('error reading last value', exc=err)
        self._sim_intercept = sim_intercept
        Device._setMode(self, mode)

    def __call__(self, *values):
        """Allow dev() as shortcut for read."""
        if values:
            # give a nicer error message than "TypeError: takes 1 argument"
            raise UsageError(self, 'not a moveable device')
        return self.read()

    def valueInfo(self):
        """Describe the values read by this device.

        Return a tuple of :class:`~nicos.core.params.Value` instances
        describing the values that :meth:`read` returns.

        This must be overridden by every Readable that returns more than one
        value in a list.  For example, a slit that returns a width and height
        would define ::

            def valueInfo(self):
                return (Value(self.name + '.width', unit=self.unit),
                        Value(self.name + '.height', unit=self.unit))

        By default, this returns a Value that indicates one return value with
        the proper unit and format string of the device.
        """
        return Value(self.name, unit=self.unit, fmtstr=self.fmtstr),

    @usermethod
    def read(self, maxage=None):
        """Read the (possibly cached) main value of the device.

        .. method:: doRead(maxage=0)

           This method must be implemented to read the actual device value from
           the device.  It is only called if the last cached value is out of
           date, or no cache is available.

           The *maxage* parameter should be given to read() calls of subdevices.
        """
        if self._sim_intercept:
            return self._sim_value
        val = self._getFromCache('value', self.doRead, maxage)
        if self._mode == SIMULATION:
            self._sim_setValue(val)
        return val

    @usermethod
    def status(self, maxage=None):
        """Return the (possibly cached) status of the device.

        The status is a tuple of one of the integer constants defined in the
        :mod:`nicos.core.status` module, and textual extended info.

        .. method:: doStatus(maxage=0)

           This method can be implemented to get actual device status from the
           device.  It is only called if the last cached value is out of
           date, or no cache is available.

           If no ``doStatus()`` is implemented, ``status()`` tries to determine
           the status via `nicos.core.utils.multiStatus` of the attached
           devices.  If that is not possible, it returns
           ``status.UNKNOWN, 'doStatus not implemented'``.

           The *maxage* parameter should be given to status() calls of
           subdevices.
        """
        if self._sim_intercept:
            return (status.OK, 'simulated ok')
        return self._getFromCache('status', self._combinedStatus, maxage)

    def _combinedStatus(self, maxage=0):
        """Return the status of the device combined from hardware status and
        movement status determined by NICOS such as timeouts.

        The default implementation just returns the hardware status, except
        that the warnlimits of the device are checked, and the status is
        changed to WARN if they are exceeded.
        """
        try:
            stvalue = self.doStatus(maxage)
        except NicosError as err:
            stvalue = (status.ERROR, str(err))
        except Exception as err:
            stvalue = (status.ERROR, 'unhandled %s: %s' %
                       (err.__class__.__name__, err))
        if stvalue[0] not in status.statuses:
            stvalue = (status.UNKNOWN,
                       'status constant %r is unknown' % stvalue[0])

        if stvalue[0] == status.OK:
            value = None
            wl = self.warnlimits
            if wl:
                value = self.read(maxage)
                if wl[0] is not None and value < wl[0]:
                    stvalue = status.WARN, \
                        statusString(stvalue[1], 'below warn limit (%s)' %
                                     self.format(wl[0], unit=True))
                elif wl[1] is not None and value > wl[1]:
                    stvalue = status.WARN, \
                        statusString(stvalue[1], 'above warn limit (%s)' %
                                     self.format(wl[1], unit=True))
            if isinstance(self, HasLimits):
                if value is None:
                    value = self.read(maxage)
                ul = self.userlimits
                # take precision into account in case we drive exactly to the
                # user limit but the device overshoots a little
                prec = self.precision if isinstance(self, HasPrecision) else 0
                if value < ul[0] - prec:
                    stvalue = status.WARN, \
                        statusString(stvalue[1], 'below user limit (%s)' %
                                     self.format(ul[0], unit=True))
                elif value > ul[1] + prec:
                    stvalue = status.WARN, \
                        statusString(stvalue[1], 'above user limit (%s)' %
                                     self.format(ul[1], unit=True))

        return stvalue

    def doStatus(self, maxage=0):
        if self._adevs:
            return multiStatus(self._adevs, maxage)
        return (status.UNKNOWN, 'doStatus not implemented')

    def poll(self, n=0, maxage=0):
        """Get status and value directly from the device and put both values
        into the cache.  For continuous polling, *n* should increase by one
        with every call to *poll*.

        .. method:: doPoll(n, maxage)

           If present, this method is called to perform additional polling,
           e.g. on parameters that can be changed from outside the NICOS
           system.  The *n* parameter can be used to perform the polling less
           frequently than the polling of value and status.

           If doPoll returns a (status, value) tuple, they are used instead of
           calling doStatus and doRead again.

        .. automethod:: _pollParam
        """
        if self._sim_intercept or self._cache is None:
            return (self.status(), self.read())
        ret = None
        if hasattr(self, 'doPoll'):
            try:
                ret = self.doPoll(n, maxage)
            except Exception:
                self.log.warning('error in doPoll', exc=1)
        if ret is not None and ret[0] is not None:
            ct = currenttime()
            self._cache.put(self, 'status', ret[0], ct, self.maxage)
            self._cache.put(self, 'value', ret[1], ct, self.maxage)
            return ret[0], ret[1]
        # updates shall always get through to the cache
        # self._cache.invalidate(self, 'value')
        # self._cache.invalidate(self, 'status')
        return self.status(maxage), self.read(maxage)

    @usermethod
    def reset(self):
        """Reset the device hardware.  Returns the new status afterwards.

        This operation is forbidden in slave mode, and a no-op for hardware
        devices in simulation mode.

        .. method:: doReset()

           This method is called if implemented.  Otherwise, this is a no-op.
        """
        if self._mode == SLAVE:
            raise ModeError('reset not possible in slave mode')
        elif self._sim_intercept:
            return status.OK, ''
        if isinstance(self, HasTimeout):
            self._setROParam('_timesout', None)
            # reset should not trigger timeoutAction()
            self._timeoutActionCalled = True
        if hasattr(self, 'doReset'):
            self.doReset()
        # make sure, status is propagated to the cache after a reset
        if self._cache:
            self._cache.invalidate(self, 'status')
        return self.status(0)

    def format(self, value, unit=False):
        """Format a value from :meth:`read` into a human-readable string.

        The device unit is not included unless *unit* is true.

        This is done using Python string formatting (the ``%`` operator) with
        the :attr:`fmtstr` parameter value as the format string.
        """
        if isinstance(value, list):
            value = tuple(value)
        try:
            ret = self.fmtstr % value
        except (TypeError, ValueError):
            ret = str(value)
        if unit and self.unit:
            return ret + ' ' + self.unit
        return ret

    def info(self):
        """Automatically add device main value and status."""
        ret = []
        try:
            val = self.read()
            ret.append(('value', val, self.format(val), self.unit, 'general'))
        except Exception as err:
            self._info_errcount += 1
            # only display the message for the first 5 times and then
            # every 20 measurements. always display if in debugmode
            if self._info_errcount <= 5 or self._info_errcount % 20 == 0:
                self.log.warning('error reading', exc=err)
            else:
                self.log.debug('error reading', exc=err)
            ret.append(('value', None, 'Error: %s' % err, '', 'general'))
        else:
            self._info_errcount = 0
        try:
            st = self.status()
        except Exception as err:
            errstr = 'Error: %s' % err
            ret.append(('status', (status.ERROR, errstr), errstr, '', 'status'))
        else:
            ret.append(('status', st, formatStatus(st), '', 'status'))
        return ret + Device.info(self)


class Waitable(Readable):
    """
    Base class for devices that can execute some action and can be waited upon.

    Subclasses *can* implement:

    * doIsCompleted()

    Subclasses *can* override:

    * _getWaiters()
    * doEstimateTime(elapsed)
    * doStatus(maxage)
    """

    busystates = (status.BUSY,)
    errorstates = {status.NOTREACHED: PositionError,
                   status.ERROR: MoveError,
                   status.DISABLED: MoveError}

    @usermethod
    def wait(self):
        """Wait until movement of device is completed.

        Return current device value after waiting.  This is a no-op for
        hardware devices in simulation mode.
        """
        return multiWait([self])[self]

    def finish(self):
        """Empty implementation of finish() for Measurables."""

    def _getWaiters(self):
        """Return a list (or adev-style dict) of all waiter adevs.

        "Waiter" adevs are the subdevices whose status must leave BUSY when
        waiting on self.

        By default, this includes *all* adevs (of which the non-waitables are
        removed), but can be overridden to remove some waitable devices as
        well, in case they are not important to determine the status of this
        device.
        """
        return self._adevs

    def doStatus(self, maxage=0):
        # overridden from Readable to take only waiters into account
        waiters = self._getWaiters()
        if waiters:
            return multiStatus(waiters, maxage)
        return (status.UNKNOWN, 'doStatus not implemented')

    def isCompleted(self):
        """Check for completion of movement.

        The default implementation, which should only be changed in special
        cases, checks the device status and acts according to the
        :attr:`busystates` and :attr:`errorstates` attributes of the device
        (which are sequences of status constants).  If the device is in a busy
        state, this method returns ``False``, if it is in an error state, it
        raises.

        .. attribute:: busystates

           A tuple of status constants that indicate that the device is busy.
           Default is: ``(status.BUSY,)``.

        .. attribute:: errorstates

           Mapping of status constants that indicate that the device movement
           has stopped with an error to exception class to raise.  Default is:
           ``{status.ERROR: MoveError, status.NOTREACHED: PositionError,
           status.DISABLED: MoveError}``.

           This can be overridden in a derived class to provide different
           behavior when waiting for a device.  For example, ``NOTREACHED``
           could be taken out of the errorstates if the user wants to continue
           in this case.

           If the states depend on device parameters, this can be implemented
           as a property.

        .. method:: doIsCompleted()

           This method can be implemented to override :meth:`isCompleted`.
        """
        if hasattr(self, 'doIsCompleted'):
            return self.doIsCompleted()
        st = self.status(0)
        if self.loglevel == 'debug':
            # only read and log the device position if debugging
            # as this could be expensive and is not needed otherwise
            try:
                position = self.read(0)
            except Exception:
                self.log.debug('isCompleted: status %r', formatStatus(st))
            else:
                self.log.debug('isCompleted: status %r at %s',
                               formatStatus(st),
                               self.format(position, unit=True))
        if st[0] in self.busystates:
            return False
        elif st[0] in self.errorstates:
            raise self.errorstates[st[0]](self, st[1])
        return True

    def estimateTime(self, elapsed):
        """Return the estimated time until end of movement/measurement or None.

        Called by ``count()`` for every detector, and ``wait()``/``multiWait()``
        for moving devices.  The result is shown to the user.  The *elapsed*
        argument is the time elapsed since the detector was started.  If the
        time needed can not (yet) be estimated return None, otherwise a float.

        .. method:: doEstimateTime

           This can be overridden in subclasses to calculate an estimation of
           the remaining time needed to complete the measurement.

           If possible avoid recursion here as all active detectors are polled
           anyway.
        """
        try:
            return self.doEstimateTime(elapsed)
        except Exception:
            self.log.debug('could not estimate time', exc=1)
            return None

    def doEstimateTime(self, elapsed):
        return None


class Moveable(Waitable):
    """
    Base class for moveable devices.

    Subclasses *need* to implement:

    * doStart(value)

    Subclasses *can* implement:

    * doStop()
    * doIsAllowed()
    * doTime()
    * doIsAtTarget()
    """

    parameters = {
        'target': Param('Last target position of a start() action',
                        unit='main', fmtstr='main', type=anytype,
                        default=None),
        'fixed':  Param('None if the device is not fixed, else a string '
                        'describing why', settable=False, userparam=False,
                        type=str),
        'fixedby':  Param('Who fixed it?', settable=False, userparam=False,
                          type=none_or(tupleof(str, int)), default=None),
        'requires': Param('Access requirements for moving the device',
                          type=dictof(str, anytype), userparam=False),
    }

    #: The type of the device value, used for typechecking in doStart().
    valuetype = anytype

    def __call__(self, *pos):
        """Allow dev() and dev(newpos) as shortcuts for read and start."""
        if not pos:
            return self.read()
        # allow tuple values to be spelled as individual items,
        # e.g. slit(1, 2, 3, 4) instead of slit((1, 2, 3, 4))
        if len(pos) == 1:
            return self.start(pos[0])
        return self.start(pos)

    def init(self):
        Readable.init(self)
        if isinstance(self, HasLimits):
            # By default, something that has limits has a floating (analog) value.
            if self.valuetype is anytype:
                self.valuetype = float
            offset = 0
            if isinstance(self, HasOffset):
                offset = self.offset
            amin, amax = self.absmin - offset, self.absmax - offset
            umin, umax = self.userlimits
            if self._mode == MASTER:
                if umin > amax or umax < amin:
                    # if user and abs limits are completely disjoint, reset
                    self.log.info('user limits were outside absolute limits; '
                                  'now reset to absolute limits')
                    restricted_limits = (amin, amax)
                elif umin == umax == 0:
                    # if userlimits are both zero, assume failure to read
                    # and try again
                    restricted_limits = self.doReadUserlimits()
                else:
                    # otherwise restrict user to be within abs limits
                    restricted_limits = (min(max(umin, amin), amax),
                                         max(min(umax, amax), amin))
                if restricted_limits != (umin, umax):
                    self.userlimits = restricted_limits
            elif self._mode != SIMULATION:
                if umin < amin:
                    self.log.warning('user minimum (%s) below absolute '
                                     'minimum (%s), please check and re-set '
                                     'limits', umin, amin)
                if umax > amax:
                    self.log.warning('user maximum (%s) above absolute '
                                     'maximum (%s), please check and re-set '
                                     'limits', umax, amax)
            if self._mode == SIMULATION:
                # special case: in simulation mode, doReadUserlimits is not
                # called, so the limits are not set from the absolute limits,
                # and are always (0, 0) except when set in the setup file
                # Set them directly (bypassing pos check, as this may work at
                # this point for simulation)
                if self.userlimits == (0.0, 0.0):
                    self._params['userlimits'] = self.abslimits

    @usermethod
    def isAllowed(self, pos):
        """Check if the given position can be moved to.

        The return value is a tuple ``(valid, why)``.  The first item is a
        boolean indicating if the position is valid, the second item is a
        string with the reason if it is invalid.

        .. method:: doIsAllowed(pos)

           This method must be implemented to check the validity.  If it does
           not exist, all positions are valid.

           Note: to implement ordinary (min, max) limits, do not use this
           method but inherit your device from :class:`HasLimits`.  This takes
           care of all limit processing.

           For complex state-dependent limit use a controller device
           with :class:`IsController` implementing :meth:`isADevTargetAllowed`.
        """
        for controller in self._controllers:
            cdev = session.getDevice(controller)
            ok, reason = cdev.isAdevTargetAllowed(self, pos)
            if not ok:
                return ok, reason

        if isinstance(self, HasLimits):
            limits = self.userlimits
            if isinstance(pos, number_types):
                if not limits[0] <= pos <= limits[1]:
                    return False, 'limits are [%s, %s]' % limits
        if hasattr(self, 'doIsAllowed'):
            return self.doIsAllowed(pos)
        return True, ''

    @usermethod
    def start(self, pos):
        """Start movement of the device to a new position.

        This method should not generally wait for completion of the movement,
        although individual devices can implement it that way if it is
        convenient.

        The validity of the given *pos* is checked by calling :meth:`isAllowed`
        before :meth:`doStart` is called.

        This operation is forbidden in slave mode.  In simulation mode, it sets
        an internal variable to the given position for hardware devices instead
        of calling :meth:`doStart`.

        .. method:: doStart(pos)

           This method must be implemented and actually move the device to the
           new position.
        """
        pos = self._check_start(pos)
        if pos is not Ellipsis:
            self._start_unchecked(pos)

    def _check_start(self, pos):
        """Do all checks if we can move to the given pos.

        Returns new, potentially type-converted pos, or Ellipsis if move
        should not be started.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'start not possible in slave mode')
        if self.fixed:
            # try to determine if we are already there
            try:
                # this may raise if the position values are not numbers
                if abs(self.read() - pos) <= getattr(self, 'precision', 0):
                    self.log.debug('device fixed; start() allowed since '
                                   'already at desired position %s', pos)
                    return Ellipsis
            except Exception:
                pass
            self.log.warning('device fixed, not moving: %s', self.fixed)
            return Ellipsis
        if self.requires:
            try:
                session.checkAccess(self.requires)
            except AccessError as err:
                raise AccessError(
                    self, 'cannot start device: %s' % err) from None
        try:
            pos = self.valuetype(pos)
        except (ValueError, TypeError) as err:
            raise InvalidValueError(self, '%r is an invalid value for this '
                                    'device: %s' % (pos, err)) from None
        ok, why = self.isAllowed(pos)
        if not ok:
            raise LimitError(self, 'moving to %s is not allowed: %s' %
                             (self.format(pos, unit=True), why))
        return pos

    def _start_unchecked(self, pos):
        """Start movement, assuming that _check_start has been done."""
        if isinstance(self, HasTimeout):
            self.resetTimeout(pos)
        if self._sim_intercept:
            self._setROParam('target', pos)
            self._sim_setValue(pos)
            self._sim_started = session.clock.time
            return
        if self._cache:
            self._cache.invalidate(self, 'value')
            self._cache.invalidate(self, 'status')
        if hasattr(self.valuetype, 'compare'):
            target_equal = self.valuetype.compare(self.target, pos)
        else:
            target_equal = (self.target == pos)
        if target_equal:
            # if the target hasn't changed, make the poller receive the correct
            # event in any case
            self._setROParam('target', None)
        self._setROParam('target', pos)
        self.doStart(pos)

    def move(self, pos):
        self.start(pos)

    def doTime(self, old_value, target):
        """Calculate the time to move from one position to another.

        Return the estimated moving time in seconds.
        This is a pure calculatory method which may be overwritten in derived
        classes.

        This basic implementation estimates the moving time
        by evaluating the `speed` or `ramp` parameters, if they are defined.
        If no calculation can be performed, return 0.
        """
        # speed is in physical units per second and ramp is per minute
        if 'speed' in self.parameters and self.speed != 0:
            return abs(target - old_value) / self.speed
        elif 'ramp' in self.parameters and self.ramp != 0:
            return abs(target - old_value) / (self.ramp / 60.)
        return 0.

    def finish(self):
        """Finish up movement of the device.

        This is the final step of waiting for a device, it is called by the
        wait loop.  Devices must not rely on this method being called, since
        it is not triggered when the movement is not waited upon.

        Default implementation is to check if the target has been reached, and
        to call :meth:`doFinish` if present.

        .. method:: doFinish()

           This method is called as part of finish().  If it returns False, the
           target check is skipped.
        """
        if self._sim_intercept:
            time = 0
            try:
                if self._sim_old_value is not None and \
                   self._sim_value is not None:
                    time = self.doTime(self._sim_old_value, self._sim_value)
            except Exception:
                self.log.warning('could not time movement', exc=1)
            if self._sim_started is not None:
                session.clock.wait(self._sim_started + time)
                self._sim_started = None
            self._sim_old_value = self._sim_value
            return
        # call doFinish
        if hasattr(self, 'doFinish'):
            if self.doFinish() is False:
                return
        # do a final read of the device
        pos = self.read(0)
        # check reached value to be equal to target
        if not self.isAtTarget(pos):
            self.log.warning('did not reach target %s, last value is %s',
                             self.format(self.target, unit=True),
                             self.format(pos, unit=True))

    def isAtTarget(self, pos=None, target=None):
        """Check if the device has arrived at the given target.
        If target is omitted it defaults to the device's currently set target.
        If pos is omitted the device checks against its current read value.

        The method calls :meth:`doIsAtTarget` if present.  Otherwise, it checks
        for equality if the value is of string or integer type, and returns
        true otherwise.

        This is the final step of waiting for a device, called by the
        :meth:`finish` method.  Currently, returning False here only means a
        warning, but may be upgraded to an exception later.

        .. method:: doIsAtTarget()

           Called to determine if the device value is at its target within
           an accuracy that is reasonable for the individual device.

           If the device wants to warn about its value, but not trigger an
           exception, this method should emit a warning and still return True.

        For devices with float values, inherit from :class:`HasPrecision`,
        which already comes with an implementation of :meth:`doIsAtTarget`.
        """
        if target is None:
            target = self.target
        if pos is None:
            pos = self.read(0)

        if hasattr(self, 'doIsAtTarget'):
            return self.doIsAtTarget(pos, target)
        elif (isinstance(pos, (str, int)) and
              target is not None):
            return target == pos
        return True

    def _hw_wait(self):
        """Wait until hardware status is not BUSY."""
        while self.doStatus(0)[0] == status.BUSY:
            session.delay(self._base_loop_delay)

    @usermethod
    def maw(self, target):
        """Move to target and wait for completion.

        Equivalent to ``dev.start(target); return dev.wait()``.
        """
        self.start(target)
        return multiWait([self])[self]

    @usermethod
    def stop(self):
        """Stop any movement of the device.

        This operation is forbidden in slave mode, and a no-op for hardware
        devices in simulation mode.

        .. method:: doStop()

           This is called to actually stop the device.  If not present,
           :meth:`stop` will try to stop all attached_devices, if any.
           Otherwise this is a no-op.

        The `stop` method will return the device status after stopping.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'stop not possible in slave mode')
        elif self._sim_intercept:
            return
        if self.fixed:
            self.log.debug('device fixed, not stopping: %s', self.fixed)
            return
        if self.requires:
            try:
                session.checkAccess(self.requires)
            except AccessError as err:
                raise AccessError(
                    self, 'cannot stop device: %s' % err) from None
        if isinstance(self, HasTimeout):
            self._setROParam('_timesout', None)
        if hasattr(self, 'doStop'):
            self.doStop()
        elif self._adevs:
            multiStop(self._adevs)
        if self._cache:
            self._cache.invalidate(self, 'value')
            self._cache.invalidate(self, 'status')

    @usermethod
    def fix(self, reason=''):
        """Fix the device: don't allow movement anymore.

        This blocks :meth:`start` or :meth:`stop` when called on the device.
        """
        eu = session.getExecutingUser()
        if self.fixedby and not session.checkUserLevel(self.fixedby[1], eu):
            # fixed and not enough rights
            self.log.error('device was fixed by %r already', self.fixedby[0])
            return False
        else:
            if self.status()[0] == status.BUSY:
                self.log.warning('device appears to be busy')
            if reason:
                reason += ' (fixed by %r)' % eu.name
            else:
                reason = 'fixed by %r' % eu.name
            self._setROParam('fixed', reason)
            self._setROParam('fixedby', (eu.name, eu.level))
            return True

    @usermethod
    def release(self):
        """Release the device, i.e. undo the effect of fix()."""
        eu = session.getExecutingUser()
        if self.fixedby and not session.checkUserLevel(self.fixedby[1], eu):
            # fixed and not enough rights
            self.log.error('device was fixed by %r and you are not allowed '
                           'to release it', self.fixedby[0])
            return False
        else:
            self._setROParam('fixed', '')
            self._setROParam('fixedby', None)
            return True

    def doEstimateTime(self, elapsed):
        """return the estimated time until end of movement or return None"""
        if self.status()[0] != status.BUSY:
            return None
        eta = set()
        waiters = self._getWaiters()
        if not waiters:
            remain = self.doTime(self.read(), self.target)
            if remain:
                return remain
            return None
        for dev in waiters:
            if hasattr(dev, 'estimateTime'):
                eta.add(dev.estimateTime(elapsed))
        eta.discard(None)
        if eta:
            return max(eta)
        return None


class Measurable(Waitable):
    """
    Base class for devices used for data acquisition.

    Subclasses *need* to implement:

    * doRead(maxage=0)
    * doSetPreset(**preset)
    * doStart()
    * doFinish()
    * doStop()

    Subclasses *can* implement:

    * doStatus(maxage=0)
    * doReadArrays(quality)
    * doIsCompleted()
    * doPause()
    * doResume()
    * doTime(**preset)
    * doSimulate(**preset)
    * doPrepare()
    * valueInfo()
    * arrayInfo()
    * presetInfo()
    * duringMeasureHook(elapsed)
    """

    parameter_overrides = {
        'unit': Override(description='(not used)', mandatory=False),
    }

    errorstates = {status.ERROR: NicosError}

    def _setMode(self, mode):
        # overwritten from Readable: don't read out detectors, it's not useful
        self._sim_intercept = mode == SIMULATION and self.hardware_access
        Device._setMode(self, mode)

    @usermethod
    def setPreset(self, **preset):
        """Set the new standard preset for this detector.

        .. method:: doSetPreset(**preset)

           This method must be present and is called to apply presets to the
           detector. Preset names can be any string, corresponding to some kind
           of preselection that the detector supports.

           Usually a 't' preset (time in seconds) is supported.

           The dictionary passed contains the keys for *all* detectors that
           participate in a measurement, so you should only process those that
           the class understands and leave others alone.

           If the dictionary is empty the detector should use its previous
           preset.

           The `presetInfo` method is called to determine the presets that the
           class supports.
        """
        if self._sim_intercept:
            self._sim_preset = preset
            return
        self.doSetPreset(**preset)

    @usermethod
    def start(self, **preset):
        """Start measurement, with either the given preset or the standard
        preset.  If a preset is given, `doSetPreset` is called with that preset
        first.

        This operation is forbidden in slave mode.

        .. method:: doStart()

           This method must be present and is called to start the measurement.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'start not possible in slave mode')
        elif self._sim_intercept:
            self._sim_started = session.clock.time
            if preset:
                self._sim_preset = preset
            return
        if preset:
            self.doSetPreset(**preset)
        self.doStart()

    def __call__(self, pos=None):
        """Allow dev(), but not dev(pos)."""
        if pos is None:
            return self.read()
        raise UsageError(self, 'device cannot be moved')

    def duringMeasureHook(self, elapsed):
        """Hook called during measurement.

        This can be overridden in subclasses to perform some periodic action
        while measuring.  The hook is called by `.count` for every detector in
        a loop.  The *elapsed* argument is the time elapsed since the detector
        was started.

        If the hook returns a data quality value from ``nicos.core.data``,
        either LIVE or INTERMEDIATE, the detector is read out and the data is
        sent to the data sinks.  The detector is responsible for determining
        how often this should be done.
        """

    @usermethod
    def pause(self):
        """Pause the measurement, if possible.

        Return True if paused successfully.  This operation is forbidden in
        slave mode.

        .. method:: doPause()

           If present, this is called to pause the measurement.  Otherwise,
           ``False`` is returned to indicate that pausing is not possible.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'pause not possible in slave mode')
        elif self._sim_intercept:
            return True
        if hasattr(self, 'doPause'):
            return self.doPause()
        return False

    @usermethod
    def resume(self):
        """Resume paused measurement.

        This operation is forbidden in slave mode.

        .. method:: doResume()

           If present, this is called to resume the measurement.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'resume not possible in slave mode')
        elif self._sim_intercept:
            return
        if hasattr(self, 'doResume'):
            self.doResume()

    @usermethod
    def finish(self):
        """Finish the measurement now.  This should do nothing if the
        measurement was already finished.

        This operation is forbidden in slave mode.

        Called by `.count` for all detectors at the end of a counting.  If
        *exception* is true, the counting was stopped due to an exception.

        .. method:: doFinish()

           This method must be present and is called to actually stop the
           measurement.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'finish not possible in slave mode')
        elif self._sim_intercept:
            if hasattr(self, 'doTime'):
                time = self.doTime(self._sim_preset)
            elif 't' in self._sim_preset:
                time = self._sim_preset['t']
            else:
                time = 0
            if self._sim_started is not None:
                session.clock.wait(self._sim_started + time)
                self._sim_started = None
            return
        self.doFinish()

    @usermethod
    def stop(self):
        """Stop (abort) the measurement now.

        This operation is forbidden in slave mode.

        Called by `.count` if an exception occurs, or if the user calls the
        `stop()` command (possibly by using the immediate stop button).

        .. method:: doStop()

           This method must be present and is called to actually abort the
           measurement.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'stop not possible in slave mode')
        elif self._sim_intercept:
            return
        return self.doStop()

    @usermethod
    def read(self, maxage=None):
        """Return a list with the scalar result(s) of the last measurement."""
        if self._sim_intercept:
            if hasattr(self, 'doSimulate'):
                result = self.doSimulate(self._sim_preset)
                if not isinstance(result, list):
                    return [result]
                return result
            return [0] * len(self.valueInfo())
        # always get fresh result from cache => maxage parameter is ignored
        if self._cache:
            self._cache.invalidate(self, 'value')
        result = self._getFromCache('value', self.doRead)
        if not isinstance(result, list):
            return [result]
        return result

    def doReadArrays(self, quality):
        return []

    @usermethod
    def readArrays(self, quality):
        """Return a list with the array result(s) of the last measurement.

        *quality* is the quality of the requested result: LIVE, INTERMEDIATE,
        FINAL, or INTERRUPTED.
        """
        if self._sim_intercept:
            arrtypes = self.arrayInfo()
            return [numpy.zeros(arrtype.shape) for arrtype in arrtypes]
        result = self.doReadArrays(quality)
        if not isinstance(result, list):
            return [result]
        return result

    @usermethod
    def prepare(self):
        """Prepare measurement before counting.

        .. method:: doPrepare()

           If present, this is called to prepare the measurement.  This method
           will be called before start counting and e.g. for scans before
           moving to the next scan point.

        Preparation can be done in the background if the device status reflects
        that fact (i.e. the device is BUSY until the preparation is done).
        """
        if not self._sim_intercept:
            if hasattr(self, 'doPrepare'):
                return self.doPrepare()

    def readResults(self, quality):
        """Return a tuple of the scalar result(s) of the last measurement and
        the array result(s) of the last measurement.

        The array results will be read first calling `readArrays` because
        there migth be side-effects on the scalar result of `read`, e.g.
        `ImageChannelMixin`'s standard implementation of `doRead` returns
        `self.readresult` which will probably be altered in `doReadArrays`.
        """
        arrays = self.readArrays(quality)
        return self.read(), arrays

    def info(self):
        """Automatically add device status (if not OK).  Does not add the
        device value since that is typically not useful for Measurables.
        """
        ret = []
        try:
            st = self.status()
        except Exception as err:
            self.log.warning('error getting status', exc=err)
            errstr = 'Error: %s' % err
            ret.append(('status', (status.ERROR, errstr), errstr, '', 'status'))
        else:
            ret.append(('status', st, formatStatus(st), '', 'status'))
        return ret + Device.info(self)

    def valueInfo(self):
        """Describe the scalar values measured by this device.

        Return a tuple of :class:`~nicos.core.params.Value` instances
        describing the values that :meth:`read` returns.

        This must be overridden by every Measurable that returns more than one
        value in a list.  The default indicates a single return value with no
        additional info about the value type.
        """
        return Value(self.name, unit=self.unit),

    def arrayInfo(self):
        """Describe the array values measured by this device.

        Return a tuple of :class:`~nicos.core.params.ArrayDesc` instances
        describing the values that :meth:`readArrays` returns.

        This must be overridden by every Measurable that wants to return one or
        more arrays.
        """
        return ()

    def presetInfo(self):
        """Return an iterable of preset keys accepted by this device.

        The default implementation returns only a 't' (time) preset.  This must
        be overridden by all measurables that support more presets.
        """
        return ('t',)


class SubscanMeasurable(Measurable):
    """
    Base class for Measurables that do not count directly, but initiate a
    subscan and extract some values from that.  For this measurable, start()
    is expected to be synchronous.  `isCompleted` always returns True.

    Subclasses *need* to implement:

    * doRead(maxage=0)
    * doStart()

    Subclasses *can* implement:

    * doReadArrays(quality)
    * valueInfo()
    * arrayInfo()
    * presetInfo()
    """

    hardware_access = False

    def start(self, **preset):
        if self._mode == SLAVE:
            raise ModeError(self, 'start not possible in slave mode')
        if preset:
            self.doSetPreset(**preset)
        # XXX start subscan measurable
        self.doStart()

    def doStop(self):
        pass

    def doIsCompleted(self):
        return True


# Use the DeviceMixinMeta metaclass here to provide the instancecheck
# Not derived from DeviceMixinBase as this class is not a mixin.
class NoDevice(metaclass=DeviceMixinMeta):
    """A class that represents "no device" attached to a :class:`DeviceAlias`."""

    __mergedattrs__ = []  # needed by the metaclass

    def __init__(self, name):
        self.name = name

    def __str__(self):
        return '<none>'

    def __reduce__(self):
        return (str, (self.name,))

    def __getattr__(self, name):
        raise AttributeError('alias %r does not point to any device' %
                             self.name)

    def __setattr__(self, name, value):
        if name != 'name':
            raise ConfigurationError('alias %r does not point to any device' %
                                     self.name)
        object.__setattr__(self, name, value)


class DeviceAlias(Device):
    """
    Generic "alias" device that can point all access to any other NICOS device.

    The device that should be accessed is set using the "alias" parameter,
    which can be configured and changed at runtime.  For example, with a
    DeviceAlias instance *T*::

        T.alias = Tcryo
        read(T)   # will read Tcryo
        T.alias = Toven
        read(T)   # will read Toven

    This allows to call e.g. the sample temperature by the same name in all
    sample environment setups, but behind the scenes implement it using
    different actual hardware devices.

    If the "alias" parameter is empty, the alias points to a special "NoDevice"
    object that raises a `ConfigurationError` on every access.
    """

    parameters = {
        'alias':    Param('Device to alias', type=none_or(nicosdev),
                          settable=True, chatty=True),
        'devclass': Param('Class name that the aliased device must be an '
                          'instance of',
                          type=str, default='nicos.core.device.Device'),
    }

    _ownattrs = ['_obj', '_mode', '_cache', 'alias']
    _ownparams = {'alias', 'name', 'devclass', 'visibility'}
    _initialized = False

    __display__ = True

    def __init__(self, name, **config):
        self._obj = None
        devclass = config.get('devclass', 'nicos.core.device.Device')
        try:
            modname, clsname = devclass.rsplit('.', 1)
            self._cls = session._nicos_import(modname, clsname)
        except Exception:
            self.log.warning('could not import class %r; using Device as the '
                             'alias devclass', devclass, exc=1)
            self._cls = Device
        Device.__init__(self, name, **config)
        # update the configured alias device - we do this after the init
        # procedure to avoid calling init stuff in the aliased dev
        devname = config.get('alias')
        if devname is None and self._cache:
            devname = self._cache.get(self, 'alias', '')
        self.doUpdateAlias(devname)
        self._initialized = True

    def __repr__(self):
        if isinstance(self._obj, NoDevice):
            return '<device %s, device alias pointing to nothing>' % self._name
        if not self.description:
            return '<device %s, alias to %s>' % (self._name, self._obj)
        return '<device %s, alias to %s "%s">' % (self._name, self._obj,
                                                  self.description)

    def doUpdateAlias(self, devname, recursive=False):
        # Important NOTE: this is never called in the poller, since the poller
        # replaces DeviceAliases by a cache reader.  Therefore we get away with
        # calling _setROParam on device creation.
        if not devname:
            self._obj = NoDevice(str(self))
            if self._cache:
                self._cache.unsetRewrite(str(self))
                self._reinitParams()
            return
        try:
            newdev = session.getDevice(devname, (self._cls, DeviceAlias),
                                       source=self)
            if newdev is self:
                raise NicosError(self, 'cannot set alias pointing to itself')
            if newdev != self._obj:
                self._obj = newdev
                if self._cache:
                    self._cache.setRewrite(str(self), devname)
                    self._reinitParams()
        except Exception:
            if self._initialized:
                raise
            # On initialization, this must not raise an error, otherwise the
            # device cannot be created at all, and will fail depending devices.
            # Instead, select another target if possible.
            if not recursive:
                self._recoverMissingDevice(devname)
            else:
                # If this is a recursive call (another target already failed),
                # do not try others to avoid infinite looping through the
                # alias config here.
                self.log.exception('cannot point to %s' % devname)
                self.doUpdateAlias('')

    def _reinitParams(self):
        if self._mode != MASTER:  # only in the copy that changed the alias
            return
        # clear all cached parameters
        self._cache.clear(str(self), exclude=self._ownparams)
        # put the parameters from the original device in the cache under the
        # name of the alias
        if not isinstance(self._obj, Device):
            return
        for pname in self._obj.parameters:
            if pname in self._ownparams:
                continue
            try:
                self._cache.put(self, pname, getattr(self._obj, pname))
            except NicosError:
                pass  # silently ignore

    def _recoverMissingDevice(self, devname):
        """Recover from not having a valid target (it does not exist, cannot be
        created or has the wrong type).
        """
        # first, check the preferred alias config
        for target, _ in session.alias_config.get(self._name, []):
            if target != devname and target in session.configured_devices:
                self.log.warning('could not find aliased device %s, '
                                 'pointing to %s instead', devname, target)
                new_target = target
                break
        else:
            # then, check the config file
            fromconfig = self._config.get('alias', '')
            self.log.warning('could not find aliased device %s, pointing '
                             'to target from setup file (%s)',
                             devname, fromconfig or 'nothing')
            new_target = fromconfig
        # if we have a potential new target, check if we can get hold of it
        if new_target:
            try:
                session.getDevice(new_target, (self._cls, DeviceAlias),
                                  source=self)
            except Exception:
                # all hope is lost!
                self.log.warning('could not find %s either, pointing to '
                                 'nothing', new_target)
                new_target = ''
        # now make the new choice of alias permanent, including in the cache
        # (which we must do with _setROParam since we might not be master yet)
        self.doUpdateAlias(new_target, recursive=True)
        Device._setROParam(self, 'alias', new_target)

    # Device methods that would not be alias-aware

    def _setROParam(self, param, value):
        raise NicosError(self, "please don't use _setROParam on a DeviceAlias")

    def _getParamConfig(self, param):
        if param in self.parameters:
            return self.parameters[param]
        return self._obj._getParamConfig(param)

    def valueInfo(self):
        # override to replace name of the aliased device with the alias' name
        new_info = []
        rx = r'^%s\b' % re.escape(self._obj.name)
        for v in self._obj.valueInfo():
            new_v = v.copy()
            # replace dev name, if at start of value name, with alias name
            new_v.name = re.sub(rx, self.name, v.name)
            new_info.append(new_v)
        return tuple(new_info)

    @usermethod
    def info(self):
        """Return "device information" from the original device."""
        # override to use the object's "info" but add a note about the alias
        ret = []
        if isinstance(self._obj, Device):
            ret = self._obj.info()
        return ret + [('alias', str(self._obj), str(self._obj), '', 'instrument')]

    @usermethod
    def version(self):
        """Return a list of versions for original device."""
        v = []
        if isinstance(self._obj, Device):
            v = self._obj.version()
        v.extend(Device.version(self))
        return v

    # these methods must not be proxied

    def __eq__(self, other):
        return self is other

    def __ne__(self, other):
        return self is not other

    # use the default __hash__
    __hash__ = Device.__hash__

    def shutdown(self):
        # re-implemented from Device to avoid running doShutdown
        # of the pointed-to device prematurely
        self.log.debug('shutting down device')
        if self._mode != SIMULATION:
            # remove subscriptions to parameter value updates
            if self._cache:
                for param, func in self._subscriptions:
                    self._cache.removeCallback(self, param, func)
        session.devices.pop(self._name, None)
        session.device_case_map.pop(self._name.lower(), None)
        session.explicit_devices.discard(self._name)

    # generic proxying of missing attributes to the object

    def __getattr__(self, name):
        if not self._initialized:
            raise AttributeError(name)
        else:
            if name in DeviceAlias._ownattrs:
                return object.__getattr__(self, name)
            return getattr(self._obj, name)

    def __setattr__(self, name, value):
        if name in DeviceAlias._ownattrs or not self._initialized:
            object.__setattr__(self, name, value)
        else:
            setattr(self._obj, name, value)

    def __delattr__(self, name):
        if name in DeviceAlias._ownattrs or not self._initialized:
            object.__delattr__(self, name)
        else:
            delattr(self._obj, name)


# proxying of special methods to the object

def make_method(name):
    def method(self, *args, **kw):
        return getattr(self._obj, name)(*args, **kw)
    return method


for name in [
        '__abs__', '__add__', '__and__', '__bool__', '__call__',
        '__contains__', '__delitem__', '__divmod__', '__float__',
        '__floordiv__', '__ge__', '__getitem__', '__gt__', '__hash__',
        '__iadd__', '__iand__', '__idivmod__', '__ifloordiv__', '__ilshift__',
        '__imatmul__', '__imod__', '__imul__', '__index__', '__int__',
        '__invert__', '__ior__', '__ipow__', '__irshift__', '__isub__',
        '__iter__', '__itruediv__', '__ixor__', '__le__', '__len__',
        '__lshift__', '__lt__', '__matmul__', '__mod__', '__mul__', '__neg__',
        '__next__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__',
        '__rdivmod__', '__reduce__', '__reduce_ex__', '__reversed__',
        '__rfloorfiv__', '__rlshift__', '__rmatmul__', '__rmod__', '__rmul__',
        '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__',
        '__rtruediv__', '__rxor__', '__setitem__', '__sub__', '__truediv__',
        '__xor__',
]:
    if hasattr(Device, name):
        setattr(DeviceAlias, name, make_method(name))

#  -*- coding: utf-8 -*-
# *****************************************************************************
# NICOS, the Networked Instrument Control System of the MLZ
# Copyright (c) 2009-2022 by the NICOS contributors (see AUTHORS)
#
# 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:
#   <NAME> <<EMAIL>>
#
# *****************************************************************************

"""Base device classes for usage in NICOS."""

import inspect
import re
from time import time as currenttime

import numpy

from nicos import session
from nicos.core import status
from nicos.core.constants import MASTER, POLLER, SIMULATION, SLAVE
from nicos.core.errors import AccessError, CacheLockError, \
    CommunicationError, ConfigurationError, InvalidValueError, LimitError, \
    ModeError, MoveError, NicosError, PositionError, ProgrammingError, \
    UsageError
from nicos.core.mixins import DeviceMixinMeta, HasLimits, HasOffset, \
    HasPrecision, HasTimeout, IsController
from nicos.core.params import INFO_CATEGORIES, Attach, Override, Param, \
    Value, anytype, dictof, floatrange, listof, nicosdev, none_or, oneof, \
    setof, tupleof
from nicos.core.utils import formatStatus, multiStatus, multiStop, multiWait, \
    statusString, usermethod
from nicos.protocols.cache import FLAG_NO_STORE
from nicos.utils import getVersions, loggers, number_types, parseDateString

ALLOWED_CATEGORIES = {v[0] for v in INFO_CATEGORIES}


def requires(**access):
    """Decorator to implement user access control.

    The access is checked based on the keywords given.  Currently, the
    keywords with meaning are:

    * ``'level'``: gives the minimum required user access level and can
      have the values ``GUEST``, ``USER`` or ``ADMIN`` as defined in the
      :mod:`nicos.core.utils` module.
    * ``'mode'``: gives the required exection mode ("master", "slave",
      "maintenance", "simulation").
    * ``'passcode'``: only usable in the interactive console: gives a
      passcode that the user has to type back.

    A special keyword is ``'helpmsg'``; if the access check fails, this gives
    a message that is appended to the error message.

    The wrapper function calls `.Session.checkAccess` to verify the
    requirements.  If the check fails, `.AccessError` is raised.
    """
    def decorator(func):
        def new_func(*args, **kwds):
            try:
                session.checkAccess(access)
            except AccessError as err:
                msg = 'cannot do %s: %s' % (func.__name__, err)
                if 'helpmsg' in access:
                    msg += ' (%s)' % access['helpmsg']
                if args and isinstance(args[0], Device):
                    raise AccessError(args[0], msg) from None
                raise AccessError(msg) from None
            return func(*args, **kwds)
        new_func.__name__ = func.__name__
        new_func.__doc__ = func.__doc__
        new_func.real_func = func
        return new_func
    return decorator


class DeviceMeta(DeviceMixinMeta):
    """
    A metaclass that automatically adds properties for the class' parameters,
    and determines a list of user methods.

    It also merges these attributes defined in the class with those defined in
    all base classes:

    * attached_devices
    * parameters
    * parameter_overrides
    * methods (autogenerated)
    """

    def __new__(mcs, name, bases, attrs):
        # check base class ordering: mixins before others
        seen_nonmixin = False
        for base in bases:
            if not isinstance(base, DeviceMeta) and seen_nonmixin:
                raise ProgrammingError('%s.%s device: mixin class %s must come'
                                       ' before all non-mixins in the base'
                                       ' class list' % (attrs['__module__'],
                                                        name, base.__name__))
            if isinstance(base, DeviceMeta):
                seen_nonmixin = True
        newtype = DeviceMixinMeta.__new__(mcs, name, bases, attrs)

        # to debug MRO problems you could use this line
        # print('MRO:', newtype, newtype.mro())
        for adevname, entry in list(newtype.attached_devices.items()):
            # adev names are always lowercased
            if adevname != adevname.lower():
                raise ProgrammingError('%r device: attached device name %r is '
                                       'not all-lowercase' % (name, adevname))
            if not isinstance(entry, Attach):
                raise ProgrammingError("Please use 'Attach' to define an "
                                       "attached device.")

        # create parameter properties
        for param, info in newtype.parameters.items():
            # parameter names are always lowercased (enforce this)
            if param != param.lower():
                raise ProgrammingError('%r device: parameter name %r is not '
                                       'all-lowercase' % (name, param))
            if not isinstance(info, Param):
                raise ProgrammingError('%r device %r parameter info should be '
                                       'a Param object' % (name, param))
            if param in ('value', 'status'):
                raise ProgrammingError('%r device: parameter names "value" '
                                       'and "status" are reserved and cannot '
                                       'be used' % (name, ))

            # process overrides
            override = newtype.parameter_overrides.get(param)
            if override:
                info = newtype.parameters[param] = override.apply(info)

            # create the getter method
            if not info.volatile:
                def getter(self, param=param):
                    if param not in self._params:
                        self._initParam(param)
                    if self._cache and param != 'name':  # no renaming !
                        value = self._cache.get(self, param, Ellipsis)
                        if value is not Ellipsis:
                            self._params[param] = value
                            return value
                    return self._params[param]
            else:
                rmethod = getattr(newtype, 'doRead' + param.title(), None)
                if rmethod is None:
                    raise ProgrammingError('%r device %r parameter is marked '
                                           'as "volatile=True", but has no '
                                           'doRead%s method' %
                                           (name, param, param.title()))

                def getter(self, param=param, rmethod=rmethod):
                    if self._mode == SIMULATION:
                        return self._initParam(param)
                    value = rmethod(self)
                    if value == self._params.get(param, Ellipsis):
                        return value
                    if self._cache:
                        self._cache.put(self, param, value)
                    self._params[param] = value
                    return value

            # create the setter method
            if not info.settable:
                def setter(self, value, param=param):
                    raise ConfigurationError(
                        self, 'the %s parameter can only be changed in the '
                        'setup file' % param)
            else:
                wmethod = getattr(newtype, 'doWrite' + param.title(), None)
                umethod = getattr(newtype, 'doUpdate' + param.title(), None)

                def setter(self, value, param=param, wmethod=wmethod,
                           umethod=umethod, chatty=info.chatty):
                    value = self._validateType(value, param)
                    if getattr(self, 'requires', None):  # for Moveables
                        try:
                            session.checkAccess(self.requires)
                        except AccessError as err:
                            raise AccessError(self, 'cannot set parameter: %s'
                                              % err) from None
                    if self._mode == SLAVE:
                        raise ModeError('setting parameter %s not possible in '
                                        'slave mode' % param)
                    elif self._mode == SIMULATION:
                        if wmethod and not self._sim_intercept:
                            rv = wmethod(self, value)
                            if rv is not None:
                                value = rv
                        if umethod:
                            umethod(self, value)
                        if chatty:
                            oldvalue = self._params[param]
                            if value != oldvalue:
                                valuestr = self.formatParam(param, value)
                                oldvstr = self.formatParam(param, oldvalue)
                                self.log.info('%s set to %s (was %s)',
                                              param, valuestr, oldvstr)
                        self._params[param] = value
                        return
                    oldvalue = getattr(self, param)
                    if wmethod:
                        # allow doWrite to override the value
                        rv = wmethod(self, value)
                        if rv is not None:
                            value = rv
                    if umethod:
                        umethod(self, value)
                    if chatty:
                        if value != oldvalue:
                            valuestr = self.formatParam(param, value)
                            oldvstr = self.formatParam(param, oldvalue)
                            self.log.info('%s set to %s (was %s)',
                                          param, valuestr, oldvstr)
                    self._params[param] = value
                    if self._cache:
                        self._cache.put(self, param, value)

            # create a property and attach to the new device class
            setattr(newtype, param,
                    property(getter, setter, doc=info.formatDoc()))
        del newtype.parameter_overrides
        if 'parameter_overrides' in attrs:
            del attrs['parameter_overrides']
        if 'valuetype' in attrs:
            newtype.valuetype = staticmethod(attrs['valuetype'])

        # check attribute names for the attached devices (created by
        # Device.__init__)
        for adevname, entry in newtype.attached_devices.items():
            pname = '_attached_%s' % adevname
            if newtype.parameters and (pname in newtype.parameters):
                raise ProgrammingError('%r device: attached device helper '
                                       'property %r collides with parameter' %
                                       (name, pname))
            if pname in attrs:
                raise ProgrammingError('%r device: attached device helper '
                                       'property %r collides with attribute' %
                                       (name, pname))

        return newtype


class Device(metaclass=DeviceMeta):
    """
    An object that has a list of parameters that are read from the
    configuration and have default values.

    Subclasses *can* implement:

    * doPreinit()
    * doInit()
    * doShutdown()
    * doVersion()
    * doInfo()
    """

    __mergedattrs__ = ['parameters', 'parameter_overrides', 'attached_devices',
                       'methods']

    # A dictionary mapping device names to classes (or lists of classes) that
    # describe this device's attached (subordinate) devices.
    attached_devices = {}

    # A dictionary mapping parameter names to parameter descriptions, given as
    # Param objects.
    parameters = {
        'name':        Param('The name of the device',
                             type=str, settable=False, userparam=False),
        'classes':     Param('Names of device class and all its base classes',
                             type=listof(str), settable=False, userparam=False),
        'description': Param('A description of the device', type=str,
                             settable=True),
        'visibility':  Param('Selects in which context the device should be '
                             'shown/included',
                             type=setof('metadata', 'namespace', 'devlist'),
                             default=['metadata', 'namespace', 'devlist'],
                             settable=False),
        # Don't allow setting loglevel to > INFO, as it could be confusing.
        'loglevel':    Param('The logging level of the device',
                             type=oneof('debug', 'info'),
                             default='info', settable=True, preinit=True),
    }

    _ownparams = {'name'}

    # A dictionary mapping parameter names to Override objects that override
    # specific properties of parameters found in base classes.
    parameter_overrides = {}

    # Set this to True on devices that are only created for a time, and whose
    # name can be reused.
    temporary = False

    # Set this to True on devices that directly access hardware, and therefore
    # should have their actions simulated.
    hardware_access = True

    # This is set by NICOS to indicate that do-methods should be intercepted
    # and their result simulated.  Combines hardware_access and device mode
    # at runtime.
    _sim_intercept = True

    # Autogenerated inventory of the class' user methods.
    methods = {}

    # Loop delay defaults.  To be set to low values in the test suite.
    _base_loop_delay = 0.1
    _long_loop_delay = 0.5

    def __init__(self, name, **config):
        # register self in device registry
        if not self.temporary:
            if name in session.devices:
                raise ProgrammingError('device with name %s already exists' %
                                       name)
            session.devices[name] = self
            session.device_case_map[name.lower()] = name

        self._name = name
        # _config: device configuration (all parameter names lower-case)
        self._config = {name.lower(): value for (name, value) in config.items()}
        # _params: parameter values from config
        self._params = {'name': name}
        # _infoparams: cached list of parameters to get on info()
        self._infoparams = []
        # _adevs: "attached" device instances
        self._adevs = {}
        # superdevs: reverse adevs for dependency tracking
        self._sdevs = set()
        # keep an explicit record of controllers
        self._controllers = set()
        # execution mode
        self._mode = session.mode

        # initialize a logger for the device
        self.__dict__['log'] = session.getLogger(name)

        try:
            # initialize device
            self.init()
        except BaseException as err:
            try:
                self.shutdown()
            except Exception:
                self.log.warning('could not shutdown after creation failed',
                                 exc=1)
            raise err

    attribute_whitelist = {
        'valuetype',  # for all devices
        'arraydesc',  # for image producers
        'autodevices',  # for HasAutoDevices
    }

    def __setattr__(self, name, value):
        # disallow modification of any public attributes that are not
        # parameters or otherwise properties
        if name[0] != '_' and name not in self.attribute_whitelist:
            obj = getattr(self.__class__, name, Ellipsis)
            if obj is Ellipsis:
                raise UsageError(self, 'device has no parameter %s, use '
                                 'ListParams(%s) to show all' % (name, self))
            elif inspect.isroutine(obj):
                raise UsageError(self, '%s cannot be assigned; it is a device '
                                 'method' % name)
            elif not isinstance(obj, property):
                # this should also be forbidden at some point, but for now just
                # emit a warning to make sure it doesn't break the world
                self.log.warning('Setting a non-parameter attribute %s.%s: '
                                 'this is deprecated and will be an error '
                                 'in a future version.', self, name)
        object.__setattr__(self, name, value)

    def __str__(self):
        return self._name

    def __repr__(self):
        if not self.description:
            return '<device %s (a %s.%s)>' % (self._name,
                                              self.__class__.__module__,
                                              self.__class__.__name__)
        return '<device %s "%s" (a %s.%s)>' % (self._name,
                                               self.description,
                                               self.__class__.__module__,
                                               self.__class__.__name__)

    def __reduce__(self):
        # Used for pickling the device e.g. when sending between daemon and GUI
        return (str, (self._name,))

    def doReadName(self):
        return self._name

    def doReadClasses(self):
        return [c.__module__ + '.' + c.__name__ for c in self.__class__.__mro__]

    def doUpdateLoglevel(self, value):
        if session.sessiontype == POLLER:
            # suppress debug/info messages from ordinary devices in the poller
            self.log.setLevel(loggers.WARNING)
        else:
            self.log.setLevel(loggers.loglevels[value])

    def _attachDevices(self):
        """Validate and create attached devices."""
        for aname, entry in sorted(self.attached_devices.items()):
            if not isinstance(entry, Attach):
                raise ProgrammingError(self, 'attached device entry for %r is '
                                       'invalid; the value should be a '
                                       'nicos.core.Attach object' % aname)
            value = self._config.pop(aname, None)
            devlist = []
            class_needed = entry.devclass
            if self._mode == SIMULATION:
                # need to relax this instance check for simulation mode;
                # aliases are not yet set correctly when the devices are
                # created
                class_needed = object

            for i, devname in enumerate(entry.check(self, aname, value)):
                if (devname is None) or \
                   (devname not in session.configured_devices and entry.missingok):
                    devlist.append(None)
                    continue
                try:
                    dev = session.getDevice(devname, class_needed, source=self)
                except UsageError:
                    raise ConfigurationError(
                        self, 'device %r item %d has wrong type (should be %s)' %
                        (aname, i + 1, entry.devclass.__name__)) from None
                devlist.append(dev)

            for dev in devlist:
                if dev is not None:
                    dev._sdevs.add(self._name)
                    if isinstance(self, IsController):
                        dev._controllers.add(self._name)
            self.__dict__['_attached_%s' % aname] = self._adevs[aname] = \
                devlist[0] if entry.single else devlist

    def init(self):
        """Initialize the object; this is called by the NICOS system when the
        device instance has been created.

        This method first initializes all attached devices (creating them if
        necessary), then initializes parameters.

        .. XXX expand parameter init procedure

        .. method:: doPreinit(mode)

           This method, if present, is called before parameters are initialized
           (except for parameters that have the ``preinit`` property set to
           true).

           This allows to initialize a hardware connection if it is necessary
           for the various ``doRead...()`` methods of other parameters that
           read the current parameter value from the hardware.

        .. method:: doInit(mode)

           This method, if present, is called after all parameters have been
           initialized.  It is the correct place to set up additional
           attributes, or to perform initial (read-only!) communication with
           the hardware.

        .. note:: ``doPreinit()`` and ``doInit()`` are called regardless of the
           current execution mode.  This means that if one of these methods
           does hardware access, it must be done only if ``mode != SIMULATION``.
        """
        self._cache = None
        self._subscriptions = []

        self._attachDevices()

        self._cache = self._getCache()
        lastconfig = None
        if self._cache:
            lastconfig = self._cache.get('_lastconfig_', self._name, None)
            old_classes = self._cache.get(self, 'classes')
            new_classes = self.doReadClasses()
            if old_classes != new_classes and self._mode == MASTER:
                self._cache.put(self, 'classes', new_classes)

        def _init_param(param, paraminfo):
            param = param.lower()
            # mandatory parameters must be in config, regardless of cache
            if paraminfo.mandatory and param not in self._config:
                raise ConfigurationError(self, 'missing configuration '
                                         'parameter %r' % param)
            # Ellipsis representing "no value" since None is a valid value for
            # some parameters
            value = Ellipsis
            # try to get from cache
            if self._cache:
                value = self._cache.get(self, param, Ellipsis)
                if param == 'name':  # clean up legacy, wrong values
                    self._cache.put(self, 'name', self._name)
                    value = self._name
                if value is not Ellipsis:
                    try:
                        value = self._validateType(value, param, paraminfo)
                    except ConfigurationError as e:
                        self.log.warning('value of %s from cache (%r) is '
                                         'invalid: %r using default handling '
                                         'instead.', param, value, e)
                        value = Ellipsis
            if value is not Ellipsis:
                if param in self._ownparams:
                    self._params[param] = value
                    return
                if param in self._config:
                    cfgvalue = self._validateType(self._config[param], param)
                    if cfgvalue != value:
                        valuestr = self.formatParam(param, value)
                        cfgvstr = self.formatParam(param, cfgvalue)
                        prefercache = paraminfo.prefercache
                        if prefercache is None:
                            prefercache = paraminfo.settable
                        if lastconfig and lastconfig.get(param) != cfgvalue:
                            self.log.warning(
                                "value of '%s' from cache (%s) differs from "
                                'configured value (%s), using configured '
                                'since it was changed in the setup file',
                                param, valuestr, cfgvstr)
                            value = cfgvalue
                            self._cache.put(self, param, value)
                        elif prefercache:
                            self.log.warning(
                                "value of '%s' from cache (%s) differs from "
                                'configured value (%s), using cached',
                                param, valuestr, cfgvstr)
                        else:
                            self.log.warning(
                                "value of '%s' from cache (%s) differs from "
                                'configured value (%s), using configured',
                                param, valuestr, cfgvstr)
                            value = cfgvalue
                            self._cache.put(self, param, value)
                elif not paraminfo.settable and paraminfo.prefercache is False:
                    # parameter is in cache, but not in config: if it is not
                    # settable and has a default, use that (since most probably
                    # the default is intended to be used but has changed)
                    defvalue = paraminfo.default
                    if defvalue != value:
                        defvalue = self._validateType(defvalue, param,
                                                      paraminfo)
                    if defvalue != value:
                        valuestr = self.formatParam(param, value)
                        defvstr = self.formatParam(param, paraminfo.default)
                        self.log.warning(
                            "value of '%s' from cache (%s) differs from "
                            'default value (%s), using default',
                            param, valuestr, defvstr)
                        value = paraminfo.default
                        self._cache.put(self, param, value)
                umethod = getattr(self, 'doUpdate' + param.title(), None)
                if umethod:
                    umethod(value)
                self._params[param] = value
            else:
                self._initParam(param, paraminfo)
                notfromcache.append(param)

            if param in self._config and paraminfo.internal:
                self.log.warning(
                    "'%s' is configured in a setup file although "
                    "declared as internal parameter",
                    param
                )

            if paraminfo.category is not None:
                if paraminfo.category not in ALLOWED_CATEGORIES:
                    self.log.error('parameter %s uses illegal category %r!',
                                   param, paraminfo.category)
                else:
                    self._infoparams.append((paraminfo.category, param,
                                             paraminfo.unit))
            # end of _init_param()

        notfromcache = []
        later = []

        for param, paraminfo in self.parameters.items():
            if paraminfo.preinit:
                _init_param(param, paraminfo)
            else:
                later.append((param, paraminfo))

        if hasattr(self, 'doPreinit'):
            self.doPreinit(self._mode)

        for param, paraminfo in later:
            _init_param(param, paraminfo)

        # warn about parameters that weren't present in cache
        if self._cache and notfromcache:
            self.log.info('these parameters were not present in cache: %s',
                          ', '.join(notfromcache))

        self._infoparams.sort()

        # subscribe to parameter value updates, if a doUpdate method exists
        if self._cache:
            for param in self.parameters:
                umethod = getattr(self, 'doUpdate' + param.title(), None)
                if umethod:
                    def updateparam(key, value, time, umethod=umethod):
                        umethod(value)
                    self._cache.addCallback(self, param, updateparam)
                    self._subscriptions.append((param, updateparam))

        if self._cache:
            self._cache.put('_lastconfig_', self._name, self._config)

        # call custom initialization
        if hasattr(self, 'doInit'):
            self.doInit(self._mode)

    def _getCache(self):
        """Indirection needed by the Cache client itself."""
        return session.cache

    def _validateType(self, value, param, paraminfo=None):
        """Validate and coerce the value of a parameter to the correct type.

        If the value can't be coerced, a ConfigurationError is raised.
        """
        paraminfo = paraminfo or self._getParamConfig(param)
        try:
            value = paraminfo.type(value)
        except (ValueError, TypeError) as err:
            raise ConfigurationError(
                self, '%r is an invalid value for parameter %s: %s' % (
                    value, param, err)) from err
        return value

    def _initParam(self, param, paraminfo=None):
        """Get an initial value for the parameter, called when the cache
        doesn't contain such a value.

        If present, a doReadParam method is called.  Otherwise, the value comes
        from either the setup file or the device-specific default value.
        """
        paraminfo = paraminfo or self.parameters[param]
        rmethod = getattr(self, 'doRead' + param.title(), None)
        umethod = getattr(self, 'doUpdate' + param.title(), None)
        done = False
        # try to read from the hardware (only in non-simulation mode)
        if not self._sim_intercept and rmethod:
            try:
                value = rmethod()
            except NicosError:
                self.log.warning('could not read initial value for parameter '
                                 '%s from device', param)
            else:
                done = True
        if not done and param in self._params:
            # happens when called from a param getter, not from init()
            value = self._params[param]
        elif not done:
            value = self._config.get(param, paraminfo.default)
        value = self._validateType(value, param, paraminfo)
        if self._cache:  # will not be there in simulation mode
            self._cache.put(self, param, value)
        # always call update methods, they should be working for simulation
        if umethod:
            umethod(value)
        self._params[param] = value
        return value

    def _setROParam(self, param, value):
        """Set an otherwise read-only parameter.

        This is useful for parameters that change at runtime, but indirectly,
        such as "last filenumber".
        """
        value = self._validateType(value, param)
        self._params[param] = value
        if self._cache:
            self._cache.put(self, param, value)

    def _getParamConfig(self, param):
        """Return the entry for the parameter from self.parameters.

        This should be used when alias resolution is desired, since it is
        overridden for DeviceAliases.
        """
        return self.parameters[param]

    def _getFromCache(self, name, func, maxage=None):
        """Get *name* from the cache, or call *func* if outdated/not present.

        If the *maxage* parameter is set, do not allow the value to be older
        than that amount of seconds.
        """
        if not self._cache:
            return func()
        val = Ellipsis
        if maxage != 0:
            val = self._cache.get(
                self, name, Ellipsis,
                mintime=currenttime() - maxage if maxage is not None else 0)
        if val is Ellipsis:
            defmaxage = getattr(self, 'maxage', None)
            val = func(defmaxage if maxage is None else maxage)
            self._cache.put(self, name, val, currenttime(), defmaxage)
        return val

    def formatParam(self, param, value, use_repr=True):
        """Format a parameter value according to its fmtstr."""
        if isinstance(value, list):
            value = tuple(value)
        fmtstr = self._getParamConfig(param).fmtstr
        if fmtstr == '%r' and not use_repr:
            fmtstr = '%s'
        if fmtstr == 'main':
            if isinstance(value, tuple):
                fmtstr = '(' + ', '.join((self.fmtstr,) * len(value)) + ')'
            else:
                fmtstr = self.fmtstr
        try:
            ret = fmtstr % value
        except (TypeError, ValueError):
            ret = repr(value)
        return ret

    def _setMode(self, mode):
        """Set a new execution mode."""
        self._mode = mode
        if mode == SIMULATION:
            # switching to simulation mode: remove cache entirely
            # and rely on saved _params and values
            self._cache = None

    def history(self, name='value', fromtime=None, totime=None):
        """Return a history of the parameter *name* (can also be ``'value'`` or
        ``'status'``).

        *fromtime* and *totime* can be used to limit the time window.  They can
        be:

        * positive numbers: interpreted as UNIX timestamps
        * negative numbers: interpreted as hours back from now
        * strings: in one of the formats 'HH:MM', 'HH:MM:SS',
          'YYYY-MM-DD', 'YYYY-MM-DD HH:MM' or 'YYYY-MM-DD HH:MM:SS'

        Default is to query the values of the last hour.
        """
        if not self._cache:
            # no cache is configured for this setup
            return []
        else:
            if fromtime is None:
                fromtime = -1
            if isinstance(fromtime, str):
                fromtime = parseDateString(fromtime)
            elif fromtime < 0:
                fromtime = currenttime() + fromtime * 3600
            if totime is None:
                totime = currenttime()
            elif isinstance(totime, str):
                totime = parseDateString(totime, enddate=True)
            elif totime < 0:
                totime = currenttime() + totime * 3600
            return self._cache.history(self, name, fromtime, totime)

    def info(self):
        """Return "device information" as an iterable of tuples ``(name,
        raw_value, formatted_value, unit, category)``.

        This "device information" is put into data files and should therefore
        include any parameters that will be essential to record the current
        status of the instrument.

        The default implementation already collects all parameters whose
        ``category`` property is set.

        .. method:: doInfo()

           This method can add more device information by returning it as a
           sequence of tuples.
        """
        ret = []
        if hasattr(self, 'doInfo') and self._mode != SIMULATION:
            ret.extend(self.doInfo())
        selfunit = getattr(self, 'unit', '')
        for category, name, unit in self._infoparams:
            try:
                parvalue = self._getFromCache(
                    name, lambda _maxage=None: getattr(self, name))
            except Exception as err:
                self.log.warning('error getting %s parameter', name, exc=err)
                continue
            parunit = (unit or '').replace('main', selfunit)
            ret.append((name, parvalue,
                        self.formatParam(name, parvalue, use_repr=False),
                        parunit, category))
        return ret

    def shutdown(self):
        """Shut down the device.  This method is called by the NICOS system when
        the device is destroyed, manually or because the current setup is
        unloaded.

        .. method:: doShutdown()

           This method is called, if present, but not in simulation mode.  It
           should perform cleanup, for example closing connections to hardware.
        """
        self.log.debug('shutting down device')
        caught_exc = None
        if self._mode != SIMULATION:
            # do not execute shutdown actions when simulating

            # remove subscriptions to parameter value updates
            if self._cache:
                for param, func in self._subscriptions:
                    self._cache.removeCallback(self, param, func)

            # execute custom shutdown actions
            if hasattr(self, 'doShutdown'):
                try:
                    self.doShutdown()
                except Exception as err:
                    caught_exc = err

        for adev in self._adevs.values():
            if isinstance(adev, list):
                for real_adev in adev:
                    real_adev._sdevs.discard(self._name)
                    real_adev._controllers.discard(self._name)
            elif adev is not None:
                adev._sdevs.discard(self._name)
                adev._controllers.discard(self._name)
        session.devices.pop(self._name, None)
        session.device_case_map.pop(self._name.lower(), None)
        session.explicit_devices.discard(self._name)
        # re-raise the doShutdown error
        if caught_exc is not None:
            raise caught_exc

    @usermethod
    def version(self):
        """Return a list of versions for this device.

        These are tuples (component, version) where a "component" can be the
        name of a Python module, or an external dependency (like a TACO
        server).

        The base implementation already collects VCS revision information
        available from all Python modules involved in the class inheritance
        chain of the device class.

        .. method:: doVersion()

           This method is called if present, and should return a list of
           (component, version) tuples that are added to the version info.
        """
        versions = getVersions(self)
        if not self._sim_intercept and hasattr(self, 'doVersion'):
            versions.extend(self.doVersion())
        return versions

    def _cachelock_acquire(self, timeout=3):
        """Acquire an exclusive lock for using this device from the cache.  This
        can be used if read access to the device needs to be locked (write
        access is locked anyway, since only one NICOS session can be the master
        session at a time).
        """
        if not self._cache:
            return
        start = currenttime()
        while True:
            try:
                self._cache.lock(self._name)
            except CacheLockError:
                if currenttime() > start + timeout:
                    raise CommunicationError(
                        self, 'device locked in cache') from None
                session.delay(self._base_loop_delay * 3)
            else:
                break

    def _cachelock_release(self):
        """Release the exclusive cache lock for this device.

        Always use like this::

           self._cachelock_acquire()
           try:
               ...  # do locked operations
           finally:
               self._cachelock_release()
        """
        if not self._cache:
            return
        try:
            self._cache.unlock(self._name)
        except CacheLockError:
            raise CommunicationError(
                self, 'device locked by other instance') from None

    def _pollParam(self, name, with_ttl=0):
        """Read a parameter from the hardware and put its value into the cache.

        This is intended to be used from :meth:`doPoll` methods, so that they
        don't have to implement parameter polling themselves.  For readable
        devices, if *with_ttl* is > 0, the cached value gets the TTL of the
        device value, determined by :attr:`maxage`, multiplied by *with_ttl*.
        """
        value = getattr(self, 'doRead' + name.title())()
        if with_ttl:
            self._cache.put(self, name, value, currenttime(),
                            getattr(self, 'maxage', 0) * with_ttl)
        else:
            self._cache.put(self, name, value)

    def pollParams(self, volatile_only=True, blocking=False, with_ttl=0,
                   param_list=None):
        """Poll all parameters (normally only volatile ones)."""
        if param_list is None:
            param_list = list(self.parameters)
        param_list = [param for param in param_list if
                      self.parameters[param].volatile or
                      (not volatile_only and
                       hasattr(self, 'doRead' + param.title()))]
        if blocking:
            for param in param_list:
                self._pollParam(param, with_ttl)
        else:
            self._cache.put_raw('poller/%s/pollparams' % self.name, param_list,
                                flag=FLAG_NO_STORE)


class Readable(Device):
    """
    Base class for all readable devices.

    Subclasses *need* to implement:

    * doRead(maxage)

    Subclasses *can* implement:

    * doReset()
    * doPoll(n, maxage)

    Subclasses *can* override:

    * doStatus(maxage)
    * valueInfo()
    """

    parameters = {
        'fmtstr':       Param('Format string for the device value', type=str,
                              default='%.3f', settable=True),
        'unit':         Param('Unit of the device main value', type=str,
                              mandatory=True, settable=True),
        'maxage':       Param('Maximum age of cached value and status (zero '
                              'to never use cached values, or None to cache '
                              'them indefinitely)', unit='s', fmtstr='%.1f',
                              default=12, settable=True,
                              type=none_or(floatrange(0, 24 * 3600))),
        'pollinterval': Param('Polling interval for value and status (or None '
                              'to disable polling)', unit='s', fmtstr='%.1f',
                              default=5, settable=True,
                              type=none_or(floatrange(0.5, 24 * 3600))),
        'warnlimits':   Param('Range in which the device value should be '
                              'in normal operation; warnings may be triggered '
                              'when it is outside', settable=True, chatty=True,
                              unit='main', fmtstr='main',
                              type=none_or(tupleof(anytype, anytype))),
    }

    def init(self):
        self._info_errcount = 0
        # value in simulation mode
        self._sim_intercept = self._mode == SIMULATION and self.hardware_access
        self._sim_old_value = None
        self._sim_value = 0  # no way to configure a useful default...
        self._sim_min = None
        self._sim_max = None
        self._sim_started = None
        self._sim_preset = {}
        Device.init(self)

    def _sim_getMinMax(self):
        """Return info about the value range this device had in a simulation.

        The return value is a list of tuples ``(value name, last value, minimum
        value, maximum value)``.  By default this has one entry, where "value
        name" is the device name.
        """
        if self._sim_min is not None:
            return [(self.name, self.format(self._sim_value),
                     self.format(self._sim_min), self.format(self._sim_max))]
        else:
            return []

    def _sim_setValue(self, pos):
        self._sim_old_value = self._sim_value
        self._sim_value = pos
        if isinstance(pos, number_types):
            if self._sim_min is None:
                self._sim_min = pos
            self._sim_min = min(pos, self._sim_min)
            if self._sim_max is None:
                self._sim_max = pos
            self._sim_max = max(pos, self._sim_max)

    def _setMode(self, mode):
        sim_intercept = mode == SIMULATION and self.hardware_access
        if sim_intercept:
            # save the last known value
            try:
                self._sim_value = self.read()  # cached value is ok here
                self.log.debug('last value before simulation mode is %r',
                               self._sim_value)
            except Exception as err:
                self.log.warning('error reading last value', exc=err)
        self._sim_intercept = sim_intercept
        Device._setMode(self, mode)

    def __call__(self, *values):
        """Allow dev() as shortcut for read."""
        if values:
            # give a nicer error message than "TypeError: takes 1 argument"
            raise UsageError(self, 'not a moveable device')
        return self.read()

    def valueInfo(self):
        """Describe the values read by this device.

        Return a tuple of :class:`~nicos.core.params.Value` instances
        describing the values that :meth:`read` returns.

        This must be overridden by every Readable that returns more than one
        value in a list.  For example, a slit that returns a width and height
        would define ::

            def valueInfo(self):
                return (Value(self.name + '.width', unit=self.unit),
                        Value(self.name + '.height', unit=self.unit))

        By default, this returns a Value that indicates one return value with
        the proper unit and format string of the device.
        """
        return Value(self.name, unit=self.unit, fmtstr=self.fmtstr),

    @usermethod
    def read(self, maxage=None):
        """Read the (possibly cached) main value of the device.

        .. method:: doRead(maxage=0)

           This method must be implemented to read the actual device value from
           the device.  It is only called if the last cached value is out of
           date, or no cache is available.

           The *maxage* parameter should be given to read() calls of subdevices.
        """
        if self._sim_intercept:
            return self._sim_value
        val = self._getFromCache('value', self.doRead, maxage)
        if self._mode == SIMULATION:
            self._sim_setValue(val)
        return val

    @usermethod
    def status(self, maxage=None):
        """Return the (possibly cached) status of the device.

        The status is a tuple of one of the integer constants defined in the
        :mod:`nicos.core.status` module, and textual extended info.

        .. method:: doStatus(maxage=0)

           This method can be implemented to get actual device status from the
           device.  It is only called if the last cached value is out of
           date, or no cache is available.

           If no ``doStatus()`` is implemented, ``status()`` tries to determine
           the status via `nicos.core.utils.multiStatus` of the attached
           devices.  If that is not possible, it returns
           ``status.UNKNOWN, 'doStatus not implemented'``.

           The *maxage* parameter should be given to status() calls of
           subdevices.
        """
        if self._sim_intercept:
            return (status.OK, 'simulated ok')
        return self._getFromCache('status', self._combinedStatus, maxage)

    def _combinedStatus(self, maxage=0):
        """Return the status of the device combined from hardware status and
        movement status determined by NICOS such as timeouts.

        The default implementation just returns the hardware status, except
        that the warnlimits of the device are checked, and the status is
        changed to WARN if they are exceeded.
        """
        try:
            stvalue = self.doStatus(maxage)
        except NicosError as err:
            stvalue = (status.ERROR, str(err))
        except Exception as err:
            stvalue = (status.ERROR, 'unhandled %s: %s' %
                       (err.__class__.__name__, err))
        if stvalue[0] not in status.statuses:
            stvalue = (status.UNKNOWN,
                       'status constant %r is unknown' % stvalue[0])

        if stvalue[0] == status.OK:
            value = None
            wl = self.warnlimits
            if wl:
                value = self.read(maxage)
                if wl[0] is not None and value < wl[0]:
                    stvalue = status.WARN, \
                        statusString(stvalue[1], 'below warn limit (%s)' %
                                     self.format(wl[0], unit=True))
                elif wl[1] is not None and value > wl[1]:
                    stvalue = status.WARN, \
                        statusString(stvalue[1], 'above warn limit (%s)' %
                                     self.format(wl[1], unit=True))
            if isinstance(self, HasLimits):
                if value is None:
                    value = self.read(maxage)
                ul = self.userlimits
                # take precision into account in case we drive exactly to the
                # user limit but the device overshoots a little
                prec = self.precision if isinstance(self, HasPrecision) else 0
                if value < ul[0] - prec:
                    stvalue = status.WARN, \
                        statusString(stvalue[1], 'below user limit (%s)' %
                                     self.format(ul[0], unit=True))
                elif value > ul[1] + prec:
                    stvalue = status.WARN, \
                        statusString(stvalue[1], 'above user limit (%s)' %
                                     self.format(ul[1], unit=True))

        return stvalue

    def doStatus(self, maxage=0):
        if self._adevs:
            return multiStatus(self._adevs, maxage)
        return (status.UNKNOWN, 'doStatus not implemented')

    def poll(self, n=0, maxage=0):
        """Get status and value directly from the device and put both values
        into the cache.  For continuous polling, *n* should increase by one
        with every call to *poll*.

        .. method:: doPoll(n, maxage)

           If present, this method is called to perform additional polling,
           e.g. on parameters that can be changed from outside the NICOS
           system.  The *n* parameter can be used to perform the polling less
           frequently than the polling of value and status.

           If doPoll returns a (status, value) tuple, they are used instead of
           calling doStatus and doRead again.

        .. automethod:: _pollParam
        """
        if self._sim_intercept or self._cache is None:
            return (self.status(), self.read())
        ret = None
        if hasattr(self, 'doPoll'):
            try:
                ret = self.doPoll(n, maxage)
            except Exception:
                self.log.warning('error in doPoll', exc=1)
        if ret is not None and ret[0] is not None:
            ct = currenttime()
            self._cache.put(self, 'status', ret[0], ct, self.maxage)
            self._cache.put(self, 'value', ret[1], ct, self.maxage)
            return ret[0], ret[1]
        # updates shall always get through to the cache
        # self._cache.invalidate(self, 'value')
        # self._cache.invalidate(self, 'status')
        return self.status(maxage), self.read(maxage)

    @usermethod
    def reset(self):
        """Reset the device hardware.  Returns the new status afterwards.

        This operation is forbidden in slave mode, and a no-op for hardware
        devices in simulation mode.

        .. method:: doReset()

           This method is called if implemented.  Otherwise, this is a no-op.
        """
        if self._mode == SLAVE:
            raise ModeError('reset not possible in slave mode')
        elif self._sim_intercept:
            return status.OK, ''
        if isinstance(self, HasTimeout):
            self._setROParam('_timesout', None)
            # reset should not trigger timeoutAction()
            self._timeoutActionCalled = True
        if hasattr(self, 'doReset'):
            self.doReset()
        # make sure, status is propagated to the cache after a reset
        if self._cache:
            self._cache.invalidate(self, 'status')
        return self.status(0)

    def format(self, value, unit=False):
        """Format a value from :meth:`read` into a human-readable string.

        The device unit is not included unless *unit* is true.

        This is done using Python string formatting (the ``%`` operator) with
        the :attr:`fmtstr` parameter value as the format string.
        """
        if isinstance(value, list):
            value = tuple(value)
        try:
            ret = self.fmtstr % value
        except (TypeError, ValueError):
            ret = str(value)
        if unit and self.unit:
            return ret + ' ' + self.unit
        return ret

    def info(self):
        """Automatically add device main value and status."""
        ret = []
        try:
            val = self.read()
            ret.append(('value', val, self.format(val), self.unit, 'general'))
        except Exception as err:
            self._info_errcount += 1
            # only display the message for the first 5 times and then
            # every 20 measurements. always display if in debugmode
            if self._info_errcount <= 5 or self._info_errcount % 20 == 0:
                self.log.warning('error reading', exc=err)
            else:
                self.log.debug('error reading', exc=err)
            ret.append(('value', None, 'Error: %s' % err, '', 'general'))
        else:
            self._info_errcount = 0
        try:
            st = self.status()
        except Exception as err:
            errstr = 'Error: %s' % err
            ret.append(('status', (status.ERROR, errstr), errstr, '', 'status'))
        else:
            ret.append(('status', st, formatStatus(st), '', 'status'))
        return ret + Device.info(self)


class Waitable(Readable):
    """
    Base class for devices that can execute some action and can be waited upon.

    Subclasses *can* implement:

    * doIsCompleted()

    Subclasses *can* override:

    * _getWaiters()
    * doEstimateTime(elapsed)
    * doStatus(maxage)
    """

    busystates = (status.BUSY,)
    errorstates = {status.NOTREACHED: PositionError,
                   status.ERROR: MoveError,
                   status.DISABLED: MoveError}

    @usermethod
    def wait(self):
        """Wait until movement of device is completed.

        Return current device value after waiting.  This is a no-op for
        hardware devices in simulation mode.
        """
        return multiWait([self])[self]

    def finish(self):
        """Empty implementation of finish() for Measurables."""

    def _getWaiters(self):
        """Return a list (or adev-style dict) of all waiter adevs.

        "Waiter" adevs are the subdevices whose status must leave BUSY when
        waiting on self.

        By default, this includes *all* adevs (of which the non-waitables are
        removed), but can be overridden to remove some waitable devices as
        well, in case they are not important to determine the status of this
        device.
        """
        return self._adevs

    def doStatus(self, maxage=0):
        # overridden from Readable to take only waiters into account
        waiters = self._getWaiters()
        if waiters:
            return multiStatus(waiters, maxage)
        return (status.UNKNOWN, 'doStatus not implemented')

    def isCompleted(self):
        """Check for completion of movement.

        The default implementation, which should only be changed in special
        cases, checks the device status and acts according to the
        :attr:`busystates` and :attr:`errorstates` attributes of the device
        (which are sequences of status constants).  If the device is in a busy
        state, this method returns ``False``, if it is in an error state, it
        raises.

        .. attribute:: busystates

           A tuple of status constants that indicate that the device is busy.
           Default is: ``(status.BUSY,)``.

        .. attribute:: errorstates

           Mapping of status constants that indicate that the device movement
           has stopped with an error to exception class to raise.  Default is:
           ``{status.ERROR: MoveError, status.NOTREACHED: PositionError,
           status.DISABLED: MoveError}``.

           This can be overridden in a derived class to provide different
           behavior when waiting for a device.  For example, ``NOTREACHED``
           could be taken out of the errorstates if the user wants to continue
           in this case.

           If the states depend on device parameters, this can be implemented
           as a property.

        .. method:: doIsCompleted()

           This method can be implemented to override :meth:`isCompleted`.
        """
        if hasattr(self, 'doIsCompleted'):
            return self.doIsCompleted()
        st = self.status(0)
        if self.loglevel == 'debug':
            # only read and log the device position if debugging
            # as this could be expensive and is not needed otherwise
            try:
                position = self.read(0)
            except Exception:
                self.log.debug('isCompleted: status %r', formatStatus(st))
            else:
                self.log.debug('isCompleted: status %r at %s',
                               formatStatus(st),
                               self.format(position, unit=True))
        if st[0] in self.busystates:
            return False
        elif st[0] in self.errorstates:
            raise self.errorstates[st[0]](self, st[1])
        return True

    def estimateTime(self, elapsed):
        """Return the estimated time until end of movement/measurement or None.

        Called by ``count()`` for every detector, and ``wait()``/``multiWait()``
        for moving devices.  The result is shown to the user.  The *elapsed*
        argument is the time elapsed since the detector was started.  If the
        time needed can not (yet) be estimated return None, otherwise a float.

        .. method:: doEstimateTime

           This can be overridden in subclasses to calculate an estimation of
           the remaining time needed to complete the measurement.

           If possible avoid recursion here as all active detectors are polled
           anyway.
        """
        try:
            return self.doEstimateTime(elapsed)
        except Exception:
            self.log.debug('could not estimate time', exc=1)
            return None

    def doEstimateTime(self, elapsed):
        return None


class Moveable(Waitable):
    """
    Base class for moveable devices.

    Subclasses *need* to implement:

    * doStart(value)

    Subclasses *can* implement:

    * doStop()
    * doIsAllowed()
    * doTime()
    * doIsAtTarget()
    """

    parameters = {
        'target': Param('Last target position of a start() action',
                        unit='main', fmtstr='main', type=anytype,
                        default=None),
        'fixed':  Param('None if the device is not fixed, else a string '
                        'describing why', settable=False, userparam=False,
                        type=str),
        'fixedby':  Param('Who fixed it?', settable=False, userparam=False,
                          type=none_or(tupleof(str, int)), default=None),
        'requires': Param('Access requirements for moving the device',
                          type=dictof(str, anytype), userparam=False),
    }

    #: The type of the device value, used for typechecking in doStart().
    valuetype = anytype

    def __call__(self, *pos):
        """Allow dev() and dev(newpos) as shortcuts for read and start."""
        if not pos:
            return self.read()
        # allow tuple values to be spelled as individual items,
        # e.g. slit(1, 2, 3, 4) instead of slit((1, 2, 3, 4))
        if len(pos) == 1:
            return self.start(pos[0])
        return self.start(pos)

    def init(self):
        Readable.init(self)
        if isinstance(self, HasLimits):
            # By default, something that has limits has a floating (analog) value.
            if self.valuetype is anytype:
                self.valuetype = float
            offset = 0
            if isinstance(self, HasOffset):
                offset = self.offset
            amin, amax = self.absmin - offset, self.absmax - offset
            umin, umax = self.userlimits
            if self._mode == MASTER:
                if umin > amax or umax < amin:
                    # if user and abs limits are completely disjoint, reset
                    self.log.info('user limits were outside absolute limits; '
                                  'now reset to absolute limits')
                    restricted_limits = (amin, amax)
                elif umin == umax == 0:
                    # if userlimits are both zero, assume failure to read
                    # and try again
                    restricted_limits = self.doReadUserlimits()
                else:
                    # otherwise restrict user to be within abs limits
                    restricted_limits = (min(max(umin, amin), amax),
                                         max(min(umax, amax), amin))
                if restricted_limits != (umin, umax):
                    self.userlimits = restricted_limits
            elif self._mode != SIMULATION:
                if umin < amin:
                    self.log.warning('user minimum (%s) below absolute '
                                     'minimum (%s), please check and re-set '
                                     'limits', umin, amin)
                if umax > amax:
                    self.log.warning('user maximum (%s) above absolute '
                                     'maximum (%s), please check and re-set '
                                     'limits', umax, amax)
            if self._mode == SIMULATION:
                # special case: in simulation mode, doReadUserlimits is not
                # called, so the limits are not set from the absolute limits,
                # and are always (0, 0) except when set in the setup file
                # Set them directly (bypassing pos check, as this may work at
                # this point for simulation)
                if self.userlimits == (0.0, 0.0):
                    self._params['userlimits'] = self.abslimits

    @usermethod
    def isAllowed(self, pos):
        """Check if the given position can be moved to.

        The return value is a tuple ``(valid, why)``.  The first item is a
        boolean indicating if the position is valid, the second item is a
        string with the reason if it is invalid.

        .. method:: doIsAllowed(pos)

           This method must be implemented to check the validity.  If it does
           not exist, all positions are valid.

           Note: to implement ordinary (min, max) limits, do not use this
           method but inherit your device from :class:`HasLimits`.  This takes
           care of all limit processing.

           For complex state-dependent limit use a controller device
           with :class:`IsController` implementing :meth:`isADevTargetAllowed`.
        """
        for controller in self._controllers:
            cdev = session.getDevice(controller)
            ok, reason = cdev.isAdevTargetAllowed(self, pos)
            if not ok:
                return ok, reason

        if isinstance(self, HasLimits):
            limits = self.userlimits
            if isinstance(pos, number_types):
                if not limits[0] <= pos <= limits[1]:
                    return False, 'limits are [%s, %s]' % limits
        if hasattr(self, 'doIsAllowed'):
            return self.doIsAllowed(pos)
        return True, ''

    @usermethod
    def start(self, pos):
        """Start movement of the device to a new position.

        This method should not generally wait for completion of the movement,
        although individual devices can implement it that way if it is
        convenient.

        The validity of the given *pos* is checked by calling :meth:`isAllowed`
        before :meth:`doStart` is called.

        This operation is forbidden in slave mode.  In simulation mode, it sets
        an internal variable to the given position for hardware devices instead
        of calling :meth:`doStart`.

        .. method:: doStart(pos)

           This method must be implemented and actually move the device to the
           new position.
        """
        pos = self._check_start(pos)
        if pos is not Ellipsis:
            self._start_unchecked(pos)

    def _check_start(self, pos):
        """Do all checks if we can move to the given pos.

        Returns new, potentially type-converted pos, or Ellipsis if move
        should not be started.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'start not possible in slave mode')
        if self.fixed:
            # try to determine if we are already there
            try:
                # this may raise if the position values are not numbers
                if abs(self.read() - pos) <= getattr(self, 'precision', 0):
                    self.log.debug('device fixed; start() allowed since '
                                   'already at desired position %s', pos)
                    return Ellipsis
            except Exception:
                pass
            self.log.warning('device fixed, not moving: %s', self.fixed)
            return Ellipsis
        if self.requires:
            try:
                session.checkAccess(self.requires)
            except AccessError as err:
                raise AccessError(
                    self, 'cannot start device: %s' % err) from None
        try:
            pos = self.valuetype(pos)
        except (ValueError, TypeError) as err:
            raise InvalidValueError(self, '%r is an invalid value for this '
                                    'device: %s' % (pos, err)) from None
        ok, why = self.isAllowed(pos)
        if not ok:
            raise LimitError(self, 'moving to %s is not allowed: %s' %
                             (self.format(pos, unit=True), why))
        return pos

    def _start_unchecked(self, pos):
        """Start movement, assuming that _check_start has been done."""
        if isinstance(self, HasTimeout):
            self.resetTimeout(pos)
        if self._sim_intercept:
            self._setROParam('target', pos)
            self._sim_setValue(pos)
            self._sim_started = session.clock.time
            return
        if self._cache:
            self._cache.invalidate(self, 'value')
            self._cache.invalidate(self, 'status')
        if hasattr(self.valuetype, 'compare'):
            target_equal = self.valuetype.compare(self.target, pos)
        else:
            target_equal = (self.target == pos)
        if target_equal:
            # if the target hasn't changed, make the poller receive the correct
            # event in any case
            self._setROParam('target', None)
        self._setROParam('target', pos)
        self.doStart(pos)

    def move(self, pos):
        self.start(pos)

    def doTime(self, old_value, target):
        """Calculate the time to move from one position to another.

        Return the estimated moving time in seconds.
        This is a pure calculatory method which may be overwritten in derived
        classes.

        This basic implementation estimates the moving time
        by evaluating the `speed` or `ramp` parameters, if they are defined.
        If no calculation can be performed, return 0.
        """
        # speed is in physical units per second and ramp is per minute
        if 'speed' in self.parameters and self.speed != 0:
            return abs(target - old_value) / self.speed
        elif 'ramp' in self.parameters and self.ramp != 0:
            return abs(target - old_value) / (self.ramp / 60.)
        return 0.

    def finish(self):
        """Finish up movement of the device.

        This is the final step of waiting for a device, it is called by the
        wait loop.  Devices must not rely on this method being called, since
        it is not triggered when the movement is not waited upon.

        Default implementation is to check if the target has been reached, and
        to call :meth:`doFinish` if present.

        .. method:: doFinish()

           This method is called as part of finish().  If it returns False, the
           target check is skipped.
        """
        if self._sim_intercept:
            time = 0
            try:
                if self._sim_old_value is not None and \
                   self._sim_value is not None:
                    time = self.doTime(self._sim_old_value, self._sim_value)
            except Exception:
                self.log.warning('could not time movement', exc=1)
            if self._sim_started is not None:
                session.clock.wait(self._sim_started + time)
                self._sim_started = None
            self._sim_old_value = self._sim_value
            return
        # call doFinish
        if hasattr(self, 'doFinish'):
            if self.doFinish() is False:
                return
        # do a final read of the device
        pos = self.read(0)
        # check reached value to be equal to target
        if not self.isAtTarget(pos):
            self.log.warning('did not reach target %s, last value is %s',
                             self.format(self.target, unit=True),
                             self.format(pos, unit=True))

    def isAtTarget(self, pos=None, target=None):
        """Check if the device has arrived at the given target.
        If target is omitted it defaults to the device's currently set target.
        If pos is omitted the device checks against its current read value.

        The method calls :meth:`doIsAtTarget` if present.  Otherwise, it checks
        for equality if the value is of string or integer type, and returns
        true otherwise.

        This is the final step of waiting for a device, called by the
        :meth:`finish` method.  Currently, returning False here only means a
        warning, but may be upgraded to an exception later.

        .. method:: doIsAtTarget()

           Called to determine if the device value is at its target within
           an accuracy that is reasonable for the individual device.

           If the device wants to warn about its value, but not trigger an
           exception, this method should emit a warning and still return True.

        For devices with float values, inherit from :class:`HasPrecision`,
        which already comes with an implementation of :meth:`doIsAtTarget`.
        """
        if target is None:
            target = self.target
        if pos is None:
            pos = self.read(0)

        if hasattr(self, 'doIsAtTarget'):
            return self.doIsAtTarget(pos, target)
        elif (isinstance(pos, (str, int)) and
              target is not None):
            return target == pos
        return True

    def _hw_wait(self):
        """Wait until hardware status is not BUSY."""
        while self.doStatus(0)[0] == status.BUSY:
            session.delay(self._base_loop_delay)

    @usermethod
    def maw(self, target):
        """Move to target and wait for completion.

        Equivalent to ``dev.start(target); return dev.wait()``.
        """
        self.start(target)
        return multiWait([self])[self]

    @usermethod
    def stop(self):
        """Stop any movement of the device.

        This operation is forbidden in slave mode, and a no-op for hardware
        devices in simulation mode.

        .. method:: doStop()

           This is called to actually stop the device.  If not present,
           :meth:`stop` will try to stop all attached_devices, if any.
           Otherwise this is a no-op.

        The `stop` method will return the device status after stopping.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'stop not possible in slave mode')
        elif self._sim_intercept:
            return
        if self.fixed:
            self.log.debug('device fixed, not stopping: %s', self.fixed)
            return
        if self.requires:
            try:
                session.checkAccess(self.requires)
            except AccessError as err:
                raise AccessError(
                    self, 'cannot stop device: %s' % err) from None
        if isinstance(self, HasTimeout):
            self._setROParam('_timesout', None)
        if hasattr(self, 'doStop'):
            self.doStop()
        elif self._adevs:
            multiStop(self._adevs)
        if self._cache:
            self._cache.invalidate(self, 'value')
            self._cache.invalidate(self, 'status')

    @usermethod
    def fix(self, reason=''):
        """Fix the device: don't allow movement anymore.

        This blocks :meth:`start` or :meth:`stop` when called on the device.
        """
        eu = session.getExecutingUser()
        if self.fixedby and not session.checkUserLevel(self.fixedby[1], eu):
            # fixed and not enough rights
            self.log.error('device was fixed by %r already', self.fixedby[0])
            return False
        else:
            if self.status()[0] == status.BUSY:
                self.log.warning('device appears to be busy')
            if reason:
                reason += ' (fixed by %r)' % eu.name
            else:
                reason = 'fixed by %r' % eu.name
            self._setROParam('fixed', reason)
            self._setROParam('fixedby', (eu.name, eu.level))
            return True

    @usermethod
    def release(self):
        """Release the device, i.e. undo the effect of fix()."""
        eu = session.getExecutingUser()
        if self.fixedby and not session.checkUserLevel(self.fixedby[1], eu):
            # fixed and not enough rights
            self.log.error('device was fixed by %r and you are not allowed '
                           'to release it', self.fixedby[0])
            return False
        else:
            self._setROParam('fixed', '')
            self._setROParam('fixedby', None)
            return True

    def doEstimateTime(self, elapsed):
        """return the estimated time until end of movement or return None"""
        if self.status()[0] != status.BUSY:
            return None
        eta = set()
        waiters = self._getWaiters()
        if not waiters:
            remain = self.doTime(self.read(), self.target)
            if remain:
                return remain
            return None
        for dev in waiters:
            if hasattr(dev, 'estimateTime'):
                eta.add(dev.estimateTime(elapsed))
        eta.discard(None)
        if eta:
            return max(eta)
        return None


class Measurable(Waitable):
    """
    Base class for devices used for data acquisition.

    Subclasses *need* to implement:

    * doRead(maxage=0)
    * doSetPreset(**preset)
    * doStart()
    * doFinish()
    * doStop()

    Subclasses *can* implement:

    * doStatus(maxage=0)
    * doReadArrays(quality)
    * doIsCompleted()
    * doPause()
    * doResume()
    * doTime(**preset)
    * doSimulate(**preset)
    * doPrepare()
    * valueInfo()
    * arrayInfo()
    * presetInfo()
    * duringMeasureHook(elapsed)
    """

    parameter_overrides = {
        'unit': Override(description='(not used)', mandatory=False),
    }

    errorstates = {status.ERROR: NicosError}

    def _setMode(self, mode):
        # overwritten from Readable: don't read out detectors, it's not useful
        self._sim_intercept = mode == SIMULATION and self.hardware_access
        Device._setMode(self, mode)

    @usermethod
    def setPreset(self, **preset):
        """Set the new standard preset for this detector.

        .. method:: doSetPreset(**preset)

           This method must be present and is called to apply presets to the
           detector. Preset names can be any string, corresponding to some kind
           of preselection that the detector supports.

           Usually a 't' preset (time in seconds) is supported.

           The dictionary passed contains the keys for *all* detectors that
           participate in a measurement, so you should only process those that
           the class understands and leave others alone.

           If the dictionary is empty the detector should use its previous
           preset.

           The `presetInfo` method is called to determine the presets that the
           class supports.
        """
        if self._sim_intercept:
            self._sim_preset = preset
            return
        self.doSetPreset(**preset)

    @usermethod
    def start(self, **preset):
        """Start measurement, with either the given preset or the standard
        preset.  If a preset is given, `doSetPreset` is called with that preset
        first.

        This operation is forbidden in slave mode.

        .. method:: doStart()

           This method must be present and is called to start the measurement.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'start not possible in slave mode')
        elif self._sim_intercept:
            self._sim_started = session.clock.time
            if preset:
                self._sim_preset = preset
            return
        if preset:
            self.doSetPreset(**preset)
        self.doStart()

    def __call__(self, pos=None):
        """Allow dev(), but not dev(pos)."""
        if pos is None:
            return self.read()
        raise UsageError(self, 'device cannot be moved')

    def duringMeasureHook(self, elapsed):
        """Hook called during measurement.

        This can be overridden in subclasses to perform some periodic action
        while measuring.  The hook is called by `.count` for every detector in
        a loop.  The *elapsed* argument is the time elapsed since the detector
        was started.

        If the hook returns a data quality value from ``nicos.core.data``,
        either LIVE or INTERMEDIATE, the detector is read out and the data is
        sent to the data sinks.  The detector is responsible for determining
        how often this should be done.
        """

    @usermethod
    def pause(self):
        """Pause the measurement, if possible.

        Return True if paused successfully.  This operation is forbidden in
        slave mode.

        .. method:: doPause()

           If present, this is called to pause the measurement.  Otherwise,
           ``False`` is returned to indicate that pausing is not possible.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'pause not possible in slave mode')
        elif self._sim_intercept:
            return True
        if hasattr(self, 'doPause'):
            return self.doPause()
        return False

    @usermethod
    def resume(self):
        """Resume paused measurement.

        This operation is forbidden in slave mode.

        .. method:: doResume()

           If present, this is called to resume the measurement.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'resume not possible in slave mode')
        elif self._sim_intercept:
            return
        if hasattr(self, 'doResume'):
            self.doResume()

    @usermethod
    def finish(self):
        """Finish the measurement now.  This should do nothing if the
        measurement was already finished.

        This operation is forbidden in slave mode.

        Called by `.count` for all detectors at the end of a counting.  If
        *exception* is true, the counting was stopped due to an exception.

        .. method:: doFinish()

           This method must be present and is called to actually stop the
           measurement.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'finish not possible in slave mode')
        elif self._sim_intercept:
            if hasattr(self, 'doTime'):
                time = self.doTime(self._sim_preset)
            elif 't' in self._sim_preset:
                time = self._sim_preset['t']
            else:
                time = 0
            if self._sim_started is not None:
                session.clock.wait(self._sim_started + time)
                self._sim_started = None
            return
        self.doFinish()

    @usermethod
    def stop(self):
        """Stop (abort) the measurement now.

        This operation is forbidden in slave mode.

        Called by `.count` if an exception occurs, or if the user calls the
        `stop()` command (possibly by using the immediate stop button).

        .. method:: doStop()

           This method must be present and is called to actually abort the
           measurement.
        """
        if self._mode == SLAVE:
            raise ModeError(self, 'stop not possible in slave mode')
        elif self._sim_intercept:
            return
        return self.doStop()

    @usermethod
    def read(self, maxage=None):
        """Return a list with the scalar result(s) of the last measurement."""
        if self._sim_intercept:
            if hasattr(self, 'doSimulate'):
                result = self.doSimulate(self._sim_preset)
                if not isinstance(result, list):
                    return [result]
                return result
            return [0] * len(self.valueInfo())
        # always get fresh result from cache => maxage parameter is ignored
        if self._cache:
            self._cache.invalidate(self, 'value')
        result = self._getFromCache('value', self.doRead)
        if not isinstance(result, list):
            return [result]
        return result

    def doReadArrays(self, quality):
        return []

    @usermethod
    def readArrays(self, quality):
        """Return a list with the array result(s) of the last measurement.

        *quality* is the quality of the requested result: LIVE, INTERMEDIATE,
        FINAL, or INTERRUPTED.
        """
        if self._sim_intercept:
            arrtypes = self.arrayInfo()
            return [numpy.zeros(arrtype.shape) for arrtype in arrtypes]
        result = self.doReadArrays(quality)
        if not isinstance(result, list):
            return [result]
        return result

    @usermethod
    def prepare(self):
        """Prepare measurement before counting.

        .. method:: doPrepare()

           If present, this is called to prepare the measurement.  This method
           will be called before start counting and e.g. for scans before
           moving to the next scan point.

        Preparation can be done in the background if the device status reflects
        that fact (i.e. the device is BUSY until the preparation is done).
        """
        if not self._sim_intercept:
            if hasattr(self, 'doPrepare'):
                return self.doPrepare()

    def readResults(self, quality):
        """Return a tuple of the scalar result(s) of the last measurement and
        the array result(s) of the last measurement.

        The array results will be read first calling `readArrays` because
        there migth be side-effects on the scalar result of `read`, e.g.
        `ImageChannelMixin`'s standard implementation of `doRead` returns
        `self.readresult` which will probably be altered in `doReadArrays`.
        """
        arrays = self.readArrays(quality)
        return self.read(), arrays

    def info(self):
        """Automatically add device status (if not OK).  Does not add the
        device value since that is typically not useful for Measurables.
        """
        ret = []
        try:
            st = self.status()
        except Exception as err:
            self.log.warning('error getting status', exc=err)
            errstr = 'Error: %s' % err
            ret.append(('status', (status.ERROR, errstr), errstr, '', 'status'))
        else:
            ret.append(('status', st, formatStatus(st), '', 'status'))
        return ret + Device.info(self)

    def valueInfo(self):
        """Describe the scalar values measured by this device.

        Return a tuple of :class:`~nicos.core.params.Value` instances
        describing the values that :meth:`read` returns.

        This must be overridden by every Measurable that returns more than one
        value in a list.  The default indicates a single return value with no
        additional info about the value type.
        """
        return Value(self.name, unit=self.unit),

    def arrayInfo(self):
        """Describe the array values measured by this device.

        Return a tuple of :class:`~nicos.core.params.ArrayDesc` instances
        describing the values that :meth:`readArrays` returns.

        This must be overridden by every Measurable that wants to return one or
        more arrays.
        """
        return ()

    def presetInfo(self):
        """Return an iterable of preset keys accepted by this device.

        The default implementation returns only a 't' (time) preset.  This must
        be overridden by all measurables that support more presets.
        """
        return ('t',)


class SubscanMeasurable(Measurable):
    """
    Base class for Measurables that do not count directly, but initiate a
    subscan and extract some values from that.  For this measurable, start()
    is expected to be synchronous.  `isCompleted` always returns True.

    Subclasses *need* to implement:

    * doRead(maxage=0)
    * doStart()

    Subclasses *can* implement:

    * doReadArrays(quality)
    * valueInfo()
    * arrayInfo()
    * presetInfo()
    """

    hardware_access = False

    def start(self, **preset):
        if self._mode == SLAVE:
            raise ModeError(self, 'start not possible in slave mode')
        if preset:
            self.doSetPreset(**preset)
        # XXX start subscan measurable
        self.doStart()

    def doStop(self):
        pass

    def doIsCompleted(self):
        return True


# Use the DeviceMixinMeta metaclass here to provide the instancecheck
# Not derived from DeviceMixinBase as this class is not a mixin.
class NoDevice(metaclass=DeviceMixinMeta):
    """A class that represents "no device" attached to a :class:`DeviceAlias`."""

    __mergedattrs__ = []  # needed by the metaclass

    def __init__(self, name):
        self.name = name

    def __str__(self):
        return '<none>'

    def __reduce__(self):
        return (str, (self.name,))

    def __getattr__(self, name):
        raise AttributeError('alias %r does not point to any device' %
                             self.name)

    def __setattr__(self, name, value):
        if name != 'name':
            raise ConfigurationError('alias %r does not point to any device' %
                                     self.name)
        object.__setattr__(self, name, value)


class DeviceAlias(Device):
    """
    Generic "alias" device that can point all access to any other NICOS device.

    The device that should be accessed is set using the "alias" parameter,
    which can be configured and changed at runtime.  For example, with a
    DeviceAlias instance *T*::

        T.alias = Tcryo
        read(T)   # will read Tcryo
        T.alias = Toven
        read(T)   # will read Toven

    This allows to call e.g. the sample temperature by the same name in all
    sample environment setups, but behind the scenes implement it using
    different actual hardware devices.

    If the "alias" parameter is empty, the alias points to a special "NoDevice"
    object that raises a `ConfigurationError` on every access.
    """

    parameters = {
        'alias':    Param('Device to alias', type=none_or(nicosdev),
                          settable=True, chatty=True),
        'devclass': Param('Class name that the aliased device must be an '
                          'instance of',
                          type=str, default='nicos.core.device.Device'),
    }

    _ownattrs = ['_obj', '_mode', '_cache', 'alias']
    _ownparams = {'alias', 'name', 'devclass', 'visibility'}
    _initialized = False

    __display__ = True

    def __init__(self, name, **config):
        self._obj = None
        devclass = config.get('devclass', 'nicos.core.device.Device')
        try:
            modname, clsname = devclass.rsplit('.', 1)
            self._cls = session._nicos_import(modname, clsname)
        except Exception:
            self.log.warning('could not import class %r; using Device as the '
                             'alias devclass', devclass, exc=1)
            self._cls = Device
        Device.__init__(self, name, **config)
        # update the configured alias device - we do this after the init
        # procedure to avoid calling init stuff in the aliased dev
        devname = config.get('alias')
        if devname is None and self._cache:
            devname = self._cache.get(self, 'alias', '')
        self.doUpdateAlias(devname)
        self._initialized = True

    def __repr__(self):
        if isinstance(self._obj, NoDevice):
            return '<device %s, device alias pointing to nothing>' % self._name
        if not self.description:
            return '<device %s, alias to %s>' % (self._name, self._obj)
        return '<device %s, alias to %s "%s">' % (self._name, self._obj,
                                                  self.description)

    def doUpdateAlias(self, devname, recursive=False):
        # Important NOTE: this is never called in the poller, since the poller
        # replaces DeviceAliases by a cache reader.  Therefore we get away with
        # calling _setROParam on device creation.
        if not devname:
            self._obj = NoDevice(str(self))
            if self._cache:
                self._cache.unsetRewrite(str(self))
                self._reinitParams()
            return
        try:
            newdev = session.getDevice(devname, (self._cls, DeviceAlias),
                                       source=self)
            if newdev is self:
                raise NicosError(self, 'cannot set alias pointing to itself')
            if newdev != self._obj:
                self._obj = newdev
                if self._cache:
                    self._cache.setRewrite(str(self), devname)
                    self._reinitParams()
        except Exception:
            if self._initialized:
                raise
            # On initialization, this must not raise an error, otherwise the
            # device cannot be created at all, and will fail depending devices.
            # Instead, select another target if possible.
            if not recursive:
                self._recoverMissingDevice(devname)
            else:
                # If this is a recursive call (another target already failed),
                # do not try others to avoid infinite looping through the
                # alias config here.
                self.log.exception('cannot point to %s' % devname)
                self.doUpdateAlias('')

    def _reinitParams(self):
        if self._mode != MASTER:  # only in the copy that changed the alias
            return
        # clear all cached parameters
        self._cache.clear(str(self), exclude=self._ownparams)
        # put the parameters from the original device in the cache under the
        # name of the alias
        if not isinstance(self._obj, Device):
            return
        for pname in self._obj.parameters:
            if pname in self._ownparams:
                continue
            try:
                self._cache.put(self, pname, getattr(self._obj, pname))
            except NicosError:
                pass  # silently ignore

    def _recoverMissingDevice(self, devname):
        """Recover from not having a valid target (it does not exist, cannot be
        created or has the wrong type).
        """
        # first, check the preferred alias config
        for target, _ in session.alias_config.get(self._name, []):
            if target != devname and target in session.configured_devices:
                self.log.warning('could not find aliased device %s, '
                                 'pointing to %s instead', devname, target)
                new_target = target
                break
        else:
            # then, check the config file
            fromconfig = self._config.get('alias', '')
            self.log.warning('could not find aliased device %s, pointing '
                             'to target from setup file (%s)',
                             devname, fromconfig or 'nothing')
            new_target = fromconfig
        # if we have a potential new target, check if we can get hold of it
        if new_target:
            try:
                session.getDevice(new_target, (self._cls, DeviceAlias),
                                  source=self)
            except Exception:
                # all hope is lost!
                self.log.warning('could not find %s either, pointing to '
                                 'nothing', new_target)
                new_target = ''
        # now make the new choice of alias permanent, including in the cache
        # (which we must do with _setROParam since we might not be master yet)
        self.doUpdateAlias(new_target, recursive=True)
        Device._setROParam(self, 'alias', new_target)

    # Device methods that would not be alias-aware

    def _setROParam(self, param, value):
        raise NicosError(self, "please don't use _setROParam on a DeviceAlias")

    def _getParamConfig(self, param):
        if param in self.parameters:
            return self.parameters[param]
        return self._obj._getParamConfig(param)

    def valueInfo(self):
        # override to replace name of the aliased device with the alias' name
        new_info = []
        rx = r'^%s\b' % re.escape(self._obj.name)
        for v in self._obj.valueInfo():
            new_v = v.copy()
            # replace dev name, if at start of value name, with alias name
            new_v.name = re.sub(rx, self.name, v.name)
            new_info.append(new_v)
        return tuple(new_info)

    @usermethod
    def info(self):
        """Return "device information" from the original device."""
        # override to use the object's "info" but add a note about the alias
        ret = []
        if isinstance(self._obj, Device):
            ret = self._obj.info()
        return ret + [('alias', str(self._obj), str(self._obj), '', 'instrument')]

    @usermethod
    def version(self):
        """Return a list of versions for original device."""
        v = []
        if isinstance(self._obj, Device):
            v = self._obj.version()
        v.extend(Device.version(self))
        return v

    # these methods must not be proxied

    def __eq__(self, other):
        return self is other

    def __ne__(self, other):
        return self is not other

    # use the default __hash__
    __hash__ = Device.__hash__

    def shutdown(self):
        # re-implemented from Device to avoid running doShutdown
        # of the pointed-to device prematurely
        self.log.debug('shutting down device')
        if self._mode != SIMULATION:
            # remove subscriptions to parameter value updates
            if self._cache:
                for param, func in self._subscriptions:
                    self._cache.removeCallback(self, param, func)
        session.devices.pop(self._name, None)
        session.device_case_map.pop(self._name.lower(), None)
        session.explicit_devices.discard(self._name)

    # generic proxying of missing attributes to the object

    def __getattr__(self, name):
        if not self._initialized:
            raise AttributeError(name)
        else:
            if name in DeviceAlias._ownattrs:
                return object.__getattr__(self, name)
            return getattr(self._obj, name)

    def __setattr__(self, name, value):
        if name in DeviceAlias._ownattrs or not self._initialized:
            object.__setattr__(self, name, value)
        else:
            setattr(self._obj, name, value)

    def __delattr__(self, name):
        if name in DeviceAlias._ownattrs or not self._initialized:
            object.__delattr__(self, name)
        else:
            delattr(self._obj, name)


# proxying of special methods to the object

def make_method(name):
    def method(self, *args, **kw):
        return getattr(self._obj, name)(*args, **kw)
    return method


for name in [
        '__abs__', '__add__', '__and__', '__bool__', '__call__',
        '__contains__', '__delitem__', '__divmod__', '__float__',
        '__floordiv__', '__ge__', '__getitem__', '__gt__', '__hash__',
        '__iadd__', '__iand__', '__idivmod__', '__ifloordiv__', '__ilshift__',
        '__imatmul__', '__imod__', '__imul__', '__index__', '__int__',
        '__invert__', '__ior__', '__ipow__', '__irshift__', '__isub__',
        '__iter__', '__itruediv__', '__ixor__', '__le__', '__len__',
        '__lshift__', '__lt__', '__matmul__', '__mod__', '__mul__', '__neg__',
        '__next__', '__or__', '__pos__', '__pow__', '__radd__', '__rand__',
        '__rdivmod__', '__reduce__', '__reduce_ex__', '__reversed__',
        '__rfloorfiv__', '__rlshift__', '__rmatmul__', '__rmod__', '__rmul__',
        '__ror__', '__rpow__', '__rrshift__', '__rshift__', '__rsub__',
        '__rtruediv__', '__rxor__', '__setitem__', '__sub__', '__truediv__',
        '__xor__',
]:
    if hasattr(Device, name):
        setattr(DeviceAlias, name, make_method(name))

#  -*- coding: utf-8 -*- # ***************************************************************************** # NICOS, the Networked Instrument Control System of the MLZ # Copyright (c) 2009-2022 by the NICOS contributors (see AUTHORS) # # 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: #   <NAME> <<EMAIL>> # # ***************************************************************************** Base device classes for usage in NICOS. Decorator to implement user access control.      The access is checked based on the keywords given.  Currently, the     keywords with meaning are:      * ``'level'``: gives the minimum required user access level and can       have the values ``GUEST``, ``USER`` or ``ADMIN`` as defined in the       :mod:`nicos.core.utils` module.     * ``'mode'``: gives the required exection mode ("master", "slave",       "maintenance", "simulation").     * ``'passcode'``: only usable in the interactive console: gives a       passcode that the user has to type back.      A special keyword is ``'helpmsg'``; if the access check fails, this gives     a message that is appended to the error message.      The wrapper function calls `.Session.checkAccess` to verify the     requirements.  If the check fails, `.AccessError` is raised. A metaclass that automatically adds properties for the class' parameters,     and determines a list of user methods.      It also merges these attributes defined in the class with those defined in     all base classes:      * attached_devices     * parameters     * parameter_overrides     * methods (autogenerated) # check base class ordering: mixins before others # to debug MRO problems you could use this line # print('MRO:', newtype, newtype.mro()) # adev names are always lowercased # create parameter properties # parameter names are always lowercased (enforce this) # process overrides # create the getter method # no renaming ! # create the setter method # for Moveables # allow doWrite to override the value # create a property and attach to the new device class # check attribute names for the attached devices (created by # Device.__init__) An object that has a list of parameters that are read from the     configuration and have default values.      Subclasses *can* implement:      * doPreinit()     * doInit()     * doShutdown()     * doVersion()     * doInfo() # A dictionary mapping device names to classes (or lists of classes) that # describe this device's attached (subordinate) devices. # A dictionary mapping parameter names to parameter descriptions, given as # Param objects. # Don't allow setting loglevel to > INFO, as it could be confusing. # A dictionary mapping parameter names to Override objects that override # specific properties of parameters found in base classes. # Set this to True on devices that are only created for a time, and whose # name can be reused. # Set this to True on devices that directly access hardware, and therefore # should have their actions simulated. # This is set by NICOS to indicate that do-methods should be intercepted # and their result simulated.  Combines hardware_access and device mode # at runtime. # Autogenerated inventory of the class' user methods. # Loop delay defaults.  To be set to low values in the test suite. # register self in device registry # _config: device configuration (all parameter names lower-case) # _params: parameter values from config # _infoparams: cached list of parameters to get on info() # _adevs: "attached" device instances # superdevs: reverse adevs for dependency tracking # keep an explicit record of controllers # execution mode # initialize a logger for the device # initialize device # for all devices # for image producers # for HasAutoDevices # disallow modification of any public attributes that are not # parameters or otherwise properties # this should also be forbidden at some point, but for now just # emit a warning to make sure it doesn't break the world # Used for pickling the device e.g. when sending between daemon and GUI # suppress debug/info messages from ordinary devices in the poller Validate and create attached devices. # need to relax this instance check for simulation mode; # aliases are not yet set correctly when the devices are # created Initialize the object; this is called by the NICOS system when the         device instance has been created.          This method first initializes all attached devices (creating them if         necessary), then initializes parameters.          .. XXX expand parameter init procedure          .. method:: doPreinit(mode)             This method, if present, is called before parameters are initialized            (except for parameters that have the ``preinit`` property set to            true).             This allows to initialize a hardware connection if it is necessary            for the various ``doRead...()`` methods of other parameters that            read the current parameter value from the hardware.          .. method:: doInit(mode)             This method, if present, is called after all parameters have been            initialized.  It is the correct place to set up additional            attributes, or to perform initial (read-only!) communication with            the hardware.          .. note:: ``doPreinit()`` and ``doInit()`` are called regardless of the            current execution mode.  This means that if one of these methods            does hardware access, it must be done only if ``mode != SIMULATION``. # mandatory parameters must be in config, regardless of cache # Ellipsis representing "no value" since None is a valid value for # some parameters # try to get from cache # clean up legacy, wrong values # parameter is in cache, but not in config: if it is not # settable and has a default, use that (since most probably # the default is intended to be used but has changed) # end of _init_param() # warn about parameters that weren't present in cache # subscribe to parameter value updates, if a doUpdate method exists # call custom initialization Indirection needed by the Cache client itself. Validate and coerce the value of a parameter to the correct type.          If the value can't be coerced, a ConfigurationError is raised. Get an initial value for the parameter, called when the cache         doesn't contain such a value.          If present, a doReadParam method is called.  Otherwise, the value comes         from either the setup file or the device-specific default value. # try to read from the hardware (only in non-simulation mode) # happens when called from a param getter, not from init() # will not be there in simulation mode # always call update methods, they should be working for simulation Set an otherwise read-only parameter.          This is useful for parameters that change at runtime, but indirectly,         such as "last filenumber". Return the entry for the parameter from self.parameters.          This should be used when alias resolution is desired, since it is         overridden for DeviceAliases. Get *name* from the cache, or call *func* if outdated/not present.          If the *maxage* parameter is set, do not allow the value to be older         than that amount of seconds. Format a parameter value according to its fmtstr. Set a new execution mode. # switching to simulation mode: remove cache entirely # and rely on saved _params and values Return a history of the parameter *name* (can also be ``'value'`` or         ``'status'``).          *fromtime* and *totime* can be used to limit the time window.  They can         be:          * positive numbers: interpreted as UNIX timestamps         * negative numbers: interpreted as hours back from now         * strings: in one of the formats 'HH:MM', 'HH:MM:SS',           'YYYY-MM-DD', 'YYYY-MM-DD HH:MM' or 'YYYY-MM-DD HH:MM:SS'          Default is to query the values of the last hour. # no cache is configured for this setup Return "device information" as an iterable of tuples ``(name,         raw_value, formatted_value, unit, category)``.          This "device information" is put into data files and should therefore         include any parameters that will be essential to record the current         status of the instrument.          The default implementation already collects all parameters whose         ``category`` property is set.          .. method:: doInfo()             This method can add more device information by returning it as a            sequence of tuples. Shut down the device.  This method is called by the NICOS system when         the device is destroyed, manually or because the current setup is         unloaded.          .. method:: doShutdown()             This method is called, if present, but not in simulation mode.  It            should perform cleanup, for example closing connections to hardware. # do not execute shutdown actions when simulating # remove subscriptions to parameter value updates # execute custom shutdown actions # re-raise the doShutdown error Return a list of versions for this device.          These are tuples (component, version) where a "component" can be the         name of a Python module, or an external dependency (like a TACO         server).          The base implementation already collects VCS revision information         available from all Python modules involved in the class inheritance         chain of the device class.          .. method:: doVersion()             This method is called if present, and should return a list of            (component, version) tuples that are added to the version info. Acquire an exclusive lock for using this device from the cache.  This         can be used if read access to the device needs to be locked (write         access is locked anyway, since only one NICOS session can be the master         session at a time). Release the exclusive cache lock for this device.          Always use like this::             self._cachelock_acquire()            try:                ...  # do locked operations            finally:                self._cachelock_release() Read a parameter from the hardware and put its value into the cache.          This is intended to be used from :meth:`doPoll` methods, so that they         don't have to implement parameter polling themselves.  For readable         devices, if *with_ttl* is > 0, the cached value gets the TTL of the         device value, determined by :attr:`maxage`, multiplied by *with_ttl*. Poll all parameters (normally only volatile ones). Base class for all readable devices.      Subclasses *need* to implement:      * doRead(maxage)      Subclasses *can* implement:      * doReset()     * doPoll(n, maxage)      Subclasses *can* override:      * doStatus(maxage)     * valueInfo() # value in simulation mode # no way to configure a useful default... Return info about the value range this device had in a simulation.          The return value is a list of tuples ``(value name, last value, minimum         value, maximum value)``.  By default this has one entry, where "value         name" is the device name. # save the last known value # cached value is ok here Allow dev() as shortcut for read. # give a nicer error message than "TypeError: takes 1 argument" Describe the values read by this device.          Return a tuple of :class:`~nicos.core.params.Value` instances         describing the values that :meth:`read` returns.          This must be overridden by every Readable that returns more than one         value in a list.  For example, a slit that returns a width and height         would define ::              def valueInfo(self):                 return (Value(self.name + '.width', unit=self.unit),                         Value(self.name + '.height', unit=self.unit))          By default, this returns a Value that indicates one return value with         the proper unit and format string of the device. Read the (possibly cached) main value of the device.          .. method:: doRead(maxage=0)             This method must be implemented to read the actual device value from            the device.  It is only called if the last cached value is out of            date, or no cache is available.             The *maxage* parameter should be given to read() calls of subdevices. Return the (possibly cached) status of the device.          The status is a tuple of one of the integer constants defined in the         :mod:`nicos.core.status` module, and textual extended info.          .. method:: doStatus(maxage=0)             This method can be implemented to get actual device status from the            device.  It is only called if the last cached value is out of            date, or no cache is available.             If no ``doStatus()`` is implemented, ``status()`` tries to determine            the status via `nicos.core.utils.multiStatus` of the attached            devices.  If that is not possible, it returns            ``status.UNKNOWN, 'doStatus not implemented'``.             The *maxage* parameter should be given to status() calls of            subdevices. Return the status of the device combined from hardware status and         movement status determined by NICOS such as timeouts.          The default implementation just returns the hardware status, except         that the warnlimits of the device are checked, and the status is         changed to WARN if they are exceeded. # take precision into account in case we drive exactly to the # user limit but the device overshoots a little Get status and value directly from the device and put both values         into the cache.  For continuous polling, *n* should increase by one         with every call to *poll*.          .. method:: doPoll(n, maxage)             If present, this method is called to perform additional polling,            e.g. on parameters that can be changed from outside the NICOS            system.  The *n* parameter can be used to perform the polling less            frequently than the polling of value and status.             If doPoll returns a (status, value) tuple, they are used instead of            calling doStatus and doRead again.          .. automethod:: _pollParam # updates shall always get through to the cache # self._cache.invalidate(self, 'value') # self._cache.invalidate(self, 'status') Reset the device hardware.  Returns the new status afterwards.          This operation is forbidden in slave mode, and a no-op for hardware         devices in simulation mode.          .. method:: doReset()             This method is called if implemented.  Otherwise, this is a no-op. # reset should not trigger timeoutAction() # make sure, status is propagated to the cache after a reset Format a value from :meth:`read` into a human-readable string.          The device unit is not included unless *unit* is true.          This is done using Python string formatting (the ``%`` operator) with         the :attr:`fmtstr` parameter value as the format string. Automatically add device main value and status. # only display the message for the first 5 times and then # every 20 measurements. always display if in debugmode Base class for devices that can execute some action and can be waited upon.      Subclasses *can* implement:      * doIsCompleted()      Subclasses *can* override:      * _getWaiters()     * doEstimateTime(elapsed)     * doStatus(maxage) Wait until movement of device is completed.          Return current device value after waiting.  This is a no-op for         hardware devices in simulation mode. Empty implementation of finish() for Measurables. Return a list (or adev-style dict) of all waiter adevs.          "Waiter" adevs are the subdevices whose status must leave BUSY when         waiting on self.          By default, this includes *all* adevs (of which the non-waitables are         removed), but can be overridden to remove some waitable devices as         well, in case they are not important to determine the status of this         device. # overridden from Readable to take only waiters into account Check for completion of movement.          The default implementation, which should only be changed in special         cases, checks the device status and acts according to the         :attr:`busystates` and :attr:`errorstates` attributes of the device         (which are sequences of status constants).  If the device is in a busy         state, this method returns ``False``, if it is in an error state, it         raises.          .. attribute:: busystates             A tuple of status constants that indicate that the device is busy.            Default is: ``(status.BUSY,)``.          .. attribute:: errorstates             Mapping of status constants that indicate that the device movement            has stopped with an error to exception class to raise.  Default is:            ``{status.ERROR: MoveError, status.NOTREACHED: PositionError,            status.DISABLED: MoveError}``.             This can be overridden in a derived class to provide different            behavior when waiting for a device.  For example, ``NOTREACHED``            could be taken out of the errorstates if the user wants to continue            in this case.             If the states depend on device parameters, this can be implemented            as a property.          .. method:: doIsCompleted()             This method can be implemented to override :meth:`isCompleted`. # only read and log the device position if debugging # as this could be expensive and is not needed otherwise Return the estimated time until end of movement/measurement or None.          Called by ``count()`` for every detector, and ``wait()``/``multiWait()``         for moving devices.  The result is shown to the user.  The *elapsed*         argument is the time elapsed since the detector was started.  If the         time needed can not (yet) be estimated return None, otherwise a float.          .. method:: doEstimateTime             This can be overridden in subclasses to calculate an estimation of            the remaining time needed to complete the measurement.             If possible avoid recursion here as all active detectors are polled            anyway. Base class for moveable devices.      Subclasses *need* to implement:      * doStart(value)      Subclasses *can* implement:      * doStop()     * doIsAllowed()     * doTime()     * doIsAtTarget() #: The type of the device value, used for typechecking in doStart(). Allow dev() and dev(newpos) as shortcuts for read and start. # allow tuple values to be spelled as individual items, # e.g. slit(1, 2, 3, 4) instead of slit((1, 2, 3, 4)) # By default, something that has limits has a floating (analog) value. # if user and abs limits are completely disjoint, reset # if userlimits are both zero, assume failure to read # and try again # otherwise restrict user to be within abs limits # special case: in simulation mode, doReadUserlimits is not # called, so the limits are not set from the absolute limits, # and are always (0, 0) except when set in the setup file # Set them directly (bypassing pos check, as this may work at # this point for simulation) Check if the given position can be moved to.          The return value is a tuple ``(valid, why)``.  The first item is a         boolean indicating if the position is valid, the second item is a         string with the reason if it is invalid.          .. method:: doIsAllowed(pos)             This method must be implemented to check the validity.  If it does            not exist, all positions are valid.             Note: to implement ordinary (min, max) limits, do not use this            method but inherit your device from :class:`HasLimits`.  This takes            care of all limit processing.             For complex state-dependent limit use a controller device            with :class:`IsController` implementing :meth:`isADevTargetAllowed`. Start movement of the device to a new position.          This method should not generally wait for completion of the movement,         although individual devices can implement it that way if it is         convenient.          The validity of the given *pos* is checked by calling :meth:`isAllowed`         before :meth:`doStart` is called.          This operation is forbidden in slave mode.  In simulation mode, it sets         an internal variable to the given position for hardware devices instead         of calling :meth:`doStart`.          .. method:: doStart(pos)             This method must be implemented and actually move the device to the            new position. Do all checks if we can move to the given pos.          Returns new, potentially type-converted pos, or Ellipsis if move         should not be started. # try to determine if we are already there # this may raise if the position values are not numbers Start movement, assuming that _check_start has been done. # if the target hasn't changed, make the poller receive the correct # event in any case Calculate the time to move from one position to another.          Return the estimated moving time in seconds.         This is a pure calculatory method which may be overwritten in derived         classes.          This basic implementation estimates the moving time         by evaluating the `speed` or `ramp` parameters, if they are defined.         If no calculation can be performed, return 0. # speed is in physical units per second and ramp is per minute Finish up movement of the device.          This is the final step of waiting for a device, it is called by the         wait loop.  Devices must not rely on this method being called, since         it is not triggered when the movement is not waited upon.          Default implementation is to check if the target has been reached, and         to call :meth:`doFinish` if present.          .. method:: doFinish()             This method is called as part of finish().  If it returns False, the            target check is skipped. # call doFinish # do a final read of the device # check reached value to be equal to target Check if the device has arrived at the given target.         If target is omitted it defaults to the device's currently set target.         If pos is omitted the device checks against its current read value.          The method calls :meth:`doIsAtTarget` if present.  Otherwise, it checks         for equality if the value is of string or integer type, and returns         true otherwise.          This is the final step of waiting for a device, called by the         :meth:`finish` method.  Currently, returning False here only means a         warning, but may be upgraded to an exception later.          .. method:: doIsAtTarget()             Called to determine if the device value is at its target within            an accuracy that is reasonable for the individual device.             If the device wants to warn about its value, but not trigger an            exception, this method should emit a warning and still return True.          For devices with float values, inherit from :class:`HasPrecision`,         which already comes with an implementation of :meth:`doIsAtTarget`. Wait until hardware status is not BUSY. Move to target and wait for completion.          Equivalent to ``dev.start(target); return dev.wait()``. Stop any movement of the device.          This operation is forbidden in slave mode, and a no-op for hardware         devices in simulation mode.          .. method:: doStop()             This is called to actually stop the device.  If not present,            :meth:`stop` will try to stop all attached_devices, if any.            Otherwise this is a no-op.          The `stop` method will return the device status after stopping. Fix the device: don't allow movement anymore.          This blocks :meth:`start` or :meth:`stop` when called on the device. # fixed and not enough rights Release the device, i.e. undo the effect of fix(). # fixed and not enough rights return the estimated time until end of movement or return None Base class for devices used for data acquisition.      Subclasses *need* to implement:      * doRead(maxage=0)     * doSetPreset(**preset)     * doStart()     * doFinish()     * doStop()      Subclasses *can* implement:      * doStatus(maxage=0)     * doReadArrays(quality)     * doIsCompleted()     * doPause()     * doResume()     * doTime(**preset)     * doSimulate(**preset)     * doPrepare()     * valueInfo()     * arrayInfo()     * presetInfo()     * duringMeasureHook(elapsed) # overwritten from Readable: don't read out detectors, it's not useful Set the new standard preset for this detector.          .. method:: doSetPreset(**preset)             This method must be present and is called to apply presets to the            detector. Preset names can be any string, corresponding to some kind            of preselection that the detector supports.             Usually a 't' preset (time in seconds) is supported.             The dictionary passed contains the keys for *all* detectors that            participate in a measurement, so you should only process those that            the class understands and leave others alone.             If the dictionary is empty the detector should use its previous            preset.             The `presetInfo` method is called to determine the presets that the            class supports. Start measurement, with either the given preset or the standard         preset.  If a preset is given, `doSetPreset` is called with that preset         first.          This operation is forbidden in slave mode.          .. method:: doStart()             This method must be present and is called to start the measurement. Allow dev(), but not dev(pos). Hook called during measurement.          This can be overridden in subclasses to perform some periodic action         while measuring.  The hook is called by `.count` for every detector in         a loop.  The *elapsed* argument is the time elapsed since the detector         was started.          If the hook returns a data quality value from ``nicos.core.data``,         either LIVE or INTERMEDIATE, the detector is read out and the data is         sent to the data sinks.  The detector is responsible for determining         how often this should be done. Pause the measurement, if possible.          Return True if paused successfully.  This operation is forbidden in         slave mode.          .. method:: doPause()             If present, this is called to pause the measurement.  Otherwise,            ``False`` is returned to indicate that pausing is not possible. Resume paused measurement.          This operation is forbidden in slave mode.          .. method:: doResume()             If present, this is called to resume the measurement. Finish the measurement now.  This should do nothing if the         measurement was already finished.          This operation is forbidden in slave mode.          Called by `.count` for all detectors at the end of a counting.  If         *exception* is true, the counting was stopped due to an exception.          .. method:: doFinish()             This method must be present and is called to actually stop the            measurement. Stop (abort) the measurement now.          This operation is forbidden in slave mode.          Called by `.count` if an exception occurs, or if the user calls the         `stop()` command (possibly by using the immediate stop button).          .. method:: doStop()             This method must be present and is called to actually abort the            measurement. Return a list with the scalar result(s) of the last measurement. # always get fresh result from cache => maxage parameter is ignored Return a list with the array result(s) of the last measurement.          *quality* is the quality of the requested result: LIVE, INTERMEDIATE,         FINAL, or INTERRUPTED. Prepare measurement before counting.          .. method:: doPrepare()             If present, this is called to prepare the measurement.  This method            will be called before start counting and e.g. for scans before            moving to the next scan point.          Preparation can be done in the background if the device status reflects         that fact (i.e. the device is BUSY until the preparation is done). Return a tuple of the scalar result(s) of the last measurement and         the array result(s) of the last measurement.          The array results will be read first calling `readArrays` because         there migth be side-effects on the scalar result of `read`, e.g.         `ImageChannelMixin`'s standard implementation of `doRead` returns         `self.readresult` which will probably be altered in `doReadArrays`. Automatically add device status (if not OK).  Does not add the         device value since that is typically not useful for Measurables. Describe the scalar values measured by this device.          Return a tuple of :class:`~nicos.core.params.Value` instances         describing the values that :meth:`read` returns.          This must be overridden by every Measurable that returns more than one         value in a list.  The default indicates a single return value with no         additional info about the value type. Describe the array values measured by this device.          Return a tuple of :class:`~nicos.core.params.ArrayDesc` instances         describing the values that :meth:`readArrays` returns.          This must be overridden by every Measurable that wants to return one or         more arrays. Return an iterable of preset keys accepted by this device.          The default implementation returns only a 't' (time) preset.  This must         be overridden by all measurables that support more presets. Base class for Measurables that do not count directly, but initiate a     subscan and extract some values from that.  For this measurable, start()     is expected to be synchronous.  `isCompleted` always returns True.      Subclasses *need* to implement:      * doRead(maxage=0)     * doStart()      Subclasses *can* implement:      * doReadArrays(quality)     * valueInfo()     * arrayInfo()     * presetInfo() # XXX start subscan measurable # Use the DeviceMixinMeta metaclass here to provide the instancecheck # Not derived from DeviceMixinBase as this class is not a mixin. A class that represents "no device" attached to a :class:`DeviceAlias`. # needed by the metaclass Generic "alias" device that can point all access to any other NICOS device.      The device that should be accessed is set using the "alias" parameter,     which can be configured and changed at runtime.  For example, with a     DeviceAlias instance *T*::          T.alias = Tcryo         read(T)   # will read Tcryo         T.alias = Toven         read(T)   # will read Toven      This allows to call e.g. the sample temperature by the same name in all     sample environment setups, but behind the scenes implement it using     different actual hardware devices.      If the "alias" parameter is empty, the alias points to a special "NoDevice"     object that raises a `ConfigurationError` on every access. # update the configured alias device - we do this after the init # procedure to avoid calling init stuff in the aliased dev # Important NOTE: this is never called in the poller, since the poller # replaces DeviceAliases by a cache reader.  Therefore we get away with # calling _setROParam on device creation. # On initialization, this must not raise an error, otherwise the # device cannot be created at all, and will fail depending devices. # Instead, select another target if possible. # If this is a recursive call (another target already failed), # do not try others to avoid infinite looping through the # alias config here. # only in the copy that changed the alias # clear all cached parameters # put the parameters from the original device in the cache under the # name of the alias # silently ignore Recover from not having a valid target (it does not exist, cannot be         created or has the wrong type). # first, check the preferred alias config # then, check the config file # if we have a potential new target, check if we can get hold of it # all hope is lost! # now make the new choice of alias permanent, including in the cache # (which we must do with _setROParam since we might not be master yet) # Device methods that would not be alias-aware # override to replace name of the aliased device with the alias' name # replace dev name, if at start of value name, with alias name Return "device information" from the original device. # override to use the object's "info" but add a note about the alias Return a list of versions for original device. # these methods must not be proxied # use the default __hash__ # re-implemented from Device to avoid running doShutdown # of the pointed-to device prematurely # remove subscriptions to parameter value updates # generic proxying of missing attributes to the object # proxying of special methods to the object