text: #  -*- coding: utf-8 -*-
  # *****************************************************************************
  # NICOS, the Networked Instrument Control System of the MLZ
  # Copyright (c) 2009-2021 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:
  #   Georg Brandl <georg.brandl@frm2.tum.de>
  #
  # *****************************************************************************
  
  """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, \
      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)
  
          # hack around a class creation bug in Python 2
          attrs['__constructed__'] = True
  
          # 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))
              # backwards compatibility: convert all entries to Attach objects
              if not isinstance(entry, Attach):
                  session.log.warning("Please use 'Attach' to define an attached"
                                      " device. The use of a tuple is deprecated!")
                  _multiple = isinstance(entry[0], list)
                  _devclass = entry[0][0] if _multiple else entry[0]
                  newtype.attached_devices[adevname] = \
                      Attach(entry[1], _devclass, multiple=_multiple)
  
          # 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),
          'lowlevel':    Param('Whether the device is not interesting to users',
                               type=bool, default=False, userparam=False,
                               prefercache=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
      }
  
      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', 'lowlevel'}
      _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
              self._cache.put(self, pname, getattr(self._obj, pname))
  
      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))
  