Source code for parlay.items.parlay_standard

import functools
from base import BaseItem
from parlay.protocols.utils import message_id_generator
from parlay.protocols.local_item import LocalItemProtocol
from twisted.internet import defer, threads
from twisted.python import failure
from parlay.server.broker import Broker, run_in_broker, run_in_thread
from twisted.internet.task import LoopingCall
from parlay.items.threaded_item import ITEM_PROXIES, ThreadedItem, ListenerStatus
from parlay.items.base import INPUT_TYPES, MSG_STATUS, MSG_TYPES, TX_TYPES, INPUT_TYPE_DISCOVERY_LOOKUP, \
    INPUT_TYPE_CONVERTER_LOOKUP
import os
import re
import inspect
import Queue
import datetime

FILE_CAP_SIZE = 400  # megabytes
FILE_CAP_UNITS = "MB"
SIZE_STEP = 1024  # bytes per megabyte


[docs]class ParlayStandardItem(ThreadedItem): """ This is a parlay standard item. It supports building inputs for the UI in an intuitive manner during discovery. Inherit from it and use the parlay decorators to get UI functionality """ def __init__(self, item_id, name, reactor=None, adapter=None): # call parent ThreadedItem.__init__(self, item_id, name, reactor, adapter) self._content_fields = [] self._topic_fields = [] self._properties = {} # Dictionary from name to (attr_name, read_only, write_only) self._datastreams = {} self.item_type = None
[docs] def create_field(self, msg_key, input, label=None, required=False, hidden=False, default=None, dropdown_options=None, dropdown_sub_fields=None): """ Create a field for the UI """ # make sure input is valid assert input in INPUT_TYPES.__dict__ discovery = {"MSG_KEY": msg_key, "INPUT": input, "REQUIRED": required, "HIDDEN": hidden} if label is not None: discovery["LABEL"] = label if default is not None: discovery["DEFAULT"] = default if input == INPUT_TYPES.DROPDOWN: discovery["DROPDOWN_OPTIONS"] = dropdown_options discovery["DROPDOWN_SUB_FIELDS"] = dropdown_sub_fields return discovery
[docs] def add_field(self, msg_key, input, label=None, required=False, hidden=False, default=None, dropdown_options=None, dropdown_sub_fields=None, topic_field=False): """ Add a field to this items discovery. This field will show up in the item's CARD in the UI """ discovery = self.create_field(msg_key, input, label, required, hidden, default, dropdown_options, dropdown_sub_fields) if topic_field: self._topic_fields.append(discovery) else: self._content_fields.append(discovery)
[docs] def add_property(self, id, attr_name=None, input=INPUT_TYPES.STRING, read_only=False, write_only=False, name=None): """ Add a property to this Item. :param id : the id of the name :param name : The name of the property (defaults to ID) :param attr_name = the name of the attr to set in 'self' when setting and getting (None if same as name) :param read_only = Read only :param write_only = write_only """ name = name if name is not None else id attr_name = attr_name if attr_name is not None else name # default # attr_name isn't needed for discovery, but for lookup self._properties[id] = {"PROPERTY": id, "PROPERTY_NAME": name, "ATTR_NAME": attr_name, "INPUT": input, "READ_ONLY": read_only, "WRITE_ONLY": write_only} # add to internal list
[docs] def add_datastream(self, id, attr_name=None, units="", name=None): """ Add a datastream to this Item. :param id : The id of the stream :param name: The name of the datastream (defaults to id) :param attr_name: the name of the attr to set in 'self' when setting and getting (same as name if None) :param units: units of streaming value that will be reported during discovery """ name = name if name is not None else id attr_name = attr_name if attr_name is not None else name # default # attr_name isn't needed for discovery, but for lookup self._datastreams[id] = {"STREAM": id, "STREAM_NAME": name, "ATTR_NAME": attr_name, "UNITS": units} # add to internal list
[docs] def clear_fields(self): """ clears all fields. Useful to change the discovery UI """ del self._topic_fields[:] del self._content_fields[:]
[docs] def get_discovery(self): """ Discovery method. You can override this in a subclass if you want, but it will probably be easier to use the self.add_property and self.add_field helper methods and call this method like: def get_discovery(self): discovery = ParlayStandardItem.get_discovery(self) # do other stuff for subclass here return discovery """ # get from parent discovery = BaseItem.get_discovery(self) discovery["TOPIC_FIELDS"] = self._topic_fields discovery["CONTENT_FIELDS"] = self._content_fields discovery["PROPERTIES"] = sorted([x for x in self._properties.values()], key=lambda v: v['PROPERTY']) discovery["DATASTREAMS"] = sorted([x for x in self._datastreams.values()], key=lambda v: v['STREAM']) if self.item_type is not None: discovery["TYPE"] = self.item_type return discovery
[docs] def send_file(self, filename, receiver=None): """ send file contents as an event message (EVENT is ParlaySendFileEvent) to a receiver :param filename: path to file that needs to be sent :type filename: str :param receiver: ID that the file needs to be sent to. by default, the receiver is None meaning the file sending event should be broadcast. If not broadcast this will generally be "UI" :type receiver: str the item will send an event message. The contents will be formatted in the following way: contents: { "EVENT": "ParlaySendFileEvent" "DESCRIPTION": [filename being sent as string], "INFO": [contents of file as string] } """ file_stats = os.stat(filename) size_mb = (int(file_stats.st_size) // SIZE_STEP) // SIZE_STEP if size_mb > FILE_CAP_SIZE: raise IOError(" ".join(["File is too big! Please ensure the file is less than", str(FILE_CAP_SIZE), FILE_CAP_UNITS, "and try again."])) with open(filename, "r") as file_to_send: try: file_contents = file_to_send.read() contents = {"EVENT": "ParlaySendFileEvent", "DESCRIPTION": filename, "INFO": file_contents} except IOError as e: print e return if receiver is None: self.send_message(tx_type=TX_TYPES.BROADCAST, msg_type=MSG_TYPES.EVENT, contents=contents) else: self.send_message(to=receiver, msg_type=MSG_TYPES.EVENT, contents=contents)
[docs] def send_message(self, to=None, from_=None, contents=None, tx_type=TX_TYPES.DIRECT, msg_type=MSG_TYPES.DATA, msg_id=None, msg_status=MSG_STATUS.OK, response_req=False, extra_topics=None): """ Sends a Parlay standard message. contents is a dictionary of contents to send """ if msg_id is None: msg_id = self._message_id_generator.next() if contents is None: contents = {} if from_ is None: from_ = self.item_id msg = {"TOPICS": {"FROM": from_, "TX_TYPE": tx_type, "MSG_TYPE": msg_type, "MSG_ID": msg_id, "MSG_STATUS": msg_status, "RESPONSE_REQ": response_req}, "CONTENTS": contents} if to is not None: msg["TOPICS"]["TO"] = to if extra_topics is not None: msg["TOPICS"].update(extra_topics) self.publish(msg)
[docs] def send_parlay_command(self, to, command, _timeout=2**32, **kwargs): """ Send a parlay command to an known ID """ msg = self.make_msg(to, command, msg_type=MSG_TYPES.COMMAND, direct=True, response_req=True, COMMAND=command, **kwargs) self.send_parlay_message(msg, timeout=_timeout, wait=False) return CommandHandle(msg, self)
[docs]def parlay_command(async=False, auto_type_cast=True): """ Make the decorated method a parlay_command. :param async: If True, will run as a normal twisted async function call. If False, parlay will spawn a separate thread and run the function synchronously (Default false) :param auto_type_cast: If true, will search the function's docstring for type info about the arguments, and provide that information during discovery """ def decorator(fn): if async: if inspect.isgeneratorfunction(fn): wrapper = run_in_broker(defer.inlineCallbacks(fn)) else: wrapper = run_in_broker(fn) else: wrapper = run_in_thread(fn) wrapper._parlay_command = True wrapper._parlay_fn = fn # in case it gets wrapped again, this is the actual function so we can pull kwarg names wrapper._parlay_arg_conversions = {} # if type casting desired, this dict from param_types to converting funcs wrapper._parlay_arg_discovery = {} if auto_type_cast and fn.__doc__ is not None: for line in fn.__doc__.split("\n"): m = re.search(r"[@:]type\s+(\w+)\s*[ :]\s*(\w+\[?\w*\]?)", line) if m is not None: arg_name, arg_type = m.groups() if arg_type in INPUT_TYPE_CONVERTER_LOOKUP: # if we know how to convert it wrapper._parlay_arg_conversions[arg_name] = INPUT_TYPE_CONVERTER_LOOKUP[arg_type] # add to convert list wrapper._parlay_arg_discovery[arg_name] = INPUT_TYPE_DISCOVERY_LOOKUP.get(arg_type, INPUT_TYPES.STRING) return wrapper return decorator
[docs]class ParlayProperty(object): """ A convenience class for creating properties of ParlayCommandItems. **Example: How to define a property**:: class MyItem(ParlayCommandItem): x = ParlayProperty(default=0, val_type=int) def __init__(self, item_id, item_name): ParlayCommandItem.__init__(self, item_id, item_name) ... **Example: How to access a property from a script**:: setup() discover() my_item = get_item_by_name("MyItem") original_value = my_item.x my_item.x = 5 """ def __init__(self, default=None, val_type=str, read_only=False, write_only=False, custom_read=None, custom_write=None): """ Init method for the ParlayProperty class :param default : an initial value for the property :param val_type : the python type of the value. e.g. str, int, list, etc. The value will be coerced to this type on set and throw an exception if it couldn't be coerced :param read_only : Set to true to make read only :param write_only : Set to true to make write only :param custom_write : Custom write function to call when writing :param custom_read : Custom read function to get the value :return: none """ self._val_lookup = {} # lookup based on instance self._init_val = default self._read_only = read_only self._write_only = write_only self._val_type = val_type self._custom_read = custom_read self._custom_write = custom_write # can't be both read and write only assert(not(self._read_only and write_only)) def __get__(self, instance, objtype=None): # return this object if we're accessing it from the class level, instead of the object level if inspect.isclass(instance): return self if self._custom_read is None: return self._val_lookup.get(instance, self._init_val) else: return self._custom_read() def __set__(self, instance, value): # special case for boolean if self._val_type == bool and isinstance(value, basestring): if value.lower() == "false" or value == "0": value = False # coerce the val val = value if self._val_type is None else self._val_type(value) self._val_lookup[instance] = val if self._custom_write is None else self._custom_write(val)
[docs]class ParlayDatastream(object): """ A convenience class for creating datastreams within ParlayCommandItems. Example Usage:: class Balloon(ParlayCommandItem): altitude = ParlayDatastream(default=0, units="ft") """ def __init__(self, default=None, val_type=None, units="", callback=lambda _: _): """ Init method for ParlayDatastream class :param default: default value for the streaming data :param units: optional string indicating units, to be returned during discovery :param callback: a functiomn to call with the new value every time this datastream changes :return: """ self._default_val = default self.listeners = {} # dict: item instance -> { dict: requester_id -> listener} self.units = units self.broker = Broker.get_instance() self._vals = {} # dict: item instance -> value self._val_type = val_type self._callback = callback def __get__(self, instance, objtype=None): return self._vals.get(instance, self._default_val) def __set__(self, instance, value): self._vals[instance] = value # set the actual value for listener in self.listeners.get(instance, {}).values(): listener(value) # call any listeners self._callback(value) # call my callback
[docs] def listen(self, instance, listener, requester_id): """ Listen to the datastream. Will call calback whenever there is a change """ listener_dict = self.listeners.get(instance, {}) listener_dict[requester_id] = listener self.listeners[instance] = listener_dict
[docs] def stop(self, instance, requester_id): """ Stop listening """ listener_dict = self.listeners.get(instance, {}) if requester_id in listener_dict: del listener_dict[requester_id]
[docs]class BadStatusError(Exception): """ Throw this if you want to return a Bad Status! """ def __init__(self, error, description=""): self.error = error self.description = description def __str__(self): return str(self.error) + "\n" + str(self.description)
[docs]class ParlayCommandItem(ParlayStandardItem): """ This is a Parlay Item that defines functions that serve as commands, with arguments. This class enables you to use the :func:`~parlay_standard.parlay_command` decorator over your command functions. Then, those functions will be available as commands that can be called from the user interface, scripts, or by other items. **Example: How to define a class as a ParlayCommandItem**:: from parlay import local_item, ParlayCommandItem, parlay_command @local_item() class MotorSimulator(ParlayCommandItem): def __init__(self, item_id, item_name): self.coord = 0 ParlayCommandItem.__init__(self, item_id, item_name) @parlay_command def move_to_coordinate(self, coordinate) self.coord = coordinate **Example: How to instantiate an item from the above definition**:: import parlay from motor_sim import MotorSimulator MotorSimulator("motor1", "motor 1") # motor1 will be discoverable parlay.start() **Example: How to interact with the instantiated item from a Parlay script**:: # script_move_motor.py setup() discover() motor_sim = get_item_by_name("motor 1") motor_sim.move_to_coordinate(500) """ #! change this to have a custom subsystem ID for the entire Python subsystem SUBSYSTEM_ID = "python" # id generator for auto numbering class instances __ID_GEN = message_id_generator(2**32, 1) def __init__(self, item_id=None, name=None, reactor=None, adapter=None): """ :param item_id : The id of the Item (Must be unique in this system) :type item_id str | int :param name : the human readible name of this item. (Advised to be unique, but not required) :type name str :rtype : object """ if item_id is None: item_id = ParlayCommandItem.SUBSYSTEM_ID + "." + self.__class__.__name__ + "." + str(ParlayCommandItem.__ID_GEN.next()) if name is None: name = self.__class__.__name__ ParlayStandardItem.__init__(self, item_id, name, reactor, adapter) self._commands = {} # dict with command name -> callback function # ease of use deferred for wait* functions self._wait_for_next_sent_message = defer.Deferred() self._wait_for_next_recv_message = defer.Deferred() self.subscribe(self._wait_for_next_recv_msg_subscriber, TO=self.item_id) self.subscribe(self._wait_for_next_sent_msg_subscriber, FROM=self.item_id) # add any function that have been decorated for member_name in [x for x in dir(self) if not x.startswith("__")]: member = getattr(self, member_name, {}) # are we a method? and do we have the flag, and is it true? if callable(member) and hasattr(member, "_parlay_command") and member._parlay_command: self._commands[member_name] = member # build the sub-field based on their signature arg_names = member._parlay_fn.func_code.co_varnames[1:member._parlay_fn.func_code.co_argcount] # remove self # get a list of the default arguments # (don't use argspec because it is needlesly strict and fails on perfectly valid Cython functions) defaults = member._parlay_fn.func_defaults if member._parlay_fn.func_defaults is not None else [] # cut params to only the last x (defaults are always at the end of the signature) params = arg_names params = params[len(params) - len(defaults):] default_lookup = dict(zip(params, defaults)) # add the sub_fields, trying to best guess their discovery types. If not possible then default to STRING member.__func__._parlay_sub_fields = [self.create_field(x, member._parlay_arg_discovery.get(x, INPUT_TYPES.STRING), default=default_lookup.get(x, None)) for x in arg_names] # run discovery to init everything for a first time # call it immediately after init self._adapter.reactor.callLater(0, ParlayCommandItem.get_discovery, self)
[docs] def get_discovery(self): """ Will auto-populate the UI with inputs for commands """ # start fresh self.clear_fields() self._add_commands_to_discovery() self._add_properties_to_discovery() self._add_datastreams_to_discovery() # call parent return ParlayStandardItem.get_discovery(self)
def _add_commands_to_discovery(self): """ Add commands to the discovery for user input """ if len(self._commands) == 0: return # nothing to do here else: # more than 1 option command_names = self._commands.keys() command_names.sort() # pretty-sort # add the command selection dropdown self.add_field("COMMAND", INPUT_TYPES.DROPDOWN, label='command', default=command_names[0], dropdown_options=[(x, x) for x in command_names], dropdown_sub_fields=[self._commands[x]._parlay_sub_fields for x in command_names]) def _add_properties_to_discovery(self): """ Add properties to discovery """ # clear properties self._properties = {} for member_name in [x for x in dir(self.__class__) if not x.startswith("__")]: member = self.__class__.__dict__.get(member_name, None) if isinstance(member, ParlayProperty): self.add_property(member_name, member_name, # lookup type name based on type func (e.g. int()) INPUT_TYPE_DISCOVERY_LOOKUP.get(member._val_type.__name__, "STRING"), read_only=member._read_only, write_only=member._write_only) def _add_datastreams_to_discovery(self): """ Add properties to discovery """ # clear properties self._datastreams = {} for member_name in sorted([x for x in dir(self.__class__) if not x.startswith("__")]): member = self.__class__.__dict__.get(member_name, None) if isinstance(member, ParlayDatastream): self.add_datastream(member_name, member_name, member.units) def _send_parlay_message(self, msg): self.publish(msg)
[docs] def on_message(self, msg): """ Will handle command messages automatically. Returns True if the message was handled, False otherwise """ # run it through the listeners for processing self._runListeners(msg) topics, contents = msg["TOPICS"], msg["CONTENTS"] msg_type = topics.get("MSG_TYPE", "") # handle property messages if msg_type == "PROPERTY": action = contents.get('ACTION', "") property_id = str(contents.get('PROPERTY', "")) try: if action == 'SET': assert 'VALUE' in contents # we need a value to set! setattr(self, self._properties[property_id]["ATTR_NAME"], contents['VALUE']) self.send_response(msg, {"PROPERTY": property_id, "ACTION": "RESPONSE"}) return True elif action == "GET": val = getattr(self, self._properties[property_id]["ATTR_NAME"]) self.send_response(msg, {"PROPERTY": property_id, "ACTION": "RESPONSE", "VALUE": val}) return True except Exception as e: self.send_response(msg, {"PROPERTY": property_id, "ACTION": "RESPONSE", "DESCRIPTION": str(e)}, msg_status=MSG_STATUS.ERROR) # handle data stream messages if msg_type == "STREAM": try: stream_id = str(contents["STREAM"]) remove = contents.get("STOP", False) requester = topics["FROM"] def sample(stream_value): self.send_message(to=requester, msg_type=MSG_TYPES.STREAM, contents={'VALUE': stream_value}, extra_topics={"STREAM": stream_id}) if remove: # if we've been asked to unsubscribe # access the stream object through the class's __dict__ so we don't just end up calling the __get__() self.__class__.__dict__[stream_id].stop(self, requester) else: #listen in if we're subscribing # access the stream object through the class's __dict__ so we don't just end up calling the __get__() self.__class__.__dict__[stream_id].listen(self, sample, requester) except Exception as e: self.send_response(msg, {"STREAM": contents.get("STREAM", "__UNKNOWN_STREAM__"), "ACTION": "RESPONSE", "DESCRIPTION": str(e)}, msg_status=MSG_STATUS.ERROR) # handle 'command' messages command = contents.get("COMMAND", "") if command in self._commands: arg_names = () try: method = self._commands[command] arg_names = method._parlay_fn.func_code.co_varnames[1: method._parlay_fn.func_code.co_argcount] # remove 'self' # add the defaults to the msg if they're not overwritten defaults = method._parlay_fn.func_defaults if method._parlay_fn.func_defaults is not None else [] # cut params to only the last x (defaults are always at the end of the signature) params = arg_names params = params[len(params) - len(defaults):] default_lookup = dict(zip(params, defaults)) for k,v in default_lookup.iteritems(): if k not in msg["CONTENTS"] or msg["CONTENTS"][k] is None: msg["CONTENTS"][k] = v kws = {k: msg["CONTENTS"][k] for k in arg_names} try: # do any type conversions (default to whatever we were sent if no conversions) kws = {k: method._parlay_arg_conversions[k](v) if k in method._parlay_arg_conversions else v for k, v in kws.iteritems()} except (ValueError, TypeError) as e: self.send_response(msg, contents={"DESCRIPTION": e.message, "ERROR": "BAD TYPE"}, msg_status=MSG_STATUS.ERROR) return None # try to run the method, return the data and say status ok def run_command(): return method(**kws) self.send_response(msg, msg_status=MSG_STATUS.PROGRESS) result = defer.maybeDeferred(run_command) result.addCallback(lambda r: self.send_response(msg, {"RESULT": r})) def bad_status_errback(f): # is this an explicitly bad status? if isinstance(f.value, BadStatusError): error = f.value self.send_response(msg, contents={"DESCRIPTION": error.description, "ERROR": error.error}, msg_status=MSG_STATUS.ERROR) # or is it unknown generic exception? else: self.send_response(msg, contents={"DESCRIPTION": f.getErrorMessage(), "TRACEBACK": f.getTraceback()}, msg_status=MSG_STATUS.ERROR) # if we get an error, then return it result.addErrback(bad_status_errback) except KeyError as e: self.send_response(msg, contents={"DESCRIPTION": "Missing Argument '%s' to command '%s'" % (e.args[0], command), "TRACEBACK": ""}, msg_status=MSG_STATUS.ERROR) return True else: return False
def _wait_for_next_sent_msg_subscriber(self, msg): d = self._wait_for_next_sent_message # set up new one before calling callback in case things triggered by the callback need to wait for the next sent self._wait_for_next_sent_message = defer.Deferred() d.callback(msg) def _wait_for_next_recv_msg_subscriber(self, msg): d = self._wait_for_next_recv_message # set up new one before calling callback in case things triggered by the callback need to wait for the next sent self._wait_for_next_recv_message = defer.Deferred() d.callback(msg)
[docs] def wait_for_next_sent_msg(self): """ Returns a deferred that will callback on the next message we SEND """ return self._wait_for_next_sent_message
[docs] def wait_for_next_recv_msg(self): """ Returns a deferred that will callback on the next message we RECEIVE """ return self._wait_for_next_recv_message
[docs] def send_response(self, msg, contents=None, msg_status=MSG_STATUS.OK): if contents is None: contents = {} # swap to and from to, from_ = msg["TOPICS"]["FROM"], msg["TOPICS"]['TO'] self.send_message(to, from_, tx_type=TX_TYPES.DIRECT, msg_type=MSG_TYPES.RESPONSE, msg_id=msg["TOPICS"]["MSG_ID"], msg_status=msg_status, response_req=False, contents=contents)
[docs]class ParlayStandardScriptProxy(object): """ A proxy class for the script to use, that will auto-detect discovery information and allow script writers to intuitively use the item """
[docs] class PropertyProxy(object): """ Proxy class for a parlay property """ def __init__(self, id, item_proxy, blocking_set=True): self._id = id self._item_proxy = item_proxy # do we want to block on a set until we get the ACK? self._blocking_set = blocking_set def __get__(self, instance, owner): msg = instance._script.make_msg(instance.item_id, None, msg_type=MSG_TYPES.PROPERTY, direct=True, response_req=True, PROPERTY=self._id, ACTION="GET") resp = instance._script.send_parlay_message(msg) # return the VALUE of the response return resp["CONTENTS"]["VALUE"] def __set__(self, instance, value): try: msg = instance._script.make_msg(instance.item_id, None, msg_type=MSG_TYPES.PROPERTY, direct=True, response_req=self._blocking_set, PROPERTY=self._id, ACTION="SET", VALUE=value) # Wait until we're sure its set resp = instance._script.send_parlay_message(msg) except TypeError as e: print "Could not set property to non JSON serializable type. You tried to set", self._id, "to", value except Exception as e: print "Caught general exception while trying to set", self._id, "to", value def __str__(self): return str(self.__get__(self._item_proxy, self._item_proxy))
[docs] class StreamProxy(object): """ Proxy class for a parlay stream """ MAX_LOG_SIZE = 1000000 def __init__(self, id, item_proxy, rate): self._id = id self._item_proxy = item_proxy self._val = None self._rate = rate self._listener = lambda _: _ self._new_value = defer.Deferred() self._reactor = self._item_proxy._script._reactor self._subscribed = False self._is_logging = False self._log = [] item_proxy._script.add_listener(self._update_val_listener)
[docs] def attach_listener(self, listener): self._listener = listener
[docs] def wait_for_value(self): """ If in thread: Will block until datastream is updated If in Broker: Will return deferred that is called back with the datastream value when updated """ self.get() return self._reactor.maybeblockingCallFromThread(lambda: self._new_value)
[docs] def get(self): if not self._subscribed: msg = self._item_proxy._script.make_msg(self._item_proxy.item_id, None, msg_type=MSG_TYPES.STREAM, direct=True, response_req=False, STREAM=self._id, STOP=False, RATE=self._rate) self._item_proxy._script.send_parlay_message(msg) self._subscribed = True return self._val
[docs] def stop(self): """ Stop streaming :return: """ msg = self._item_proxy._script.make_msg(self._item_proxy.item_id, None, msg_type=MSG_TYPES.STREAM, direct=True, response_req=False, STREAM=self._id, STOP=True) self._item_proxy._script.send_parlay_message(msg) self._subscribed = False
def _update_val_listener(self, msg): """ Script listener that will update the val whenever we get a stream update """ topics, contents = msg["TOPICS"], msg['CONTENTS'] if topics.get("MSG_TYPE", "") == MSG_TYPES.STREAM and topics.get("STREAM", "") == self._id \ and 'VALUE' in contents: new_val = contents["VALUE"] if self._is_logging: self._add_to_log(new_val) self._listener(new_val) self._val = new_val temp = self._new_value self._new_value = defer.Deferred() # set up a new one temp.callback(new_val) return ListenerStatus.KEEP_LISTENER def _add_to_log(self, update_val): """ Helper function for adding the latest val to the log list """ if len(self._log) >= self.MAX_LOG_SIZE: # handle overflow # NOTE: pop() then append() is faster than list[1:].append() self._log.pop(0) self._log.append(self._create_data_entry(update_val)) # if adequate list size, append normally
[docs] def start_logging(self, rate): """ Script function that enables logging. When a new value is pushed to the datastream the value will get pushed to the end of the log. """ self._rate = rate self.get() self._is_logging = True
[docs] def stop_logging(self): """ Script function that disables logging. """ self.stop() self._is_logging = False # reset logging variable
[docs] def clear_log(self): """ Resets the internal log """ self._log = []
[docs] def get_log(self): """ Public interface to get the stream log """ return self._log
@staticmethod def _create_data_entry(update_val): """ Creates a data entry with val and timestamp """ return datetime.datetime.now(), update_val
def __init__(self, discovery, script): """ discovery: The discovery dictionary for the item that we're proxying script: The script object we're running in """ self.name = discovery["NAME"] self.item_id = discovery["ID"] self._discovery = discovery self._script = script self.datastream_update_rate_hz = 2 self.timeout = 120 self._command_id_lookup = {} # look at the discovery and add all commands, properties, and streams # commands func_dict = next(iter([x for x in discovery['CONTENT_FIELDS'] if x['MSG_KEY'] == 'COMMAND']), None) if func_dict is not None: # if we have commands command_names = [x[0] for x in func_dict["DROPDOWN_OPTIONS"]] command_ids = [x[1] for x in func_dict["DROPDOWN_OPTIONS"]] command_args = [x for x in func_dict["DROPDOWN_SUB_FIELDS"]] for i in range(len(command_names)): func_name = command_names[i] func_id = command_ids[i] self._command_id_lookup[func_name] = func_id # add to lookup for fast access later arg_names = [(x['MSG_KEY'], x.get('DEFAULT', None)) for x in command_args[i]] def _closure_wrapper(f_name=func_name, f_id=func_id, _arg_names=arg_names, _self=self): @run_in_broker @defer.inlineCallbacks def func(*args, **kwargs): if len(args) + len(kwargs) > len(_arg_names): raise KeyError("Too many Arguments. Expected arguments are: " + str([str(x[0]) for x in _arg_names])) # add positional args with name lookup for j in range(len(args)): kwargs[_arg_names[j][0]] = args[j] # check args for name, default in _arg_names: if name not in kwargs and default is None: raise TypeError("Missing argument: "+name) # send the message and block for response msg = _self._script.make_msg(_self.item_id, f_id, msg_type=MSG_TYPES.COMMAND, direct=True, response_req=True, COMMAND=f_name, **kwargs) resp = yield _self._script.send_parlay_message(msg, timeout=_self.timeout) yield defer.returnValue(resp['CONTENTS'].get('RESULT', None)) # set this object's function to be that function setattr(_self, f_name, func) # need this trickery so closures work in a loop _closure_wrapper() # properties for prop in discovery.get("PROPERTIES", []): property_id = prop["PROPERTY"] property_name = prop["PROPERTY_NAME"] if "PROPERTY_NAME" in prop else property_id setattr(self, property_name, ParlayStandardScriptProxy.PropertyProxy(property_id, self)) # streams for stream in discovery.get("DATASTREAMS", []): stream_id = stream["STREAM"] stream_name = stream["STREAM_NAME"] if "STREAM_NAME" in stream else stream_id setattr(self, stream_name, ParlayStandardScriptProxy.StreamProxy(stream_id, self, self.datastream_update_rate_hz))
[docs] def send_parlay_command(self, command, **kwargs): """ Manually send a parlay command. Returns a handle that can be paused on """ # send the message and block for response msg = self._script.make_msg(self.item_id, self._command_id_lookup[command], msg_type=MSG_TYPES.COMMAND, direct=True, response_req=True, COMMAND=command, **kwargs) # make the handle that sets up the listeners handle = CommandHandle(msg, self._script) self._script.send_parlay_message(msg, timeout=self.timeout, wait=False) return handle
[docs] def get_datastream_handle(self, name): return object.__getattribute__(self, name)
# Some re-implementation so our instance-bound descriptors will work instead of having to be class-bound. # Thanks: http://blog.brianbeck.com/post/74086029/instance-descriptors def __getattribute__(self, name): value = object.__getattribute__(self, name) if isinstance(value, ParlayStandardScriptProxy.PropertyProxy): value = value.__get__(self, self.__class__) return value def __setattr__(self, name, value): try: obj = object.__getattribute__(self, name) except AttributeError: pass else: if isinstance(obj, ParlayStandardScriptProxy.PropertyProxy): return obj.__set__(self, value) return object.__setattr__(self, name, value)
# register the proxy so it can be used in scripts ITEM_PROXIES['ParlayStandardItem'] = ParlayStandardScriptProxy ITEM_PROXIES['ParlayCommandItem'] = ParlayStandardScriptProxy
[docs]class CommandHandle(object): """ This is a command handle that wraps a command message and allows blocking until certain messages are recieved """ def __init__(self, msg, script): """ :param msg the message that we're handling :param script the script context that we're in """ topics, contents = msg["TOPICS"], msg["CONTENTS"] assert 'MSG_ID' in topics and 'TO' in topics and 'FROM' in topics self._msg = msg self._msg_topics = topics self._msg_content = contents # :type ParlayScript self._script = script self.msg_list = [] # list of al messages with the same message id but swapped TO and FROM self._done = False # True when we're done listening (So we can clean up) self._queue = Queue.Queue() # add our listener self._script.add_listener(self._generic_on_message) def _generic_on_message(self, msg): """ Listener function that powers the handle. This should only be called by a script in the reactor thread in its listener loop """ topics, contents = msg["TOPICS"], msg["CONTENTS"] if topics.get("MSG_ID", None) == self._msg_topics["MSG_ID"] \ and topics.get("TO", None) == self._msg_topics["FROM"] \ and topics.get("FROM", None) == self._msg_topics['TO']: # add it to the list if the msg ids match but to and from are swapped (this is for inspection later) self.msg_list.append(msg) # add it to the message queue for messages that we have not looked at yet self._queue.put_nowait(msg) status = topics.get("MSG_STATUS", None) msg_type = topics.get("MSG_TYPE", None) if msg_type == MSG_TYPES.RESPONSE and status != MSG_STATUS.PROGRESS: # if it's a response but not an ack, then we're done self._done = True # remove this function from the listeners list return self._done @run_in_thread
[docs] def wait_for(self, fn, timeout=None): """ Block and wait for a message in our queue where fn returns true. Return that message """ msg = self._queue.get(timeout=timeout, block=True) while not fn(msg): msg = self._queue.get(timeout=timeout, block=True) return msg
@run_in_thread
[docs] def wait_for_complete(self): """ Called from a scripts thread. Blocks until the message is complete. """ msg = self.wait_for(lambda msg: msg["TOPICS"].get("MSG_STATUS",None) != MSG_STATUS.PROGRESS and msg["TOPICS"].get("MSG_TYPE", None) == MSG_TYPES.RESPONSE) # if the status is OK, then get the result, optherwise get the description status = msg["TOPICS"].get("MSG_STATUS", None) if status == MSG_STATUS.OK: return msg["CONTENTS"]["RESULT"] elif status == MSG_STATUS.ERROR: raise BadStatusError("Error returned from item", msg["CONTENTS"].get("DESCRIPTION", ""))
@run_in_thread
[docs] def wait_for_ack(self): """ Called from a scripts thread. Blocks until the message is ackd """ msg = self.wait_for(lambda msg: msg["TOPICS"].get("MSG_STATUS",None) == MSG_STATUS.PROGRESS and msg["TOPICS"].get("MSG_TYPE", None) == MSG_TYPES.RESPONSE) return msg