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)
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)
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
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()
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()
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)
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)
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)
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)
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)
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)
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')
class BoolActuator(AbstractActuator): """ Boolean valued actuator""" _status = CBool(transient=True) default = CBool
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)
class UserBoolSensor(AbstractSensor): """Boolean-valued user-editable sensor""" user_editable = CBool(True) _status = CBool
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()
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
class UserAnySensor(AbstractSensor): """User editable sensor type that accepts values of any types""" user_editable = CBool(True) _status = Any
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()
class FloatActuator(AbstractActuator): """Floating point valued actuator""" _status = CFloat(transient=True) default = CFloat silent = CBool(True)
class UserFloatSensor(AbstractNumericSensor): """Float-valued user-editable sensor""" user_editable = CBool(True) _status = CFloat silent = CBool(True)
class UserStrSensor(AbstractSensor): """String-valued user-editable sensor""" user_editable = CBool(True) _status = CUnicode
class UserIntSensor(AbstractNumericSensor): """Integer-valued user-editable sensor""" user_editable = CBool(True) _status = CInt(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)
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)
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
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))