Beispiel #1
0
class RpioActuator(AbstractActuator):
    """
        Boolean-valued actuator for setting Raspberry Pi GPIO port statuses (on/off).
    """

    _status = CBool(transient=True)

    #: GPIO port id
    port = CInt

    #: Set to True to have inversed status value
    inverted = CBool(False)

    _hw_service = Instance(AbstractSystemService, transient=True)

    view = AbstractActuator.view + ["port"]

    def setup(self):
        self._hw_service = self.system.request_service('RpioService')
        self._hw_service.enable_output_port(self.port)

    def _port_changed(self, old, new):
        if not self._hw_service:
            return
        if old:
            self._hw_service.disable_output_port(old)

        self._hw_service.enable_output_port(new)

    def _status_changed(self):
        self.logger.debug("%s gpio actuator status changed %s", self.name,
                          repr(self._status))
        self._hw_service.set_output_port_status(
            self.port, self._status if not self.inverted else not self._status)
Beispiel #2
0
class ArduinoDigitalSensor(AbstractArduinoSensor):
    """
        Boolean-valued sensor object for digital Arduino input pins
    """

    _status = CBool

    #: Enable built-in pull-up resistor
    pull_up_resistor = CBool(False)

    #: Set to True to have inversed status value
    inverted = CBool(False)

    def setup(self, *args, **kwargs):
        super().setup(*args, **kwargs)
        # TODO this is temporary message to aid debugging
        self.logger.warning('Setup digital sensor %d (pull_up: %s)', self.pin,
                            self.pull_up_resistor)
        self._arduino.subscribe_digital(self.pin, self)
        if self.pull_up_resistor:
            self._enable_pullup()

    def _enable_pullup(self):
        self._arduino.set_pin_mode(self.pin, arduino_service.PIN_MODE_PULLUP)

    def set_status(self, status, **kwargs):
        if self.inverted:
            status = not status
        return super().set_status(status, **kwargs)

    def cleanup(self):
        self._arduino.unsubscribe_digital(self.pin)
Beispiel #3
0
class MotorSettings(HasTraits):
    """Defines some settable parameters of USMC controller. See Standa manual
    for parameter details. Note that only a few parameters are defined here.
    """
    #USMC parameters
    AccelT = Float(desc='acceleration time (in ms)', style='readonly')
    DecelT = Float(desc='deceleration time (in ms)')
    PTimeout = Float(
        desc='time (in ms) after which current will be reduced to 60% of normal'
    )
    MaxLoft = Range(0,
                    2**16,
                    100,
                    desc='value in full steps for backlash operation')
    LoftPeriod = Float(1000, desc='speed of the last stage of backlash ')
    #USMC start parameters
    SlStart = CBool(True, desc='slow start/stop mode')
    SDivisor = Enum(8, [1, 2, 4, 8], desc='step divisor')
    ForceLoft = CBool(
        False,
        desc=
        'force backlash when target position is the same as current position')
    LoftEn = CBool(False, desc='backlash operation enabled')
    DefDir = CBool(False, desc='direction for backlash operation')
    #USMC Mode
    Tr1En = CBool(True, desc='limit switch 1 enabled')
    Tr2En = CBool(True, desc='limit switch 2 enabled')
    Tr1T = CBool(True, desc='limit switch 1 TRUE state')
    Tr2T = CBool(True, desc='limit switch 2 TRUE state')
    #extra parameters
    speed = Float(200., desc='motor speed in steps/s')
    reversed = Bool(False, desc='if motor rotation is reversed or not')

    view = motor_settings_view
Beispiel #4
0
class AbstractPollingSensor(AbstractSensor):
    """ Abstract baseclass for sensor that polls periodically its status"""

    #: How often to do polling
    interval = CFloat(5)

    #: This can be used to enable/disable polling
    poll_active = CBool(True)

    _stop = CBool(False, transient=True)
    _pollthread = Any(transient=True)
    view = AbstractSensor.view + ["interval"]
    silent = CBool(True)
    history_frequency = CFloat(1.0)

    def setup(self):
        self._restart()

    def _poll_active_changed(self, old, new):
        if not self.traits_inited():
            return
        if new:
            self._restart()
        else:
            self._pollthread.cancel()

    def _restart(self):
        if self._stop:
            return
        if self._pollthread and self._pollthread.is_alive():
            self._pollthread.cancel()
        if self.poll_active:
            self.update_status()
            self._pollthread = threading.Timer(
                self.interval, threaded(self.system, self._restart))
            time_after_interval = datetime.now() + timedelta(
                seconds=self.interval)
            self._pollthread.name = "PollingSensor: %s next poll at %s (%.2f sek)" % (
                self.name, time_after_interval, self.interval)
            self._pollthread.start()

    def update_status(self):
        pass

    def cleanup(self):
        self._stop = True
        if self._pollthread:
            self._pollthread.cancel()
Beispiel #5
0
class StatusSaverService(AbstractUserService):
    """
        Service which is responsible for scheduling dumping system into file periodically.
    """

    autoload = True

    #: Dump saving interval, in seconds. Default 30 minutes.
    dump_interval = CFloat(30 * 60)

    _exit = CBool(False)
    _timer = Any(transient=True)

    def setup(self):
        if self.system.filename:
            self.system.on_trait_change(self.exit_save, "pre_exit_trigger")
            if self.dump_interval:
                self.save_system_periodically()

    def save_system_periodically(self):
        self.logger.debug('Saving system state')
        self.system.save_state()
        self._timer = Timer(self.dump_interval, self.save_system_periodically)
        self._timer.start()

    def exit_save(self):
        self.system.save_state()
        self._exit = True

    def cleanup(self):
        if self._timer and self._timer.is_alive():
            self._timer.cancel()
Beispiel #6
0
class ArduinoRemoteDigitalSensor(AbstractArduinoSensor):
    """
        Sensor which listens to status changes of remote digital input pin
        (transmission via VirtualWire).

        Needs `AutomateFirmata <https://github.com/tuomas2/AutomateFirmata>`_
    """

    _status = CBool

    #: Source device number
    device = CInt

    #: Set to True to have inversed status value
    inverted = CBool(False)

    def setup(self, *args, **kwargs):
        super().setup(*args, **kwargs)
        self._arduino.subscribe_virtualwire_digital_broadcast(
            self, self.device)
        self._arduino.send_virtualwire_command(
            self.device, arduino_service.VIRTUALWIRE_SET_PIN_MODE, self.pin,
            pyfirmata.INPUT)

    def set_status(self, status, **kwargs):
        if self.inverted:
            status = not status
        return super().set_status(status, **kwargs)

    def cleanup(self):
        self._arduino.unsubscribe_virtualwire_digital_broadcast(
            self, self.device)
class EpsFactorized(Component):
    '''
    Factorized dielectric component.
    
    Input:
    eps_inf: float, int
        The high frequency dielectric constant.
    wLO, wTO: float
        The LO and TO mode energies (default units are eV).
    gLO, gTO: float
        The LO and TO mode lifetime (default units are eV).
    '''
    
    test_gui = CBool(True)
    
    def __init__(self, eps_inf=1, wLO=171., gLO=1.1, wTO=93., gTO=20.):
        # Define the parameters
        Component.__init__(self, ('eps_inf', 'wLO', 'gLO', 'wTO', 'gTO'))
        self._id_name = 'd56098f3-cb7a-4313-8aca-9862344be0d0'
        
        #Set the initial values
        self.eps_inf.value = eps_inf
        self.wLo.value = wLO
        self.gLO.value = gLO
        self.wTO.value = wTO
        self.gTO.value = gTO
        
        #Set the units
        self.eps_inf.units = None
        self.wLo.units = 'eV'
        self.gLO.units = 'eV'
        self.wTO.units = 'eV'
        self.gTO.units = 'eV'
        
        #Set the bounds
        self.eps_inf.bmin = 1.
        self.wLo.bmin = 0.
        self.gLO.bmin = 0.
        self.wTO.bmin = 0.
        self.gTO.bmin = 0. 
        
        self.eps_inf.bmax = None 
        self.wLo.bmax = None 
        self.gLO.bmax = None 
        self.wTO.bmax = None  
        self.gTO.bmax = None 
        
        # Define the function as a function of the already defined parameters,
        # x being the independent variable value
        def function(self, x):
            ei = self.eps_inf.value
            wL = self.wLo.value
            gL = self.gLO.value
            wT = self.wTO.value
            gT = self.gTO.value
            return ei * (wL**2 - E**2 - 1j*gL*x) / (wT**2 - E**2 - 1j*gT*x)
Beispiel #8
0
class TemperatureSensor(AbstractPollingSensor):

    """
        W1 interface (on Raspberry Pi board) that polls polling temperature.
        (kernel modules w1-gpio and w1-therm required).
        Not using RPIO, but placed this here, since this is also Raspberry Pi related sensor.
    """

    _status = CFloat

    #: Address of W1 temperature sensor (something like ``"28-00000558263c"``), see what you have in
    #: ``/sys/bus/w1/devices/``
    addr = CUnicode
    view = list(set(UserFloatSensor.view + AbstractPollingSensor.view + ["addr"]))

    #: Maximum jump in temperature, between measurements. These temperature sensors
    #: tend to give sometimes erroneous results.
    max_jump = CFloat(5.0)

    _error_count = Int(0, transient=True)

    #: Maximum number of erroneous measurements, until value is really set
    max_errors = Int(5)

    _first_reading = CBool(True, transient=True)

    def get_status_display(self, **kwargs):
        if 'value' in kwargs:
            value = kwargs['value']
        else:
            value = self.status
        return u"%.1f ⁰C" % value

    def update_status(self):
        w1file = "/sys/bus/w1/devices/%s/w1_slave" % self.addr
        try:
            f = open(w1file)
        except IOError:
            self.logger.error("IO-error, can't open %s, not set", w1file)
            return

        try:
            temp = float(f.read().split("\n")[1].split(" ")[9].split("=")[1]) / 1000.
        except IOError:
            self.logger.error("IO-error, cant' read %s, not set", w1file)
            return
        if (abs(temp-self.status) > self.max_jump
                and self._error_count < self.max_errors
                and not self._first_reading):
            self._error_count += 1
        else:
            self._first_reading = False
            self._error_count = 0
            self.set_status(temp)
        f.close()
class MyComponent(Component):

    """
    """

    test_gui = CBool(True)

    def __init__(self, parameter_1=1, parameter_2=2):
        # Define the parameters
        Component.__init__(self, ('parameter_1', 'parameter_2'))
        self._id_name = 'dcbd09ee-a420-4700-b60d-3a59e07e1237'

        # Optionally we can set the initial values
        self.parameter_1.value = parameter_1
        self.parameter_2.value = parameter_2

        # The units (optional)
        self.parameter_1.units = 'Tesla'
        self.parameter_2.units = 'Kociak'

        # Once defined we can give default values to the attribute
        # For example we fix the attribure_1 (optional)
        self.parameter_1.free = False

        # And we set the boundaries (optional)
        self.parameter_1.bmin = 0.
        self.parameter_1.bmax = None

        # Optionally, to boost the optimization speed we can also define
        # the gradients of the function we the syntax:
        # self.parameter.grad = function
        self.parameter_1.grad = self.grad_parameter_1
        self.parameter_2.grad = self.grad_parameter_2

    # Define the function as a function of the already defined parameters,
    # x being the independent variable value
    def function(self, x):
        p1 = self.parameter_1.value
        p2 = self.parameter_2.value
        return p1 + x * p2

    # Optionally define the gradients of each parameter
    def grad_parameter_1(self, x):
        """
        Returns d(function)/d(parameter_1)
        """
        return 0

    def grad_parameter_2(self, x):
        """
        Returns d(function)/d(parameter_2)
        """
        return x
class EpsLorentzDrude(Component):
    '''
    Lorentz-Drude dielectric component.
    
    Input:
    eps_inf: float, int
        The high frequency dielectric constant.
    wLO, wTO: float
        The LO and TO mode energies (default units are eV).
    gamma: float
        The lifetime (default units are eV).
    '''
    
    test_gui = CBool(True)
    
    def __init__(self, eps_inf=1, wLO=171., wTO=93., gamma=1.1):
        # Define the parameters
        Component.__init__(self, ('eps_inf', 'wLO', 'wTO', 'gamma'))
        self._id_name = '838abcb5-0a09-4e8c-b5dd-1f28e77fc475'
        
        #Set the initial values
        self.eps_inf.value = eps_inf
        self.wLo.value = wLO
        self.wTO.value = wTO
        self.gamma.value = gamma
        
        #Set the units
        self.eps_inf.units = None
        self.wLo.units = 'eV'
        self.wTO.units = 'eV'
        self.gamma.units = 'eV'
        
        #Set the bounds
        self.eps_inf.bmin = 1.
        self.wLo.bmin = 0.
        self.wTO.bmin = 0.
        self.gamma.bmin = 0. 
        
        self.eps_inf.bmax = None 
        self.wLo.bmax = None 
        self.wTO.bmax = None  
        self.gamma.bmax = None 
        
        # Define the function as a function of the already defined parameters,
        # x being the independent variable value
        def function(self, x):
            ei = self.eps_inf.value
            wL = self.wLo.value
            wT = self.wTO.value
            g = self.gamma.value
            return ei * (1 + (wL**2 - wT**2) / (wT**2 - E**2 - 1j*g*x)
Beispiel #11
0
class ArduinoDigitalActuator(AbstractArduinoActuator):
    """
        Boolean-valued actuator object for digital Arduino output pins
    """
    _status = CBool(transient=True)

    def setup(self, *args, **kwargs):
        super().setup(*args, **kwargs)
        self._arduino.setup_digital(self.pin)

    def _status_changed(self):
        self._arduino.change_digital(self.pin, self._status)

    def cleanup(self):
        self._arduino.cleanup_digital_actuator(self.pin)
Beispiel #12
0
class RpioSensor(UserBoolSensor):

    """
        Boolean-valued sensor object that reads Raspberry Pi GPIO input pins.
    """

    user_editable = CBool(False)

    #: GPIO port
    port = Int

    #: Set to True to have inversed status value
    inverted = Bool(False)

    #: Button setup: "down": pushdown resistor, "up": pushup resistor, or "none": no resistor set up.
    button_type = Enum("down", "up", "none")

    view = UserBoolSensor.view + ["port", "button_type"]

    history_frequency = CFloat(1.0)

    _hw_service = Instance(AbstractSystemService, transient=True)

    def setup(self):
        self._hw_service = self.system.request_service('RpioService')
        self._hw_service.enable_input_port(self.port, self.gpio_callback, self.button_type)

    def _button_type_changed(self, new):
        if self._hw_service:
            self._hw_service.disable_input_port(self.port)
            self._hw_service.enable_input_port(self.port, self.gpio_callback, new)

    def gpio_callback(self, gpio_id, value):
        self.set_status(value if not self.inverted else not value)

    def update_status(self):
        self.gpio_callback(None, self._hw_service.get_input_status(self.port))

    def _port_changed(self, old, new):
        if not self._hw_service:
            return
        if old:
            self._hw_service.disable_input_port(old)
        self._hw_service.enable_input_port(new, self.gpio_callback, self.button_type)
Beispiel #13
0
class AbstractArduinoSensor(AbstractSensor):
    """
        Abstract base class for Arduino sensors
    """

    history_frequency = CFloat(1.0)
    user_editable = CBool(False)

    #: Arduino service number (specify, if more than 1 ArduinoServices are configured in system)
    service = CInt(0)

    #: Arduino pin number
    pin = CInt

    _arduino = Instance(AbstractSystemService, transient=True)

    def setup(self, *args, **kwargs):
        super().setup(*args, **kwargs)
        self.logger.debug('Arduino sensor setup')
        self._arduino = self.system.request_service('ArduinoService',
                                                    self.service)
Beispiel #14
0
class ArduinoRemoteDigitalActuator(AbstractArduinoActuator):
    """
        Actuator that sends target device digital output pin status change requests

        Needs `AutomateFirmata <https://github.com/tuomas2/AutomateFirmata>`_
    """

    _status = CBool(transient=True)

    #: Target device number
    device = CInt

    def setup(self, *args, **kwargs):
        super().setup(*args, **kwargs)
        self._arduino.send_virtualwire_command(
            self.device, arduino_service.VIRTUALWIRE_SET_PIN_MODE, self.pin,
            pyfirmata.OUTPUT)

    def _status_changed(self):
        self._arduino.send_virtualwire_command(
            self.device, arduino_service.VIRTUALWIRE_SET_DIGITAL_PIN_VALUE,
            self.pin, self.status)
Beispiel #15
0
class AbstractService(HasTraits):
    """
        Base class for System and UserServices
    """

    #: If set to *True*, service is loaded automatically (if not explicitly prevented
    #: in :attr:`automate.system.System.exclude_services`). Overwrite this in subclasses,
    autoload = False
    system = Instance(SystemBase)
    logger = Instance(logging.Logger, transient=True)
    log_level = CInt(logging.INFO)
    is_mocked = CBool(False, transient=True)
    _id = 0

    @property
    def id(self):
        return id(self)

    @property
    def name(self):
        return '%s.%s' % (self.__class__.__name__, self._id)

    def _log_level_changed(self, new_value):
        if self.logger:
            self.logger.setLevel(new_value)

    @property
    def initialized(self):
        return bool(self.system)

    def setup_system(self, system, name=None, id=0):
        self.system = system
        self._id = id
        self.logger = self.system.logger.getChild(self.name)
        self.logger.setLevel(self.log_level)
        self.logger.info('Setup')
        self.system.register_service(self)
        self.setup()

    def cleanup_system(self):
        if self.system:
            self.logger.info('Cleaning up')
            self.cleanup()

    def setup(self):
        """
            Initialize service here. Define in subclasses.
        """

    def cleanup(self):
        """
            Cleanup actions must be performed here. This must be blocking until service is
            fully cleaned up.

            Define in subclasses.
        """

    def reload(self):
        self.cleanup()
        self.setup()
        self.logger.info('Reloading %s ready!', self)

    def __repr__(self):
        return '<' + self.__class__.__name__ + ' instance%s>' % (
            '' if self.system else ' not initialized')
Beispiel #16
0
class BoolActuator(AbstractActuator):

    """ Boolean valued actuator"""
    _status = CBool(transient=True)
    default = CBool
Beispiel #17
0
class ArduinoService(AbstractSystemService):
    """
        Service that provides interface to Arduino devices via
        `pyFirmata library <https://github.com/tino/pyFirmata>`_.
    """

    #: Arduino devices to use, as a list
    device = Str("/dev/ttyUSB0")

    #: Arduino device board type, choices: 'arduino', 'arduino_mega', 'arduino_due'.
    device_type = Str('arduino')

    #: Arduino sample rate in milliseconds (i.e. how often to send data to host)
    sample_rate = Int(500)

    #: Device home address (0-255) in VirtualWire network. This should be same for all
    #: devices in a network.
    home_address = CInt(0)

    #: Device address (0-255) in VirtualWire network. Unique for each device.
    device_address = CInt(0)

    #: VirtualWire transfer pin
    virtualwire_tx_pin = CInt(0)

    #: VirtualWire receiver pin
    virtualwire_rx_pin = CInt(0)

    #: VirtualWire PTT (push to talk) pin
    virtualwire_ptt_pin = CInt(0)

    #: LCD I2C Port (often 0x27), 0 to disable I2C LCD
    lcd_port = CInt(0)

    #: LCD Columns
    lcd_columns = CInt(16)

    #: LCD Rows
    lcd_rows = CInt(2)

    #: Report pin values on LCD
    lcd_reporting = CBool(False)

    #: Wakeup pin (0 to disable) that wakes Arduino from sleep mode. On Atmega328 based boards,
    #: possible values are 2 and 3
    wakeup_pin = CInt(0)

    #: VirtualWire speed, (bit rate = speed * 1000 bps). Values 1-9 are allowed.
    #: Values up to 7 should be working, but your mileage may vary.
    virtualwire_speed = CInt(2)

    #: Send keep-alive messages periodically over serial port to prevent Arduino device from
    #: falling to power save mode.
    keep_alive = CBool(True)

    #: Set analog reference (Note: Arduino platform dependent constants)
    analog_reference = CInt(ANALOG_REFERENCE_UNSET)

    #: Report digital ports instantly? If disabled, digital ports are reported only
    #: periodically with sample rate.
    instant_digital_reporting = CBool(True)

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self._sens_analog = {}
        self._sens_digital = {}
        self._sens_virtualwire_digital = defaultdict(
            list)  # source device -> list of sensors
        self._sens_virtualwire_analog = defaultdict(
            list)  # source device -> list of sensors
        self._act_digital = {}
        self._board = None
        self._lock = None
        self._iterator_thread = None
        self._keepalive_thread = None

    class FileNotReadableError(Exception):
        pass

    def setup(self):
        self.logger.debug("Initializing Arduino subsystem")

        # Initialize configured self.boards

        try:
            if not os.access(self.device, os.R_OK):
                raise self.FileNotReadableError
            board = pyfirmata.Board(self.device,
                                    layout=pyfirmata.BOARDS[self.device_type])
            self._board = board
            self.is_mocked = False
        except (self.FileNotReadableError, OSError) as e:
            if isinstance(
                    e,
                    self.FileNotReadableError) or e.errno == os.errno.ENOENT:
                self.logger.warning(
                    'Your arduino device %s is not available. Arduino will be mocked.',
                    self.device)
                self._board = None
                self.is_mocked = True
            else:
                raise
        self._lock = Lock()
        if self._board:
            self._board.add_cmd_handler(pyfirmata.STRING_DATA,
                                        self._string_data_handler)
            self._iterator_thread = it = threading.Thread(
                target=threaded(self.system, iterate_serial, self._board))
            it.daemon = True
            it.name = "PyFirmata thread for {dev}".format(dev=self.device)
            it.start()

            self.reset()
            self.configure_sample_rate()
            self.configure_instant_digital_reporting()
            self.configure_virtualwire()
            self.configure_lcd()
            self.configure_analog_reference()
            self._keep_alive()

    def configure_virtualwire(self):
        if not self._board:
            return
        with self._lock:
            self.logger.debug('Configuring virtualwire')
            self._board.send_sysex(SYSEX_SETUP_VIRTUALWIRE, [
                self.virtualwire_rx_pin,
                self.virtualwire_tx_pin,
                self.virtualwire_ptt_pin,
                self.wakeup_pin,
                self.virtualwire_speed,
                self.home_address,
                self.device_address,
            ])
            self.setup_virtualwire_input()

    def reset(self):
        if not self._board:
            return
        self.write(pyfirmata.SYSTEM_RESET)

    def configure_sample_rate(self):
        if not self._board:
            return
        self._board.send_sysex(pyfirmata.SAMPLING_INTERVAL,
                               pyfirmata.util.to_two_bytes(self.sample_rate))

    def configure_instant_digital_reporting(self):
        if not self._board:
            return
        self._board.send_sysex(SYSEX_SET_INSTANT_DIGITAL_REPORTING,
                               [self.instant_digital_reporting])

    def configure_analog_reference(self):
        if self.analog_reference == -1:
            return
        if not self._board:
            return
        with self._lock:
            self.logger.debug('Configuring Analog reference to setting %d',
                              self.analog_reference)
            self._board.send_sysex(SYSEX_SET_ANALOG_REFERENCE,
                                   [self.analog_reference])

    def configure_lcd(self):
        if not self._board:
            return
        with self._lock:
            self.logger.debug('Configuring LCD in port %d cols %d rows %d',
                              self.lcd_port, self.lcd_columns, self.lcd_rows)
            self._board.send_sysex(SYSEX_SETUP_LCD, [
                self.lcd_port, self.lcd_columns, self.lcd_rows,
                self.lcd_reporting
            ])

    def lcd_set_cursor(self, col: int, row: int):
        if not self._board:
            return
        with self._lock:
            self.logger.debug('LCD Setting cursor to %d %d', col, row)
            self._board.send_sysex(SYSEX_LCD_COMMAND,
                                   bytearray([LCD_SET_CURSOR, col, row]))

    def lcd_clear(self):
        if not self._board:
            return
        with self._lock:
            self.logger.debug('Clearing LCD')
            self._board.send_sysex(SYSEX_LCD_COMMAND, bytearray([LCD_CLEAR]))

    def _lcd_print_raw(self, value: str):
        if not self._board:
            return
        with self._lock:
            self.logger.debug('Printing to LCD: %s', value)
            self._board.send_sysex(
                SYSEX_LCD_COMMAND,
                bytearray([LCD_PRINT]) + bytearray(value.encode('utf-8')) +
                b'\0')

    def lcd_print(self, value: str):
        lines = value.split('\n')
        self.lcd_clear()
        for line_number, line in enumerate(lines):
            self.lcd_set_cursor(0, line_number)
            self._lcd_print_raw(line)

    def lcd_set_reporting(self, value: bool):
        if not self._board:
            return
        with self._lock:
            self.logger.debug('Setting LCD backlight to %d', value)
            self._board.send_sysex(SYSEX_LCD_COMMAND,
                                   bytearray([LCD_SET_REPORTING, value]))

    def lcd_set_backlight(self, value: bool):
        if not self._board:
            return
        with self._lock:
            self.logger.debug('Setting LCD backlight to %d', value)
            self._board.send_sysex(SYSEX_LCD_COMMAND,
                                   bytearray([LCD_SET_BACKLIGHT, value]))

    def _keep_alive(self):
        if self.keep_alive:
            self.logger.debug('Sending keep-alive message to Arduino')
            self._board.send_sysex(SYSEX_KEEP_ALIVE, [0])
        interval = 60
        self._keepalive_thread = threading.Timer(
            interval, threaded(self.system, self._keep_alive))
        self._keepalive_thread.name = "Arduino keepalive (60s)"
        self._keepalive_thread.start()

    def _string_data_handler(self, *data):
        str_data = bytearray(data[::2]).decode('ascii')
        my_logger = self.logger.debug
        if str_data.startswith('E:'):
            my_logger = self.logger.error
        elif str_data.startswith('W:'):
            my_logger = self.logger.warning
        my_logger('String data: %s', str_data)

    def cleanup(self):
        self.logger.debug("Cleaning up Arduino subsystem. ")
        if self._board:
            self._board.exit()
        if self._iterator_thread and self._iterator_thread.is_alive():
            self._iterator_thread.join()
        if self._keepalive_thread:
            self._keepalive_thread.cancel()

    def reload(self):
        digital_sensors = list(self._sens_digital.items())
        analog_sensors = list(self._sens_analog.items())
        digital_actuators = list(self._act_digital.items())

        for pin_nr, (_type, pin) in digital_actuators:
            # actuator.cleanup() would be better approach ...
            self.cleanup_digital_actuator(pin_nr)

        for pin_nr, (sens, pin) in digital_sensors:
            sens.cleanup()
        for pin_nr, (sens, pin) in analog_sensors:
            sens.cleanup()
        super().reload()
        # Restore subscriptions
        for pin_nr, (sens, pin) in digital_sensors:
            sens.setup()
        for pin_nr, (sens, pin) in analog_sensors:
            sens.setup()

        for pin_nr, (_type, pin) in digital_actuators:
            # Perhaps it would be cleaner to do these too via act.setup()?
            # But we should store actuator instance here then.

            setup_func = {
                'p': self.setup_pwm,
                'o': self.setup_digital
            }.get(_type)

            # TODO: servo reload not implemented
            if setup_func:
                setup_func(pin_nr)
            else:
                self.logger.error(
                    'Reloading not implemented for type %s (pin %d)', _type,
                    pin_nr)
        self.logger.info('Arduino pins are now set up!')

    def send_virtualwire_command(self, recipient, command, *args):
        if not self._board:
            return
        with self._lock:
            board = self._board
            data = bytearray(
                [self.home_address, self.device_address, recipient, command])
            for a in args:
                if isinstance(a, bytearray):
                    data.extend(a)
                elif isinstance(a, str):
                    data.extend(bytearray(a.encode('utf-8')))
                else:
                    data.append(a)
            self.logger.debug('VW: Sending command %s', data)
            board.send_sysex(SYSEX_VIRTUALWIRE_MESSAGE, data)

    def setup_virtualwire_input(self):
        if not self._board:
            return
        self._board.add_cmd_handler(SYSEX_VIRTUALWIRE_MESSAGE,
                                    self._virtualwire_message_callback)

    def _virtualwire_message_callback(self, sender_address, command, *data):
        self.logger.debug('pulse %s %s %s', int(sender_address), hex(command),
                          bytearray(data))
        if command == VIRTUALWIRE_DIGITAL_BROADCAST:
            if len(data) != 2:
                self.logger.error('Broken digital data: %s %s %s',
                                  sender_address, command, data)
                return
            port_nr, value = data
            for s in self._sens_virtualwire_digital[sender_address]:
                port = s.pin // 8
                pin_in_port = s.pin % 8
                if port_nr == port:
                    s.set_status(bool(value & 1 << pin_in_port))
        elif command == VIRTUALWIRE_ANALOG_BROADCAST:
            if len(data) != 3:
                self.logger.error('Broken analog data: %s %s %s',
                                  sender_address, command, data)
                return
            pin, msb, lsb = data
            value = (msb << 8) + lsb
            value = value / 1023.  # Arduino gives 10 bit resolution
            self.logger.debug('Analog data: %s %s', pin, value)
            for s in self._sens_virtualwire_analog[sender_address]:
                if s.pin == pin:
                    s.set_status(value)

    def write(self, *data):
        data = bytearray(data)
        if not self._board:
            return
        with self._lock:
            self.logger.debug('Writing %s', data)
            self._board.sp.write(data)

    def subscribe_virtualwire_digital_broadcast(self, sensor, source_device):
        self._sens_virtualwire_digital[source_device].append(sensor)

    def unsubscribe_virtualwire_digital_broadcast(self, sensor, source_device):
        self._sens_virtualwire_digital[source_device].remove(sensor)

    def subscribe_virtualwire_analog_broadcast(self, sensor, source_device):
        self._sens_virtualwire_analog[source_device].append(sensor)

    def unsubscribe_virtualwire_analog_broadcast(self, sensor, source_device):
        self._sens_virtualwire_analog[source_device].remove(sensor)

    def setup_digital(self, pin_nr):
        if not self._board:
            self._act_digital[pin_nr] = PinTuple('o', None)
            return
        with self._lock:
            pin = self._board.get_pin("d:{pin}:o".format(pin=pin_nr))
            self._act_digital[pin_nr] = PinTuple('o', pin)

    def setup_pwm(self, pin_nr):
        if not self._board:
            self._act_digital[pin_nr] = PinTuple('p', None)
            return
        with self._lock:
            pin = self._board.get_pin("d:{pin}:p".format(pin=pin_nr))
            self._act_digital[pin_nr] = PinTuple('p', pin)

    def setup_servo(self, pin_nr, min_pulse, max_pulse, angle):
        if not self._board:
            self._act_digital[pin_nr] = PinTuple('s', None)
            return
        with self._lock:
            pin = self._board.get_pin("d:{pin}:s".format(pin=pin_nr))
            self._act_digital[pin_nr] = PinTuple('s', pin)
            self._board.servo_config(pin_nr, min_pulse, max_pulse, angle)

    def change_digital(self, pin_nr, value):
        """ Change digital Pin value (boolean). Also PWM supported(float)"""
        if not self._board:
            return
        with self._lock:
            self._act_digital[pin_nr].pin.write(value)

    # Functions for input signals
    def handle_analog(self, obj, name, old, new):
        pin = obj.pin_number
        if not self._board:
            return
        self._sens_analog[pin][0].set_status(new)

    def handle_digital(self, obj, name, old, new):
        pin = obj.pin_number
        if not self._board:
            return
        self._sens_digital[pin][0].set_status(new)

    def subscribe_analog(self, pin_nr, sens):
        if not self._board:
            return
        with self._lock:
            pin = self._board.get_pin("a:{pin}:i".format(pin=pin_nr))
            self._sens_analog[pin_nr] = (sens, pin)
            s = pin.read()
        if s is not None:
            sens.set_status(s)
        pin.on_trait_change(self.handle_analog, "value")

    def cleanup_digital_actuator(self, pin_nr):
        pin = self._act_digital.pop(pin_nr, None)

    def cleanup_virtualwire_actuator(self, pin_nr):
        pin = self._act_digital.pop(pin_nr, None)

    def unsubscribe_digital(self, pin_nr):
        pin = self._sens_digital.pop(pin_nr, None)
        if pin:
            pin[1].remove_trait('value')

    def unsubscribe_analog(self, pin_nr):
        pin = self._sens_analog.pop(pin_nr, None)
        if pin:
            pin[1].remove_trait('value')

    def subscribe_digital(self, pin_nr, sens):
        if not self._board:
            return
        with self._lock:
            pin = self._board.get_pin("d:{pin}:i".format(pin=pin_nr))
            self._sens_digital[pin_nr] = (sens, pin)
            s = pin.read()
        if s is not None:
            sens.set_status(s)
        pin.on_trait_change(self.handle_digital, "value")

    def set_pin_mode(self, pin_number, mode):
        if not self._board:
            return
        self.logger.debug('Setting pin mode for pin %s to %s', pin_number,
                          mode)
        self.write(pyfirmata.SET_PIN_MODE, pin_number, mode)
Beispiel #18
0
class UserBoolSensor(AbstractSensor):
    """Boolean-valued user-editable sensor"""
    user_editable = CBool(True)
    _status = CBool
Beispiel #19
0
class WebService(TornadoService):
    """
    Web User Interface Service for Automate
    """

    #: Restrict usage to only monitoring statuses (default: ``True``).
    #: If WebService is not in read_only mode, it is possible to run arbitrary Python commands
    #: through eval/exec via web browser. This is, of course, a severe security issue.
    #: Secure SSL configuration HIGHLY recommended, if not operating in ``read_only`` mode.
    read_only = CBool(True)

    #: Default view that is displayed when entering the server.
    #: Can be the name of any view in views.py
    default_view = Str('system')

    #: HTTP port to listen
    http_port = Int(8080)

    #: Authentication for logging into web server. (user,password) pairs in a tuple.
    http_auth = Tuple

    #: Let websocket connection die after ``websocket_timeout`` time of no ping reply from client.
    websocket_timeout = CInt(60 * 5)

    #: Tags that are shown in user defined view
    user_tags = CSet(trait=Str, value={'user'})

    #: Django debugging mode (slower, more info shown when error occurs)
    debug = CBool(False)

    #: User-defined custom pages as a dictionary of form ``{name: template_content}``
    custom_pages = Dict(key_trait=Unicode, value_trait=Unicode)

    #: set to True, if you want to launch multiple servers with same system. Authentication and
    #: other settings are then taken from master service. Only web server settings (http host/port)
    #: are used from slave.
    slave = CBool(False)

    #: In this dictionary you can define your custom Django settings which will override the default ones
    django_settings = Dict()

    #: From /set/object/value and /toggle/object, redirect to /set_ready/object/value after after executing action
    redirect_from_setters = CBool(True)

    #: Show hidden objects in web UI (those prefixed with _)
    show_hidden = CBool(False)

    #: Full exposed url root
    server_url = Unicode()

    _sockets = List(transient=True)

    def get_filehandler_class(service):
        class MyFileHandler(tornado.web.StaticFileHandler):
            def validate_absolute_path(self, *args, **kwargs):
                session_id = getattr(
                    self.request.cookies.get('sessionid', None), 'value', None)
                from django.contrib.sessions.middleware import SessionMiddleware
                mw = SessionMiddleware()
                session_data = mw.SessionStore(session_id)
                if not session_data.get('logged_in', False):
                    raise tornado.web.HTTPError(403, 'not logged in')

                return super().validate_absolute_path(*args, **kwargs)

            def check_etag_header(self):
                """ Disable etag_header checking (checks only modified time). Due to etag caching
                    file changes were not detected at all. """
                return False

        return MyFileHandler

    def get_tornado_handlers(self):
        if self.slave:
            return self.system.request_service(
                'WebService').get_tornado_handlers()
        super_handlers = super().get_tornado_handlers()
        path = os.path.join(os.path.dirname(__file__), 'static')
        static = [('/static/(.*)', tornado.web.StaticFileHandler, {
            'path': path
        })]
        return static + super_handlers

    def setup(self):
        if not self.slave:
            os.environ[
                'DJANGO_SETTINGS_MODULE'] = 'automate.extensions.webui.settings'
            from django.conf import settings
            settings.DEBUG = self.debug
            if not 'SECRET_KEY' in self.django_settings:
                self.logger.warning(
                    'Insecure settings! Please set proper SECRET_KEY in '
                    'WebService.django_settings!')
            if not 'TIME_ZONE' in self.django_settings:
                os.environ.pop(
                    'TZ'
                )  # Django uses America/Chicago as default timezone. Let's clean this up.
                time.tzset()
            if self.server_url:
                self.django_settings['SERVER_URL'] = self.server_url
            if self.system.raven_dsn:
                settings.INSTALLED_APPS.append(
                    'raven.contrib.django.raven_compat')
                settings.RAVEN_CONFIG = {
                    'dsn': self.system.raven_dsn,
                    'release': __version__
                }
            installed_apps = self.django_settings.pop('INSTALLED_APPS', None)
            if installed_apps:
                settings.INSTALLED_APPS.extend(list(installed_apps))

            for key, value in self.django_settings.items():
                setattr(settings, key, value)

            from automate.extensions.webui.djangoapp.views import set_globals
            set_globals(self, self.system)

        super().setup()
        if not self.slave:
            self.system.request_service('LogStoreService').on_trait_change(
                self.push_log, 'most_recent_line')

            self.system.on_trait_change(
                self.update_sockets,
                'objects.status, objects.changing, objects.active, '
                'objects.program_status_items')

    def get_websocket(service):
        if service.slave:
            return service.system.request_service('WebService').get_websocket()

        class WebSocket(WebSocketHandler):
            def data_received(self, chunk):
                pass

            def __init__(self, application, request, **kwargs):
                self.log_requested = False
                self.subscribed_objects = set()
                self.last_message = None
                self.logged_in = False

                super().__init__(application, request, **kwargs)

            def check_origin(self, origin):
                return True

            def write_json(self, **kwargs):
                msg = json.dumps(kwargs)
                service.logger.debug('Sending to client %s', msg)
                self.write_message(msg)

            def open(self):
                self.session_id = session_id = getattr(
                    self.request.cookies.get('sessionid', None), 'value', None)
                from django.contrib.sessions.middleware import SessionMiddleware
                mw = SessionMiddleware()
                session_data = mw.SessionStore(session_id)

                if session_data.get('logged_in',
                                    False) or not service.http_auth:
                    self.logged_in = True
                else:
                    service.logger.warning("Not (yet) logged in %s",
                                           session_id)

                service.logger.debug("WebSocket opened for session %s",
                                     session_id)
                service._sockets.append(self)

            def _authenticate(self, username, password):
                if (username, password) == service.http_auth:
                    self.logged_in = True
                    service.logger.debug('Websocket user %s logged in.',
                                         username)
                else:
                    service.logger.warning(
                        'Authentication failure: user %s with passwd %s != %s ',
                        username, password, service.http_auth)
                    self.close()

            def _ping(self):
                pass

            def _set_status(self, name, status):
                if service.read_only:
                    service.logger.warning(
                        "Could not perform operation: read only mode enabled")
                    return
                obj = service.system.namespace.get(name, None)
                if obj:
                    obj.status = status

            def _subscribe(self, objects):
                self.subscribed_objects.update(objects)

            def _unsubscribe(self, objects):
                self.subscribed_objects -= set(objects)

            def _clear_subscriptions(self):
                self.subscribed_objects.clear()

            def _send_command(self, command):
                if not service.read_only:
                    service.system.cmd_exec(command)
                else:
                    service.logger.warning(
                        "Could not perform operation: read only mode enabled")

            def _fetch_objects(self):
                data = [(i.name, i.get_as_datadict())
                        for i in service.system.objects_sorted]
                self.write_json(action='return', rv=data)

            def _request_log(self):
                self.log_requested = True

            def on_message(self, json_message):
                """
                    Message format: {'action': 'action_name', other_kwargs...)
                """

                message = json.loads(json_message)
                service.logger.debug('Message received from client: %s',
                                     message)

                action = message.pop('action', '')

                if self.logged_in:
                    action_func = getattr(self, '_' + action, None)
                    if action_func:
                        service.logger.debug('Running websocket action %s',
                                             action)
                        action_func(**message)
                    else:
                        service.logger.error(
                            'Not logged in or unknown message %s', message)
                elif action == 'authenticate':
                    return self._authenticate(**message)

                self.last_message = datetime.datetime.now()

            def on_close(self):
                service.logger.debug("WebSocket closed for session %s",
                                     self.session_id)
                service._sockets.remove(self)

        return WebSocket

    def push_log(self, new):
        for s in self._sockets:
            if s.log_requested:
                try:
                    s.write_json(action='log', data=new)
                except WebSocketClosedError:
                    pass

    def update_sockets(self, obj, attribute, old, new):
        from django.template.loader import render_to_string
        if isinstance(obj, StatusObject):
            self.logger.debug('Update_sockets %s %s %s %s', obj, attribute,
                              old, new)
            for s in self._sockets:
                if s.last_message and (
                        s.last_message < datetime.datetime.now() -
                        datetime.timedelta(seconds=self.websocket_timeout)):
                    self.logger.info('Closing connection %s due to timeout',
                                     s.session_id)
                    s.on_close()
                    s.close(code=1000, reason='Timeout')
                    continue
                if obj.name in s.subscribed_objects:
                    if attribute == 'active':
                        s.write_json(action='program_active',
                                     name=obj.name,
                                     active=obj.active)
                    elif attribute in ['status', 'changing']:
                        s.write_json(action='object_status',
                                     name=obj.name,
                                     status=obj.status,
                                     time=int(1000 * time.time()),
                                     display=obj.get_status_display(),
                                     changing=obj.changing)

    def get_wsgi_application(self):
        from django.core.wsgi import get_wsgi_application
        return get_wsgi_application()
Beispiel #20
0
class SocketSensor(AbstractSensor):
    """
        Sensor that reads a TCP socket.

        Over TCP port, it reads data per lines and tries to set the status of the sensor
        to the value specified by the line. If content of the line is 'close', then connection
        is dropped.
    """

    #: Hostname/IP to listen. Use ``'0.0.0.0'``  to listen all interfaces.
    host = CStr('0.0.0.0')

    #: Port to listen
    port = CInt

    #: set to ``True`` to tell SocketSensor to stop listening to port
    stop = CBool(transient=True)

    _socket = Instance(socket.socket, transient=True)
    _status = CInt

    def listen_loop(self):
        while not self.stop:
            try:
                self.logger.info('%s listening to connections in port %s',
                                 self.name, self.port)
                self._socket.listen(1)
                self._socket.settimeout(1)
                while not self.stop:
                    try:
                        conn, addr = self._socket.accept()
                    except socket.timeout:
                        continue
                    break
                self.logger.info('%s connected from %s', self.name, addr)
                conn.settimeout(1)
                while not self.stop:
                    try:
                        data = conn.recv(1024)
                        if not data:
                            break
                        self.status = int(data.strip())
                        conn.sendall('OK\n')
                    except socket.timeout:
                        data = ''
                    except ValueError:
                        if data.strip() == 'close':
                            break
                        conn.sendall('NOK\n')
            except socket.error as e:
                self.logger.info("%s: Error %s caught.", self, e)
            except:
                if self.stop:
                    return
                else:
                    raise
            conn.close()
            self.logger.info('%s: connection %s closed', self.name, addr)
        self._socket.close()

    def setup(self):
        self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
        self._socket.bind((self.host, self.port))
        t = threading.Thread(target=self.listen_loop,
                             name='SocketSensor %s' % self.name)
        t.start()

    def cleanup(self):
        self.stop = True
Beispiel #21
0
class UserAnySensor(AbstractSensor):
    """User editable sensor type that accepts values of any types"""
    user_editable = CBool(True)
    _status = Any
Beispiel #22
0
class CronTimerSensor(AbstractSensor):
    """
        Scheduled start/stop timer. Both start and stop times
        are configured by cron-type string (see man 5 crontab for description of the
        definition format).
    """
    class CronListStr(Unicode):
        """Validation class for cron-compatible strings (for timers)"""
        def validate(self, object, name, value):
            vals = value.split(";")
            for v in vals:
                try:
                    c = croniter(v)
                except:
                    self.error(object, name, value)
                    return
            return value

    _status = CBool(False)

    #: Semicolon separated lists of cron-compatible strings that indicate
    #: when to switch status to ``True``
    timer_on = CronListStr("0 0 0 0 0")

    #: Semicolon separated lists of cron-compatible strings that indicate
    #: when to switch status to ``False``
    timer_off = CronListStr("0 0 0 0 0")

    _update_timer = Any(transient=True)  # Timer object
    _timerlock = Any(transient=True)  # Lock object

    view = UserBoolSensor.view + ["timer_on", "timer_off"]

    def setup_system(self, *args, **traits):
        self._timerlock = Lock()
        super().setup_system(*args, **traits)
        self.update_status()

    def _now(self):
        return datetime.now()

    def update_status(self):
        with self._timerlock:
            now = self._now()
            next_iters = [CroniterOn(i, now) for i in self.timer_on.split(";")] + \
                         [CroniterOff(i, now) for i in self.timer_off.split(";")]

            for i in next_iters:
                i.get_next(datetime)

            next_iters.sort(key=lambda x: x.get_current(datetime))

            prev_iters = deepcopy(next_iters)

            for i in prev_iters:
                i.get_prev(datetime)
            prev_iters.sort(key=lambda x: x.get_current(datetime))

            self.status = isinstance(prev_iters[-1], CroniterOn)

            self._setup_next_update(next_iters[0].get_current(datetime))

    def _timer_on_changed(self, name, new):
        self.update_status()

    def _timer_off_changed(self, name, new):
        self.update_status()

    def _setup_next_update(self, next_update_time):
        now = self._now()
        if self._update_timer and self._update_timer.is_alive():
            self._update_timer.cancel()

        delay = next_update_time - now + timedelta(seconds=5)
        self.logger.info('Setting timer to %s, %s seconds, at %s', delay,
                         delay.seconds, now + delay)
        self._update_timer = threading.Timer(
            delay.seconds, threaded(
                self.system,
                self.update_status,
            ))
        self._update_timer.name = (
            "Timer for TimerSensor %s at %s (%s seconds)" %
            (self.name, now + delay, delay.seconds))
        self._update_timer.start()

    def cleanup(self):
        with self._timerlock:
            if self._update_timer:
                self._update_timer.cancel()
Beispiel #23
0
class FloatActuator(AbstractActuator):

    """Floating point valued actuator"""
    _status = CFloat(transient=True)
    default = CFloat
    silent = CBool(True)
Beispiel #24
0
class UserFloatSensor(AbstractNumericSensor):
    """Float-valued user-editable sensor"""
    user_editable = CBool(True)
    _status = CFloat
    silent = CBool(True)
Beispiel #25
0
class UserStrSensor(AbstractSensor):
    """String-valued user-editable sensor"""
    user_editable = CBool(True)
    _status = CUnicode
Beispiel #26
0
class UserIntSensor(AbstractNumericSensor):
    """Integer-valued user-editable sensor"""
    user_editable = CBool(True)
    _status = CInt(0)
Beispiel #27
0
class RpioService(AbstractSystemService):
    """
        Service that provides interface to Raspberry Pi GPIO via
        `RPIO library <http://pythonhosted.org/RPIO/>`_.
    """

    #: Perform GPIO cleanup when exiting (default: False).
    gpio_cleanup = CBool(False)

    #: Use RPIO instead of RPI.GPIO
    rpio = CBool(False)

    _gpio_thread = Any

    _hw = Any

    def setup(self):
        self.logger.info(
            "Initializing RpioService (Raspberry Pi GPIO support)")
        try:
            if self.rpio:
                import RPIO
                import RPIO.PWM
            else:
                import RPi.GPIO as RPIO
            self.is_mocked = False
        except (ImportError, SystemError, RuntimeError):
            self.logger.warning(
                'RPIO module could not be imported. Enabling mocked RPIO')
            self.logger.warning(
                "To use Raspberry Pi GPIO ports (sensors / actuators) please install module RPIO"
            )
            import mock
            RPIO = mock.MagicMock()
            self.is_mocked = True

        self._hw = RPIO

        self._hw.setmode(RPIO.BCM)
        if self.rpio:
            self._gpio_thread = t = Thread(target=RPIO.wait_for_interrupts,
                                           name='RpioService thread')
            t.daemon = True
            t.start()

        self.logger.info("RPIO initialized")

    def cleanup(self):
        if self.rpio:
            self._hw.stop_waiting_for_interrupts()
            self._hw.cleanup_interrupts()
            self._gpio_thread.join()

        if self.gpio_cleanup:
            self._hw.cleanup()

    def enable_input_port(self, port, callback, pull_up_down):
        pud = {
            "down": self._hw.PUD_DOWN,
            "up": self._hw.PUD_UP,
            "none": self._hw.PUD_OFF
        }
        if self.rpio:
            self._hw.setup(port, self._hw.IN)
            self._hw.add_interrupt_callback(port,
                                            callback,
                                            edge="both",
                                            pull_up_down=pud[pull_up_down])
        else:
            self._hw.setup(port, self._hw.IN, pull_up_down=pud[pull_up_down])
            self._hw.add_event_detect(
                port, self._hw.BOTH, lambda _port: threaded(
                    self.system, callback, _port, self._hw.input(_port))())

    def get_input_status(self, port):
        return self._hw.input(port)

    def disable_input_port(self, port):
        if self.rpio:
            self._hw.del_interrupt_callback(port)
        else:
            self._hw.remove_event_detect(port)
            self._hw.cleanup(port)

    def enable_output_port(self, port):
        self._hw.setup(port, self._hw.OUT)

    def disable_output_port(self, port):
        self._hw.setup(port, self._hw.IN)

    def get_pwm_module(self):
        if not self.rpio:
            self.logger.error(
                'PWM supported only when rpio attribute in RpioService is enabled'
            )
        try:
            self._hw.PWM.setup()
            signal.signal(signal.SIGCHLD, signal.SIG_IGN)
            self.logger.warning(
                'SIGCHLD is now ignored totally due to RPIO.PWM bug. Might cause side effects!'
            )
        except RuntimeError as e:
            if 'has already been called before' in e.message:
                pass
            else:
                raise

        return self._hw.PWM

    def set_output_port_status(self, port, status):
        self._hw.output(port, status)
Beispiel #28
0
class System(SystemBase):
    #: Name of the system (shown in WEB UI for example)
    name = CStr

    #: Allow referencing objects by their names in Callables. If disabled, you can still refer to objects by names
    #: by Object('name')
    allow_name_referencing = CBool(True)

    #: Filename to where to dump the system state
    filename = Str

    # LOGGING
    ###########

    #: Name of the file where logs are stored
    logfile = CUnicode

    #: Reference to logger instance (read-only)
    logger = Instance(logging.Logger)

    #: Sentry: Raven DSN configuration (see http://sentry.io)
    raven_dsn = Str

    #: Raven client (is created automatically if raven_dsn is set and this is left empty)
    raven_client = Instance(raven.Client, transient=True)

    #: Format string of the log handler that writes to stdout
    log_format = Str('%(asctime)s %(log_color)s%(name)s%(reset)s %(message)s')

    #: Format string of the log handler that writes to logfile
    logfile_format = Str(
        '%(process)d:%(threadName)s:%(name)s:%(asctime)s:%(levelname)s:%(message)s'
    )

    #: Log level of System logger
    log_level = CInt(logging.INFO, transient=True)

    @on_trait_change('log_level', post_init=True)
    def log_level_changed(self, new):
        self.logger.setLevel(new)

    # SERVICES
    ###########

    #: Add here services that you want to be added automatically. This is meant to be re-defined in subclass.
    default_services = CList(trait=Str)

    #: List of services that are loaded in the initialization of the System.
    services = CList(trait=Instance(AbstractService))

    #: List of servicenames that are desired to be avoided (even if normally autoloaded).
    exclude_services = CSet(trait=Str)

    #: Reference to the worker thread (read-only)
    worker_thread = Instance(StatusWorkerThread, transient=True)

    #: System namespace (read-only)
    namespace = Instance(Namespace)

    # Set of all SystemObjects within the system. This is where SystemObjects are ultimately stored
    # in the System initialization. (read-only)
    objects = CSet(trait=SystemObject)

    #: Property giving objects sorted alphabetically (read-only)
    objects_sorted = Property(depends_on='objects')

    @cached_property
    def _get_objects_sorted(self):
        return sorted(list(self.objects), key=operator.attrgetter('_order'))

    #: Read-only property giving all sensors of the system
    sensors = Property(depends_on='objects[]')

    @cached_property
    def _get_sensors(self):
        return {
            i
            for i in self.objects_sorted if isinstance(i, AbstractSensor)
        }

    #: Read-only property giving all actuator of the system
    actuators = Property(depends_on='objects[]')

    @cached_property
    def _get_actuators(self):
        return {
            i
            for i in self.objects_sorted if isinstance(i, AbstractActuator)
        }

    #: Read-only property giving all objects that have program features in use
    programs = Property(depends_on='objects[]')

    @cached_property
    def _get_programs(self):
        from .program import Program, DefaultProgram
        return {
            i
            for i in self.objects_sorted
            if isinstance(i, (Program, DefaultProgram))
        }

    #: Read-only property giving all :class:`~program.Program` objects
    ordinary_programs = Property(depends_on='programs[]')

    @cached_property
    def _get_ordinary_programs(self):
        from . import program
        return {i for i in self.programs if isinstance(i, program.Program)}

    #: Start worker thread automatically after system is initialized
    worker_autostart = CBool(True)

    #: Trigger which is triggered after initialization is ready (used by Services)
    post_init_trigger = Event

    #: Trigger which is triggered before quiting (used by Services)
    pre_exit_trigger = Event

    #: Read-only property that gives list of all object tags
    all_tags = Property(depends_on='objects.tags[]')

    #: Number of state backup files
    num_state_backups = CInt(5)

    @cached_property
    def _get_all_tags(self):
        newset = set([])
        for i in self.system.objects:
            for j in i.tags:
                if j:
                    newset.add(j)
        return newset

    #: Enable experimental two-phase queue handling technique (not recommended)
    two_phase_queue = CBool(False)

    @classmethod
    def load_or_create(cls,
                       filename=None,
                       no_input=False,
                       create_new=False,
                       **kwargs):
        """
            Load system from a dump, if dump file exists, or create a new system if it does not exist.
        """
        parser = argparse.ArgumentParser()
        parser.add_argument('--no_input', action='store_true')
        parser.add_argument('--create_new', action='store_true')
        args = parser.parse_args()

        if args.no_input:
            print('Parameter --no_input was given')
            no_input = True
        if args.create_new:
            print('Parameter --create_new was given')
            create_new = True
            no_input = True

        def savefile_more_recent():
            time_savefile = os.path.getmtime(filename)
            time_program = os.path.getmtime(sys.argv[0])
            return time_savefile > time_program

        def load_pickle():
            with open(filename, 'rb') as of:
                statefile_version, data = pickle.load(of)

            if statefile_version != STATEFILE_VERSION:
                raise RuntimeError(
                    f'Wrong statefile version, please remove state file {filename}'
                )
            return data

        def load():
            print('Loading %s' % filename)
            obj_list, config = load_pickle()
            system = System(load_state=obj_list, filename=filename, **kwargs)

            return system

        def create():
            print('Creating new system')
            config = None
            if filename:
                try:
                    obj_list, config = load_pickle()
                except FileNotFoundError:
                    config = None
            return cls(filename=filename, load_config=config, **kwargs)

        if filename and os.path.isfile(filename):
            if savefile_more_recent() and not create_new:
                return load()
            else:
                if no_input:
                    print('Program file more recent. Loading that instead.')
                    return create()
                while True:
                    answer = input(
                        'Program file more recent. Do you want to load it? (y/n) '
                    )
                    if answer == 'y':
                        return create()
                    elif answer == 'n':
                        return load()
        else:
            return create()

    def save_state(self):
        """
            Save state of the system to a dump file :attr:`System.filename`
        """
        if not self.filename:
            self.logger.error('Filename not specified. Could not save state')
            return
        self.logger.debug('Saving system state to %s', self.filename)
        for i in reversed(range(self.num_state_backups)):
            fname = self.filename if i == 0 else '%s.%d' % (self.filename, i)
            new_fname = '%s.%d' % (self.filename, i + 1)
            try:
                os.rename(fname, new_fname)
            except FileNotFoundError:
                pass

        with open(self.filename, 'wb') as file, self.worker_thread.queue.mutex:
            obj_list = list(self.objects)
            config = {
                obj.name: obj.status
                for obj in obj_list if getattr(obj, 'user_editable', False)
            }
            data = obj_list, config
            pickle.dump((STATEFILE_VERSION, data), file,
                        pickle.HIGHEST_PROTOCOL)

    @property
    def cmd_namespace(self):
        """
            A read-only property that gives the namespace of the system for evaluating commands.
        """
        import automate
        ns = dict(
            list(automate.__dict__.items()) + list(self.namespace.items()))
        return ns

    def __getattr__(self, item):
        if self.namespace and item in self.namespace:
            return self.namespace[item]
        raise AttributeError

    def get_unique_name(self, obj, name='', name_from_system=''):
        """
            Give unique name for an Sensor/Program/Actuator object
        """
        ns = self.namespace
        newname = name
        if not newname:
            newname = name_from_system

        if not newname:
            newname = u"Nameless_" + obj.__class__.__name__

        if not newname in ns:
            return newname

        counter = 0
        while True:
            newname1 = u"%s_%.2d" % (newname, counter)
            if not newname1 in ns:
                return newname1
            counter += 1

    @property
    def services_by_name(self):
        """
            A property that gives a dictionary that contains services as values and their names as keys.
        """
        srvs = defaultdict(list)
        for i in self.services:
            srvs[i.__class__.__name__].append(i)
        return srvs

    @property
    def service_names(self):
        """
            A property that gives the names of services as a list
        """
        return set(self.services_by_name.keys())

    def flush(self):
        """
            Flush the worker queue. Usefull in unit tests.
        """
        self.worker_thread.flush()

    def name_to_system_object(self, name):
        """
            Give SystemObject instance corresponding to the name
        """
        if isinstance(name, str):
            if self.allow_name_referencing:
                name = name
            else:
                raise NameError(
                    'System.allow_name_referencing is set to False, cannot convert string to name'
                )
        elif isinstance(name, Object):
            name = str(name)
        return self.namespace.get(name, None)

    def eval_in_system_namespace(self, exec_str):
        """
            Get Callable for specified string (for GUI-based editing)
        """
        ns = self.cmd_namespace
        try:
            return eval(exec_str, ns)
        except Exception as e:
            self.logger.warning('Could not execute %s, gave error %s',
                                exec_str, e)
            return None

    def register_service_functions(self, *funcs):
        """
            Register function in the system namespace. Called by Services.
        """
        for func in funcs:
            self.namespace[func.__name__] = func

    def register_service(self, service):
        """
            Register service into the system. Called by Services.
        """
        if service not in self.services:
            self.services.append(service)

    def request_service(self, type, id=0):
        """
            Used by Sensors/Actuators/other services that need to use other services for their
            operations.
        """
        srvs = self.services_by_name.get(type)
        if not srvs:
            return

        ser = srvs[id]

        if not ser.system:
            ser.setup_system(self, id=id)
        return ser

    def cleanup(self):
        """
            Clean up before quitting
        """

        self.pre_exit_trigger = True

        self.logger.info("Shutting down %s, please wait a moment.", self.name)
        for t in threading.enumerate():
            if isinstance(t, TimerClass):
                t.cancel()
        self.logger.debug('Timers cancelled')

        for i in self.objects:
            i.cleanup()

        self.logger.debug('Sensors etc cleanups done')

        for ser in (i for i in self.services
                    if isinstance(i, AbstractUserService)):
            ser.cleanup_system()
        self.logger.debug('User services cleaned up')
        if self.worker_thread.is_alive():
            self.worker_thread.stop()
        self.logger.debug('Worker thread really stopped')

        for ser in (i for i in self.services
                    if isinstance(i, AbstractSystemService)):
            ser.cleanup_system()
        self.logger.debug('System services cleaned up')
        threads = list(t.name for t in threading.enumerate()
                       if t.is_alive() and not t.daemon)
        if threads:
            self.logger.info(
                'After cleanup, we have still the following threads '
                'running: %s', ', '.join(threads))

    def cmd_exec(self, cmd):
        """
            Execute commands in automate namespace
        """

        if not cmd:
            return
        ns = self.cmd_namespace
        import copy
        rval = True
        nscopy = copy.copy(ns)
        try:
            r = eval(cmd, ns)
            if isinstance(r, SystemObject) and not r.system:
                r.setup_system(self)
            if callable(r):
                r = r()
                cmd += "()"
            self.logger.info("Eval: %s", cmd)
            self.logger.info("Result: %s", r)
        except SyntaxError:
            r = {}
            try:
                exec(cmd, ns)
                self.logger.info("Exec: %s", cmd)
            except ExitException:
                raise
            except Exception as e:
                self.logger.info("Failed to exec cmd %s: %s.", cmd, e)
                rval = False
            for key, value in list(ns.items()):
                if key not in nscopy or not value is nscopy[key]:
                    if key in self.namespace:
                        del self.namespace[key]
                    self.namespace[key] = value
                    r[key] = value
            self.logger.info("Set items in namespace: %s", r)
        except ExitException:
            raise
        except Exception as e:
            self.logger.info("Failed to eval cmd %s: %s", cmd, e)
            return False

        return rval

    def __init__(self,
                 load_state: 'List[SystemObject]' = None,
                 load_config: 'Dict[str, Any]' = None,
                 **traits):
        super().__init__(**traits)
        if not self.name:
            self.name = self.__class__.__name__
            if self.name == 'System':
                self.name = os.path.split(sys.argv[0])[-1].replace('.py', '')

        # Initialize Sentry / raven client, if is configured
        if not self.raven_client and self.raven_dsn:
            self.raven_client = raven.Client(
                self.raven_dsn,
                release=__version__,
                tags={'automate-system': self.name})

        self._initialize_logging()
        self.worker_thread = StatusWorkerThread(name="Status worker thread",
                                                system=self)
        self.logger.info('Initializing services')
        self._initialize_services()
        self.logger.info('Initializing namespace')
        self._initialize_namespace(load_state)

        if load_config:
            self.logger.info('Loading config')
            for obj_name, status in load_config.items():
                if hasattr(self, obj_name):
                    getattr(self, obj_name).status = status

        self.logger.info('Initialize user services')
        self._setup_user_services()

        if self.worker_autostart:
            self.logger.info('Starting worker thread')
            self.worker_thread.start()

        self.post_init_trigger = True

    def _initialize_logging(self):
        root_logger = logging.getLogger('automate')
        self.logger = root_logger.getChild(self.name)

        # Check if root level logging has been set up externally.

        if len(root_logger.handlers) > 0:
            root_logger.info('Logging has been configured already, '
                             'skipping logging configuration')
            return

        root_logger.propagate = False
        root_logger.setLevel(self.log_level)
        self.logger.setLevel(self.log_level)

        if self.raven_client:
            sentry_handler = SentryHandler(client=self.raven_client,
                                           level=logging.ERROR)
            root_logger.addHandler(sentry_handler)

        if self.logfile:
            formatter = logging.Formatter(fmt=self.logfile_format)
            log_handler = logging.FileHandler(self.logfile)
            log_handler.setFormatter(formatter)
            root_logger.addHandler(log_handler)

        stream_handler = logging.StreamHandler()

        from colorlog import ColoredFormatter, default_log_colors
        colors = default_log_colors.copy()
        colors['DEBUG'] = 'purple'

        stream_handler.setFormatter(
            ColoredFormatter(self.log_format,
                             datefmt='%H:%M:%S',
                             log_colors=colors))
        root_logger.addHandler(stream_handler)

        self.logger.info('Logging setup ready')

    def _initialize_namespace(self, load_state=None):
        self.namespace = Namespace(system=self)
        self.namespace.set_system(load_state)

        self.logger.info('Setup loggers per object')
        for name, obj in self.namespace.items():
            if isinstance(obj, SystemObject):
                ctype = obj.__class__.__name__
                obj.logger = self.logger.getChild('%s.%s' % (ctype, name))

    def _initialize_services(self):
        # Add default_services, if not already
        for servname in self.default_services:
            if servname not in self.service_names | self.exclude_services:
                self.services.append(get_service_by_name(servname)())

        # Add autorun services if not already
        for servclass in get_autoload_services():
            if servclass.__name__ not in self.service_names | self.exclude_services:
                self.services.append(servclass())

    def _setup_user_services(self):
        for ser in (i for i in self.services
                    if isinstance(i, AbstractUserService)):
            self.logger.info('...%s', ser.__class__.__name__)
            ser.setup_system(self)
Beispiel #29
0
class StatusObject(AbstractStatusObject, ProgrammableSystemObject,
                   CompareMixin):
    """
        Baseclass for Sensors and Actuators
    """

    editable = True

    #: Determines minimum time required for switching. State change is then delayed if necessary.
    safety_delay = CFloat(0.)

    #: Determines when :attr:`.safety_delay` needs to be taken into account: when status is
    #: rising, falling or both.
    safety_mode = Enum("rising", "falling", "both")

    #: Similar to :attr:`.safety_delay`, but just delays change to make sure that events shorter
    #: than change_delay are not taken into account
    change_delay = CFloat(0.)

    #: As :attr:`.safety_mode`, but for :attr:`.change_delay`
    change_mode = Enum("rising", "falling", "both")

    #: Do not emit actuator status changes into logs
    silent = CBool(False)

    #: (property) Is delayed change taking place at the moment?
    changing = Property(trait=Bool,
                        transient=True,
                        depends_on='_timed_action, _queued_job')

    # Deque of history, which consists of tuples (timestamp, status), read only
    history = Any()  # transient=True)

    #: Transpose of history (timesstamps, statuses)
    history_transpose = Property(transient=True)

    #: Amount of status change events to be stored in history
    history_length = CInt(1000)

    #: How often new values are saved to history, in seconds
    history_frequency = CFloat(0)

    #: Show stdev seconds (0 to disable)
    show_stdev_seconds = CInt(0)

    @cached_property
    def _get_changing(self):
        if self._queued_job or self._timed_action:
            return True
        return False

    # Thread of currently running action
    _timed_action = Instance(threading.Timer, transient=True)

    # Reference of status change job that is in the worker queue is saved here
    _queued_job = Instance(StatusWorkerTask, transient=True)

    # Time when status was last changed
    _last_changed = CFloat

    # The time when last change started
    _change_start = CFloat(transient=True)

    # Lock that is acquired when changing the status
    _status_lock = Instance(Lock, transient=True)

    logger = Instance(logging.Logger, transient=True)

    view = [
        "name", "status", "description", "safety_delay", "safety_mode",
        "change_delay", "change_mode", "history_length", 'history_frequency'
    ] + SystemObject.view

    simple_view = []

    # used by Web UI, for templates
    data_type = Str(transient=True)

    def _get_history_transpose(self):
        return list(zip(*self.history)) if self.history else [[0], [0]]

    @property
    def times(self):
        return self.history_transpose[0]

    @property
    def datetimes(self):
        return [datetime.datetime.fromtimestamp(i) for i in self.times]

    @property
    def statuses(self):
        return self.history_transpose[1]

    def status_at_time(self, T):
        if isinstance(T, datetime.datetime):
            T = T.timestamp()
        t_max = 0
        times, statuses = self.history_transpose
        if T < times[0]:
            return 0.
        for t in times:
            if t <= T:
                t_max = t
            else:
                break
        t_index = times.index(t_max)
        return statuses[t_index]

    @staticmethod
    def _convert_times(t_a, t_b):
        if t_a is None:
            t_a = 0.
        elif isinstance(t_a, datetime.datetime):
            t_a = t_a.timestamp()

        if t_b is None:
            t_b = time.time()
        elif isinstance(t_b, datetime.datetime):
            t_b = t_b.timestamp()

        return t_a, t_b

    @lru_cache()
    def integral(self, t_a=None, t_b=None):
        self.logger.debug('Calculating integral for %s', self)
        t_a, t_b = self._convert_times(t_a, t_b)
        history = ((t, s) for t, s in self.history
                   if t_a <= t <= t_b and isinstance(s, Number))
        t_prev = t_a
        s_prev = self.status_at_time(t_a)
        if not isinstance(s_prev, Number):
            s_prev = 0.
        s_sum = 0.
        for t, s in history:
            s_sum += s_prev * (t - t_prev)
            s_prev, t_prev = s, t
        s_sum += s_prev * (t_b - t_prev)
        return s_sum

    def average(self, t_a=None, t_b=None):
        t_a, t_b = self._convert_times(t_a, t_b)
        if t_a == t_b:
            return 0.
        return self.integral(t_a, t_b) / (t_b - t_a)

    def stdev(self, t: int = 10) -> float:
        values = []
        now = time.time()
        for t_, value in reversed(self.history):
            if t_ < now - t:
                break
            values.append(value)
        return statistics.stdev(values) if len(values) > 1 else 0.0

    def __init__(self, *args, **kwargs):
        self._status_lock = Lock('statuslock')
        super().__init__(*args, **kwargs)

    def __setstate__(self, *args, **kwargs):
        self._status_lock = Lock('statuslock')
        super().__setstate__(*args, **kwargs)

    def _history_length_changed(self, new_value):
        self.history = collections.deque(list(self.history or [])[-new_value:],
                                         maxlen=new_value)

    @property
    def is_program(self):
        """
            A property which can be used to check if StatusObject uses program features or not.
        """
        from automate.callables import Empty
        return not (isinstance(self.on_activate, Empty) and isinstance(
            self.on_deactivate, Empty) and isinstance(self.on_update, Empty))

    #: Status of the object.
    status = Property(depends_on="_status, _status_trigger", transient=True)

    #: To force trigger status change events even if status itself does not change
    _status_trigger = Event

    def get_status_display(self, **kwargs):
        """
            Define how status is displayed in UIs (add units etc.).
        """
        if 'value' in kwargs:
            value = kwargs['value']
        else:
            value = self.status

        if self.show_stdev_seconds:
            stdev = self.stdev(self.show_stdev_seconds)
            return f'{value}±{stdev:2.2}'
        else:
            return str(value)

    def get_as_datadict(self):
        """
            Get data of this object as a data dictionary. Used by websocket service.
        """
        d = super().get_as_datadict()
        d.update(
            dict(status=self.status,
                 data_type=self.data_type,
                 editable=self.editable))
        return d

    @cached_property
    def _get_status(self):
        return self._status

    def _set_status(self, value):
        self.set_status(value)

    def setup_system(self, *args, **kwargs):
        super().setup_system(*args, **kwargs)
        if not self.history:
            self.history = collections.deque(maxlen=self.history_length)
        self.data_type = self._status.__class__.__name__

    def set_status(self, new_status, origin=None, force=False):
        """
            For sensors, this is synonymous to::

                sensor.status = new_status

            For (non-slave) actuators, origin argument (i.e. is the program that is
            changing the status) need to be given,
        """
        raise NotImplementedError

    def update_status(self):
        """
           In sensors: implement particular value reading from device etc. here (this calls set_status(value)).
           In actuators: set value in particular device.
           Implement in subclasses.
        """

    def activate_program(self, program):
        """
            When program controlling this object activates, it calls this function.
        """

    def deactivate_program(self, program):
        """
            When program controlling this object deactivates, it calls this function.
        """

    def get_program_status(self, program):
        """
            Determine status of this object set by a particular program.
            Useful only for Actuators but defined here for interface compatibility.
        """
        return self.status

    def _set_real_status(self, status, prog):
        # _status_lock is acquired at StatusWorkerTask. Do not call this function from anywhere else!
        # This function is finally called by queue and this sets the value as desired.
        # Thus no need to worry that this function would be called in two threads
        # simultaneously (caller always WorkerThread).
        # prog is always None for non-actuators

        self.logger.debug('_set_real_status %s', status)
        if not self.silent:
            if isinstance(self, AbstractActuator):
                self.logger.info(u"%s %s (by %s) setting status to %s",
                                 self.__class__.__name__, self, prog, status)

        # if not self.slave and prog != getattr(self, 'program', None):
        if not getattr(self, 'slave',
                       True) and not prog is getattr(self, 'program', None):
            self.logger.debug('program had changed, not changing')
            return

        self._change_start = 0.
        change_time = self._last_changed = time.time()

        try:
            if self._status == status:
                self._status_trigger = True
            else:
                if self.history:
                    last_time, last_value = self.history[-1]
                    if self._last_changed - last_time < self.history_frequency:
                        self.history.pop()
                        change_time = last_time
                if status is not None:
                    self.history.append((change_time, status))
                    self.integral.cache_clear()
                self._status = status
        except TraitError as e:
            self.logger.warning(
                'Wrong type of status %s was passed to %s. Error: %s', status,
                self, e)

    def _add_statuschange_to_queue(self, status, prog, with_statuslock=False):
        # This function is used for delayed actions
        if self.system.two_phase_queue:
            with self._status_lock if with_statuslock else nomutex:
                if status != self._status:
                    self._queued_job = StatusWorkerTask(
                        func=self._set_real_status,
                        args=(status, prog),
                        object=self)
                    self.system.worker_thread.put(self._queued_job)
        else:
            self.system.worker_thread.put(
                DummyStatusWorkerTask(self._set_real_status, status, prog))

    def _are_delays_active(self, new_status):
        try:
            mode = "rising" if new_status > self._status else "falling"
        except TypeError:
            # if we can not determine whether value is rising or falling, we'll assume it's rising
            mode = 'rising'
        safety_active = False
        change_active = False
        if self.safety_delay > 0. and self.safety_mode in [mode, "both"]:
            safety_active = True
        if self.change_delay > 0. and self.change_mode in [mode, "both"]:
            change_active = True
        return safety_active, change_active

    def _do_change_status(self, status, force=False):
        """
        This function is called by
           - set_status
           - _update_program_stack if active program is being changed
             - thia may be launched by sensor status change.
             status lock is necessary because these happen from different
             threads.

        This does not directly change status, but adds change request
        to queue.
        """
        self.system.worker_thread.put(
            DummyStatusWorkerTask(self._request_status_change_in_queue,
                                  status,
                                  force=force))

    @property
    def next_scheduled_action(self):
        return getattr(self._timed_action, 'next_action', None)

    def _request_status_change_in_queue(self, status, force=False):
        def timer_func(func, *args):
            with self._status_lock:
                func(*args)
                self._timed_action = None

        with self._status_lock:
            timenow = time.time()
            self.logger.debug("_do_change_status prg:%s status:%s",
                              getattr(self, 'program', None), status)

            if self._timed_action:
                if self._timed_action.is_alive():
                    self.logger.debug(
                        "Cancelling previous safety/change_delay action. Now changing to %s",
                        status)
                    self._timed_action.cancel()
                self._timed_action = None

            self._queued_job = None

            safetydelay_active, changedelay_active = self._are_delays_active(
                status)
            run_now = not (safetydelay_active or changedelay_active)

            if not self._change_start:
                self._change_start = timenow

            if status == self._status and not force:
                self._change_start = 0
                self.logger.debug('Status same %s, no need to change', status)
                return

            orig_changedelay = self.change_delay if changedelay_active else 0
            safetydelay = self.safety_delay if safetydelay_active else 0

            changedelay = orig_changedelay - (timenow - self._change_start)

            if changedelay < 0:
                changedelay = orig_changedelay

            if run_now or (changedelay <= 0. and
                           (timenow - self._last_changed > safetydelay)):
                self.logger.debug(
                    "Adding status change to queue, about to change status to %s",
                    status)
                if self.system.two_phase_queue:
                    self._add_statuschange_to_queue(
                        status, getattr(self, "program", None))
                else:
                    self._set_real_status(status,
                                          getattr(self, 'program', None))

            else:
                timesince = time.time() - self._last_changed
                delaytime = max(0, safetydelay - timesince, changedelay)
                time_after_delay = datetime.datetime.now(
                ) + datetime.timedelta(seconds=delaytime)
                self.logger.debug(
                    "Scheduling safety/change_delay timer for %f sek. Now %s. Going to change to %s.",
                    delaytime, self._status, status)
                self._timed_action = threading.Timer(
                    delaytime,
                    timer_func,
                    args=(self._add_statuschange_to_queue, status,
                          getattr(self, "program", None), False))
                self._timed_action.name = "Safety/change_delay for %s timed at %s (%f sek)" % (
                    self.name, time_after_delay, delaytime)
                self._timed_action.next_action = time_after_delay
                self._timed_action.start()
                return False
Beispiel #30
0
class AbstractSensor(StatusObject):
    """ Base class for all sensors """

    #: Is sensor user-editable in UIs. This variable is meant for per-instance tuning for Sensors,
    #: whereas :attr:`.editable` is for per-class adjustment.
    user_editable = CBool(False)

    #: Status type is defined as _status, but always use :attr:`.status` property to set/get values.
    _status = Any

    #: Default value for status
    default = Any

    #: If non-zero, Sensor status will be reset to default after defined time (in seconds).
    reset_delay = CFloat

    _reset_timer = Any(transient=True)

    #: Do not log status changes
    silent = CBool(False)

    #: Filter status with a function (lambdas are not serializable, don't use them if you are
    #: using system state saving)
    status_filter = Any

    view = StatusObject.view + ['default', 'name', 'tags', 'reset_delay']
    simpleview = StatusObject.simple_view + ['_status']

    def get_as_datadict(self):
        d = super().get_as_datadict()
        d.update(dict(user_editable=self.user_editable))
        return d

    def _setup_reset_delay(self):
        if self.reset_delay:
            if self._reset_timer and self._reset_timer.is_alive():
                self._reset_timer.cancel()
            self._reset_timer = threading.Timer(
                self.reset_delay, lambda: self.set_status(self.default))
            self._reset_timer.start()

    def set_status(self, status, origin=None, force=False):
        """
            Compatibility to actuator class.
            Also :class:`~automate.callables.builtin_callables.SetStatus`
            callable can be used for sensors too, if so desired.
        """

        if status != self.default:
            self._setup_reset_delay()

        if self.status_filter:
            status = self.status_filter(status)

        return self._do_change_status(status, force)

    def update_status(self):
        """A method to read and update actual status. Implement it in subclasses, if necessary"""

    def _status_changed(self):
        if not self.silent:
            self.logger.info("%s status changed to %s", self,
                             repr(self.status))

    def __str__(self):
        return self.name

    def setup_system(self, system, *args, **kwargs):
        name, traits = self._passed_arguments
        default = traits.get('default', None)
        super().setup_system(system, *args, **kwargs)
        load_state = kwargs.get('load_state', None)
        if not default is None and not load_state:
            self.set_status(default)
        elif load_state and self._status is not None:
            self.history.append((time.time(), self._status))