def __init__(self, adapter, port): """ :param adapter: The adapter that will serve as an interface for interacting with the broker """ self._port = port # A list of items that we will need to discover for. # The base protocol will use this dictionary to feed items to # the UI self.items = [] self._error_codes = [] # The list of connected item IDs found in the initial sweep in # connectionMade() self._item_ids = [] BaseProtocol.__init__(self) # Set the LineReceiver to line mode. This causes lineReceived to be called # when data is sent to the serial port. We will get a line whenever the END_BYTE # appears in the buffer self.setLineMode() self.delimiter = END_BYTE_STR # The buffer that we will be storing the data that arrives via the serial connection self._binary_buffer = bytearray() self.adapter = adapter # Event IDs are 16-bit (2 byte) numbers so we need a radix # of 65535 or 0xFFFF in hex # NOTE: The number of bits in an event ID is subject to change, # the constant NUM_EVENT_ID_BITS can easily be changed to accommodate this. self._event_id_generator = message_id_generator((2**self.NUM_EVENT_ID_BITS)) # From parlay.utils, calls _message_queue_handler() whenever # a new message is added to the MessageQueue object self._message_queue = MessageQueue(self._message_queue_handler) self._attached_item_d = None # Dictionary that maps ID # to Deferred object self._discovery_msg_ids = {} # Sequence number is a nibble as of now, so the domain should be # 0 <= seq number <= 15 # which means the radix will be 16, but to be safe I'll do # 2^SEQ_BITS where SEQ_BITS is a member constant that can easily be changed self._seq_num = message_id_generator((2**self.SEQ_BITS)) # ACKs should be deferred objects because you want to catch them on the way # back via asynchronous communication. self._ack_deferred = defer.Deferred() # Store discovered item IDs so that we do not push duplicates to the # item requesting the discovery self._in_progress = False self._discovery_deferred = defer.Deferred() self._ack_table = {seq_num : defer.Deferred() for seq_num in range(2**self.SEQ_BITS)} self._ack_window = SlidingACKWindow(self.WINDOW_SIZE, self.NUM_RETRIES)
def __init__(self, item_id, name, reactor=None, adapter=None, parents=None): BaseItem.__init__(self, item_id, name, adapter=adapter, parents=parents) self._reactor = self._adapter.reactor if reactor is None else reactor self._msg_listeners = [] self._system_errors = [] self._system_events = [] self._timer = None self._auto_update_discovery = True #: If True auto update discovery with broadcast discovery messages self.discovery = {} #: The current discovery information to pull from self._message_id_generator = message_id_generator(65535, 100) # Add this listener so it will be first in the list to pickup errors, warnings and events. self.add_listener(self._system_listener) self.add_listener(self._discovery_request_listener) self._adapter.subscribe(self._discovery_broadcast_listener, type='DISCOVERY_BROADCAST')
def __init__(self, item_id, name, reactor=None, adapter=None): BaseItem.__init__(self, item_id, name, adapter=adapter) self._reactor = self._adapter.reactor if reactor is None else reactor self._msg_listeners = [] self._system_errors = [] self._system_events = [] self._timer = None self._auto_update_discovery = True #: If True auto update discovery with broadcast discovery messages self.discovery = {} #: The current discovery information to pull from self._message_id_generator = message_id_generator(65535, 100) # Add this listener so it will be first in the list to pickup errors, warnings and events. self.add_listener(self._system_listener) self.add_listener(self._discovery_request_listener) self._adapter.subscribe(self._discovery_broadcast_listener, type='DISCOVERY_BROADCAST')
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, parents=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, parents) 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 = self.__class__.__dict__.get(member_name, None) # 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: # get a reference to this OBJECTs bound functiion now, now the class's unbound function member = getattr(self, member_name, {}) 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 self.get_discovery() 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) or isinstance(member, ParlayProperty): self.add_datastream(member_name, member_name, "") def _send_parlay_message(self, msg): self.publish(msg) 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": def on_get(val): self.send_response(msg, {"PROPERTY": property_id, "ACTION": "RESPONSE", "VALUE": val}) defer.maybeDeferred(lambda: getattr(self, self._properties[property_id]["ATTR_NAME"]))\ .addCallback(on_get) 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 (that aren't value messages) if msg_type == "STREAM" and 'VALUE' not in contents: 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 = log_stack_on_error(result) 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) 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 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 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)
class PCOMMessage(object): _item_lookup_map = {} # If we get a string ID , we need to assign a item ID. Start at 0xfc00 and go to 0xffff _item_id_generator = message_id_generator(0xffff, 0xfc00) VALID_JSON_MESSAGE_TYPES = [ "COMMAND", "EVENT", "RESPONSE", "PROPERTY", "STREAM" ] GLOBAL_ERROR_CODE_ID = 0xff def __init__(self, to=None, from_=None, msg_id=0, tx_type=None, msg_type=None, attributes=0, response_code=None, response_req=None, msg_status=None, contents=None, data=None, data_fmt=None, topics=None, description=''): # TODO: Change response_req to response_code # private variables only accessed through @property functions self._attributes = None self._format_string = '' self._data = [] self.description = description self.to = to self.from_ = from_ self.msg_id = msg_id self.tx_type = tx_type self.msg_type = msg_type self.response_req = response_req self.msg_status = msg_status self.contents = contents self.priority = 0 self.attributes = attributes self.format_string = data_fmt self.data = data self.response_code = response_code self.topics = topics @classmethod def _get_item_id(cls, name): """ Gets a item ID from an item name """ # if we're an int we're good if type(name) == int: return name if name in cls._item_lookup_map: return cls._item_lookup_map[name] else: # if the item ID wasn't in our map, generate an int for it # and add it to the map item_id = cls._item_id_generator.next() cls._item_lookup_map[name] = item_id cls._item_lookup_map[item_id] = name return item_id @classmethod def _get_name_from_id(cls, item_id): """ Gets a item name from an item ID """ if item_id in cls._item_lookup_map: return cls._item_lookup_map[item_id] return item_id @staticmethod def _look_up_id(map, destination_id, name): if isinstance(name, basestring): # TODO: use .get() to avoid key error return map[destination_id].get(name, None) else: return name @classmethod def _get_data_format(cls, msg): """ Takes a msg and does the appropriate table lookup to obtain the format data for the command/property/stream. Returns a tuple in the form of (data, format) where data is a list and format is a format string. :param msg: :return: """ data = [] fmt = '' if msg.msg_type == "COMMAND": # If the message type is "COMMAND" there should be an # entry in the 'CONTENTS' table for the command ID if msg.to in pcom_serial.PCOM_COMMAND_MAP: # command will be a CommandInfo object that has a list of parameters and format string command_id = msg.contents.get("COMMAND", INVALID_ID) command_int_id = cls._look_up_id( pcom_serial.PCOM_COMMAND_NAME_MAP, msg.to, command_id) if command_int_id is None: logger.error( "Could not find integer command ID for command name: {0}" .format(command_id)) return # TODO: check for KeyError command = pcom_serial.PCOM_COMMAND_MAP[msg.to].get( command_int_id, None) if command is None: return data, fmt fmt = str(msg.contents.get('__format__', command["format"])) for param in command["input params"]: # TODO: May need to change default value to error out data.append(msg.contents.get(str(param), 0)) elif msg.msg_type == "PROPERTY": # If the message type is a "PROPERTY" there should be # a "PROPERTY" entry in the "CONTENTS" that has the property ID action = msg.contents.get('ACTION', None) if action == "GET": data = [] fmt = '' elif action == "SET": if msg.to in pcom_serial.PCOM_PROPERTY_MAP: property_id = msg.contents.get("PROPERTY", INVALID_ID) property = cls._look_up_id( pcom_serial.PCOM_PROPERTY_NAME_MAP, msg.to, property_id) if property is None: logger.error( "Could not find integer property ID for property name: {0}" .format(property)) return data, fmt prop = pcom_serial.PCOM_PROPERTY_MAP[msg.to][property] fmt = prop["format"] content_data = msg.contents.get('VALUE', 0) if type(content_data ) == list: # we have a variable property list data = content_data else: data.append(content_data) data = serial_encoding.cast_data(fmt, data) elif msg.msg_type == "STREAM": # no data or format string for stream messages rate = msg.contents.get("RATE", None) data = [rate] if rate else [] fmt = 'f' if rate else '' return data, fmt @classmethod def from_json_msg(cls, json_msg): """ Converts a dictionary message to a PCOM message object :param json_msg: JSON message :return: PCOM message object """ msg_id = json_msg['TOPICS']['MSG_ID'] to = cls._get_item_id(json_msg['TOPICS']['TO']) from_ = cls._get_item_id(json_msg['TOPICS']['FROM']) msg_type = json_msg['TOPICS']['MSG_TYPE'] response_req = json_msg['TOPICS'].get("RESPONSE_REQ", False) msg_status = 0 # TODO: FIX THIS tx_type = json_msg['TOPICS'].get('TX_TYPE', "DIRECT") contents = json_msg['CONTENTS'] topics = json_msg['TOPICS'] msg = cls(to=to, from_=from_, msg_id=msg_id, response_req=response_req, msg_type=msg_type, msg_status=msg_status, tx_type=tx_type, contents=contents, topics=topics) # Set data and format using class function msg.data, msg.format_string = cls._get_data_format(msg) return msg def _is_response_req(self): """ If the msg is an order a response is expected. :return: """ return (self.category()) == MessageCategory.Order @staticmethod def get_subsystem(id): """" Gets the subsystem of the message. """ return (id & SUBSYSTEM_MASK) >> SUBSYSTEM_SHIFT def _get_data(self): """ Helper function for returning the data of the PCOM Message. Returns an error message if there wasn't any data to get. :param index: :return: """ if len(self.data) == 1: return self.data[0] if len(self.data) > 1: return self.data return None def get_tx_type_from_id(self, id): """ Given an ID, returns the msg['TOPICS']['TX_TYPE'] that should be assigned :param id: destination item ID :return: """ subsystem_id = self.get_subsystem(id) return "BROADCAST" if subsystem_id == BROADCAST_ID else "DIRECT" @staticmethod def get_name_from_id(item_id, map, id_to_find, default_val="No name found"): """ Gets name from item ID. Assuming name is the KEY and ID is the value in <map> dictionary :param item_id: :param map: :param default_val: :return: """ item_name_map = map.get(item_id, None) if not item_name_map: return default_val for name in item_name_map: if item_name_map[name] == id_to_find: return name return default_val def _build_parlay_command_message(self, msg): """ Adds fields to Parlay message <msg> in accordance with command spec (3/15/2017). Command messages have: 1) TOPICS -> MSG_TYPE -> COMMAND 2) CONTENTS -> COMMAND -> COMMAND ID 3) CONTENTS -> COMMAND NAME (optional) :param msg: Parlay message to be modified :return: None """ destination_integer_id = self.to msg['TOPICS']['MSG_TYPE'] = "COMMAND" msg['CONTENTS']['COMMAND'] = self.response_code msg['CONTENTS']['COMMAND_NAME'] = self.get_name_from_id( destination_integer_id, pcom_serial.PCOM_COMMAND_NAME_MAP, self.response_code) def _build_parlay_property_get_msg(self, msg): """ Adds fields to Parlay message <msg> in accordance with property GET spec (3/15/2017) Property GET messages have: 1) TOPICS -> MSG_TYPE -> PROPERTY 2) CONTENTS -> PROPERTY -> PROPERTY ID 3) CONTENTS -> ACTION -> GET :param msg: Parlay message that will be modified :return: None """ msg['TOPICS']['MSG_TYPE'] = "PROPERTY" msg['CONTENTS']['PROPERTY'] = self.response_code msg['CONTENTS']['ACTION'] = "GET" def _build_parlay_property_set_msg(self, msg): """ Adds fields to Parlay message <msg> in accordance with property SET spec (3/15/2017) Property SET messages have: 1) TOPICS -> MSG_TYPE -> PROPERTY 2) CONTENTS -> PROPERTY -> PROPERTY ID 3) CONTENTS -> ACTION -> SET 4) CONTENTS -> VALUE -> PROPERTY VALUE :param msg: Parlay message (dictionary) that will be modified. :return: None """ msg['TOPICS']['MSG_TYPE'] = "PROPERTY" msg['CONTENTS']['PROPERTY'] = self.response_code msg['CONTENTS']['ACTION'] = "SET" msg['CONTENTS']['VALUE'] = self._get_data() def _build_parlay_error_response_msg(self, msg): """ Adds fields to Parlay message <msg> in accordance with Parlay error response spec. (3/15/2017) Parlay error responses have: 1) CONTENTS -> ERROR_CODE -> ERROR STATUS # (from embedded board) 2) CONTENTS -> DESCRIPTION -> ERROR DESCRIPTION 3) TOPICS -> MSG_STATUS -> ERROR 4) TOPICS -> RESPONSE_REQ -> False :param msg: Parlay message (dictionary) that will be modified. :return: None """ def _get_id_name_from_error_code(error_code): if error_code >> pcom_serial.PCOMSerial.SUBSYSTEM_SHIFT == self.GLOBAL_ERROR_CODE_ID: return self.from_ return (self.from_ & pcom_serial.PCOMSerial.SUBSYSTEM_ID_MASK) | ( error_code >> pcom_serial.PCOMSerial.SUBSYSTEM_SHIFT) def _get_specific_code(error_code): if error_code >> pcom_serial.PCOMSerial.SUBSYSTEM_SHIFT != self.GLOBAL_ERROR_CODE_ID: return error_code & pcom_serial.PCOMSerial.ITEM_ID_MASK return error_code msg['TOPICS']['MSG_STATUS'] = "ERROR" msg['TOPICS']['RESPONSE_REQ'] = False msg['CONTENTS']['ERROR_CODE'] = self.msg_status msg['CONTENTS']['DESCRIPTION'] = pcom_serial.PCOM_ERROR_CODE_MAP.get( self.msg_status, self.description) msg['CONTENTS']['ERROR ORIGIN'] = pcom_serial.PCOM_ITEM_NAME_MAP.get( _get_id_name_from_error_code(self.msg_status), "REACTOR") msg['CONTENTS']['ITEM SPECIFIC ERROR CODE'] = _get_specific_code( self.msg_status) msg['CONTENTS']['INFO'] = self.data def _build_parlay_command_response(self, msg): """ Adds fields to Parlay message <msg> in accordance with Parlay command response spec. (3/15/2017) Parlay command responses have: 1) TOPICS -> MSG_STATUS -> PROGRESS/OK 2) CONTENTS -> DATA depending on result :param msg: Parlay message (dictionary) that will be modified. :return: None """ msg_option = self.option() item = pcom_serial.PCOM_COMMAND_MAP.get(self.from_, None) if item or self.response_code == 0: if msg_option == ResponseCommandOption.Complete: msg['TOPICS']['MSG_STATUS'] = "OK" elif msg_option == ResponseCommandOption.Inprogress: msg['TOPICS']['MSG_STATUS'] = "PROGRESS" else: msg["TOPICS"]["MSG_STATUS"] = "ERROR" msg["CONTENTS"][ "DESCRIPTION"] = "PCOM ERROR: Could not find item:", self.from_ return if self.response_code == 0: self._build_contents_map(["Subsystems"], msg["CONTENTS"]) return cmd = item.get(self.response_code, pcom_serial.PCOMSerial.build_command_info("", [], [])) self._build_contents_map(cmd["output params"], msg["CONTENTS"]) def _build_parlay_property_response(self, msg): """ Adds fields to Parlay message <msg> in accordance with Parlay property response spec. (3/15/2017) :param msg: Parlay message (dictionary) that will be modified. :return: None """ sender_integer_id = self.from_ msg_option = self.option() msg['TOPICS']['MSG_STATUS'] = "OK" if msg_option == ResponsePropertyOption.Get_Response: msg['CONTENTS']['ACTION'] = "RESPONSE" id = self.response_code msg['CONTENTS']['PROPERTY'] = self.response_code msg['CONTENTS']['VALUE'] = self._get_data() elif msg_option == ResponsePropertyOption.Set_Response: msg['CONTENTS']['ACTION'] = "RESPONSE" msg['CONTENTS']['PROPERTY'] = self.response_code pass # NOTE: set responses do not have a 'value' field elif msg_option == ResponsePropertyOption.Stream_Response: msg['TOPICS']['MSG_TYPE'] = "STREAM" id = self.response_code if type(id) == int: # convert to stream name ID id = self.get_name_from_id(sender_integer_id, pcom_serial.PCOM_STREAM_NAME_MAP, self.response_code, default_val=self.response_code) # TODO: remove stream ID in topics when other platforms conform to spec. msg['TOPICS']['STREAM'] = id msg['CONTENTS']['STREAM'] = id msg['CONTENTS']['VALUE'] = self._get_data() def _build_parlay_notification(self, msg): """ Adds fields to Parlay message <msg> in accordance with Parlay notification spec. (3/15/2017) :param msg: Parlay message (dictionary) that will be modified. :return: None """ msg['TOPICS']["MSG_TYPE"] = "EVENT" msg['CONTENTS']['EVENT'] = self.response_code msg['CONTENTS']['ERROR_CODE'] = self.msg_status msg['CONTENTS']["INFO"] = self.data msg['CONTENTS']['DESCRIPTION'] = pcom_serial.PCOM_ERROR_CODE_MAP.get( self.msg_status, self.description) msg['TOPICS']['RESPONSE_REQ'] = False def _build_broadcast(self, msg): """ Changes message to type BROADCAST. :param msg: Parlay message (dictionary) that will be modified. :return: None """ msg_option = self.option() if msg_option == BroadcastNotificationOptions.External: msg['TOPICS']['TX_TYPE'] = "BROADCAST" if "TO" in msg['TOPICS']: del msg['TOPICS']['TO'] else: raise Exception("Received internal broadcast message") def _add_notification_msg_status(self, msg): """ Adds status fields to Parlay message <msg> :param msg: Parlay message (dictionary) that will be modified. :return: None """ if self.msg_status == 0: msg['TOPICS']['MSG_STATUS'] = "INFO" elif self.msg_status > 0: msg['TOPICS']['MSG_STATUS'] = "ERROR" else: msg['TOPICS']['MSG_STATUS'] = "WARNING" def _build_parlay_stream_msg(self, msg, is_on): """ Adds fields to Parlay message <msg> for stream commands. NOTE: The stream ID is put in contents and topics because some Parlay implementations check for it in TOPICS and some check for it in CONTENTS. :param msg: Parlay dictionary message that will be modified. :param is_on: Whether the stream command is turning the stream on or off. :return: """ sender_integer_id = self.from_ msg["TOPICS"]["MSG_TYPE"] = "STREAM" if type(id) == int: # convert to stream name ID id = self.get_name_from_id(sender_integer_id, pcom_serial.PCOM_STREAM_NAME_MAP, self.response_code, default_val=self.response_code) msg["TOPICS"]["STREAM"] = id msg["CONTENTS"]["STREAM"] = id msg["CONTENTS"]["STOP"] = is_on # TODO: Find out from Frances what the data looks like for stream commands def to_json_msg(self): """ Converts from PCOMMessage type to Parlay JSON message. Returns message when translation is complete. :return: Parlay JSON message equivalent of this object """ # Initialize our potential JSON message msg = {'TOPICS': {}, 'CONTENTS': {}} msg['TOPICS']['TO'] = self._get_name_from_id(self.to) msg['TOPICS']['FROM'] = self._get_name_from_id(self.from_) msg['TOPICS']['FROM_NAME'] = pcom_serial.PCOM_ITEM_NAME_MAP.get( self.from_, "") msg['TOPICS']['MSG_ID'] = self.msg_id # Default the message transmission type to "DIRECt". # This may be changed later if the message is a broadcast notification. msg['TOPICS']['TX_TYPE'] = "DIRECT" # Retrieve our message components msg_category = self.category() msg_sub_type = self.sub_type() msg_option = self.option() msg['TOPICS']['RESPONSE_REQ'] = self._is_response_req() # Handle Parlay command messages if msg_category == MessageCategory.Order: if msg_sub_type == OrderSubType.Command: if msg_option == OrderCommandOption.Normal: self._build_parlay_command_message(msg) elif msg_sub_type == OrderSubType.Property: if msg_option == OrderPropertyOption.Get_Property: self._build_parlay_property_get_msg(msg) elif msg_option == OrderPropertyOption.Set_Property: self._build_parlay_property_set_msg(msg) elif msg_option == OrderPropertyOption.Stream_On: self._build_parlay_stream_msg(msg, is_on=True) elif msg_option == OrderPropertyOption.Stream_Off: self._build_parlay_stream_msg(msg, is_on=False) else: raise Exception("Unhandled message subtype {}", msg_sub_type) # Handle Parlay responses elif msg_category == MessageCategory.Order_Response: msg['TOPICS']['MSG_TYPE'] = "RESPONSE" if self.msg_status != STATUS_SUCCESS: self._build_parlay_error_response_msg(msg) return msg if msg_sub_type == ResponseSubType.Command: self._build_parlay_command_response(msg) elif msg_sub_type == ResponseSubType.Property: self._build_parlay_property_response(msg) # Handle Parlay notifications elif msg_category == MessageCategory.Notification: self._build_parlay_notification(msg) if msg_sub_type == NotificationSubType.Broadcast: self._build_broadcast(msg) elif msg_option == NotificationSubType.Direct: msg['TOPICS']['TX_TYPE'] = "DIRECT" else: raise Exception("Unhandled notification type") self._add_notification_msg_status(msg) return msg def _build_contents_map(self, output_param_names, contents_map): """ Given the output names of a command and a data list, modifies <contents_map> accordingly. :param output_param_names: The output names of a command found during discovery. :param contents_map: The map that will be produced as part of a Parlay JSON message. :return: None """ # If we don't have any data we do not need add anything to the contents # dictionary. if len(self.data) == 0: return # If there is only one output parameter we need to add a "RESULT" key to the # contents dictionary and fill it with our data as per Parlay spec 3/15/2017. if len(output_param_names) == 1: contents_map["RESULT"] = self.data[0] if len( self.data) == 1 else self.data return # If the number of output parameters does not match the number of data pieces # we can not reliably add { output_name -> data } pairs to the CONTENTS dictionary # so we should report an error and not add anything to CONTENTS. if len(output_param_names) != len(self.data): logger.error( "[PCOM] ERROR: Could not produce contents dictionary for data: {0} and" " output parameters: {1}".format(self.data, output_param_names)) return # If we got here we know that the number of output parameters is the same # as the number of data pieces so we can match our output parameters to data pieces # 1:1. Simply zip them together and throw them in the CONTENTS dictionary. contents_map.update(dict(zip(output_param_names, self.data))) def category(self): return (self.msg_type & CATEGORY_MASK) >> CATEGORY_SHIFT def sub_type(self): return (self.msg_type & SUB_TYPE_MASK) >> SUB_TYPE_SHIFT def option(self): return (self.msg_type & OPTION_MASK) >> OPTION_SHIFT @property def data(self): return self._data @data.setter def data(self, value): if hasattr(value, '__iter__'): self._data = list(value) elif value is None: self._data = [] else: self._data = [value] @property def attributes(self): return self._attributes @attributes.setter def attributes(self, value): self._attributes = value if value is not None: self.priority = value & 0x01