def __init__(self, request, client_address, server): # self.logger = server.logger or logging.getLogger(__name__) self.http_tx_queue = server.http_tx_queue self.status_observer = server.status_observer self.mqtt_topics = Topics() self.debug_comms = False # if true communication details are logged self.http_api_test_mode = False # When on, does not send MQTT messages super(wbcHTTPRequestHandler, self).__init__(request, client_address, server)
def __init__( self, shared_state, tx_queue, rx_queue, settings, data_queue=None, event_queue=None, timeout=10, histfile_size=1000, exit_signal=None, logger=None, ): self._prompt_base = "cli" self._prompt_format = "{} | {} > " self._reply_greeting = "<< message from" self._bstr_as_hex = True self._pretty_prints = False self._minimal_prints = False self._silent_loop = False self._max_queue_size = 1000 self._max_data_queue_size = 1 # Data queues contains list messges self._file = None self._histfile = os.path.expanduser("~/.wm-shell-history") self._histfile_size = histfile_size self._tracking_loop_timeout = 1 self._tracking_loop_iterations = float("Inf") self._raise_errors = False self._skip_replies = True super().__init__() self.settings = settings self.intro = self.intro.format(**self.settings.to_dict()) self.prompt = self._prompt_format.format( datetime.datetime.now().strftime("%H:%M.%S"), self._prompt_base) self.request_queue = tx_queue self.response_queue = rx_queue self.data_queue = data_queue self.event_queue = event_queue self.wait_api_lock = Lock() self.mqtt_topics = Topics() self.exit_signal = exit_signal self.timeout = timeout self.logger = logger or logging.getLogger(__name__) self.device_manager = shared_state["devices"] self._shared_state = shared_state self._flush_lock = threading.Lock() self.start_data_event_queue_peroidic_flush_timer()
def __init__(self, deviceManager: MeshManagement): self._processingActive: bool = True self._networkIdToUse: int = 0 self._diagnosticIntervalSecs: SetDiagnosticsIntervals = SetDiagnosticsIntervals.intervalOff self._deviceManager: MeshManagement = deviceManager self._mqttSendFn = None self._diagnosticActivationStatuses = dict() self.mqtt_topics = Topics() self._defaultPollTimeSec = 0.1 self._operationStart = None self._sinksAddressInfo = None self._messageTxList = list() self._defaultOperationTimeOutSec = 30
class wbcHTTPRequestHandler(http.server.SimpleHTTPRequestHandler): """A simple HTTP server class. Only overrides the do_GET from the HTTP server so it catches all the GET requests and processes them into commands. """ class HTTP_response_fields(Enum): path = "path" params = "params" gw_and_sinks = "gateways_and_sinks" command = "command" text = "text" code = "code" class HTTP_server_commands(Enum): data_tx = "datatx" start = "start" stop = "stop" set_config = "setconfig" get_info = "info" class HTTP_server_response_codes(Enum): http_response_ok = 200 http_response_code_unknown_command = 500 # pylint: disable=locally-disabled, too-many-arguments, broad-except, # unused-argument, invalid-name # pylint: disable=locally-disabled, too-many-statements, too-many-locals, # too-many-branches, too-many-nested-blocks def __init__(self, request, client_address, server): # self.logger = server.logger or logging.getLogger(__name__) self.http_tx_queue = server.http_tx_queue self.status_observer = server.status_observer self.mqtt_topics = Topics() self.debug_comms = False # if true communication details are logged self.http_api_test_mode = False # When on, does not send MQTT messages super(wbcHTTPRequestHandler, self).__init__( request, client_address, server ) def end_headers(self): self.send_my_headers() super().end_headers() def send_my_headers(self): self.send_header( "Cache-Control", "no-cache, no-store, must-revalidate" ) self.send_header("Pragma", "no-cache") self.send_header("Expires", "0") def _process_request(self, verb): """ Decodes an incoming http request regardless of its verb""" __default_command = "info" # Parse into commands and parameters slitted = urllib.parse.urlsplit(self.path) params = dict( urllib.parse.parse_qsl(urllib.parse.urlsplit(self.path).query) ) try: command = slitted.path.split("/")[1] except KeyError: command = __default_command if command == "": command = __default_command if self.debug_comms is True: self.logger.info( dict( protocol="http", verb=verb, path=self.path, params=str(params), command=command, gateways_and_sinks=str( self.status_observer.gateways_and_sinks ), ) ) self._mesh_control(command, params) # flake8: noqa def do_GET(self): """Process a single HTTP GET request. """ self._process_request("GET") def do_POST(self): """Process a single HTTP POST request. """ self._process_request("POST") def _mesh_control(self, command, params): """ Decodes an incoming payload and acts upon it """ # By default assume that gateway configuration does not need # refreshing after command is executed refresh = False response = dict() # Create HTTP response header response[self.HTTP_response_fields.path.value] = self.path response[self.HTTP_response_fields.params.value] = str(params) response[self.HTTP_response_fields.gw_and_sinks.value] = str( self.status_observer.gateways_and_sinks ) response[self.HTTP_response_fields.command.value] = command if len(command) > 0: self.logger.info("HTTP command '%s' received", command) response[self.HTTP_response_fields.text.value] = f"{command} ok!" response[ self.HTTP_response_fields.code.value ] = self.HTTP_server_response_codes.http_response_ok.value config_messages = list() messages = list() # Go through all gateways and sinks that are currently known gateways_and_sinks = self.status_observer.gateways_and_sinks for gateway_id, sinks in gateways_and_sinks.items(): # Sends the command towards all the discovered sinks for sink_id, sink in sinks.items(): command_was_ok = False if command == self.HTTP_server_commands.data_tx.value: # Handle transmit request. ( command_was_ok, new_messages, ) = self._handle_datatx_command( gateway_id, refresh, response, sink, sink_id, command, params, gateways_and_sinks, ) if command_was_ok is not True: break else: if len(new_messages) > 0: for msg in new_messages: messages.append(msg) elif command == self.HTTP_server_commands.start.value: ( command_was_ok, refresh, new_messages, ) = self._handle_start_command( gateway_id, refresh, sink_id ) if len(new_messages) > 0: for msg in new_messages: messages.append(msg) elif command == self.HTTP_server_commands.stop.value: ( command_was_ok, refresh, new_messages, ) = self._handle_stop_command( gateway_id, refresh, sink_id ) if len(new_messages) > 0: for msg in new_messages: messages.append(msg) elif command == self.HTTP_server_commands.set_config.value: ( command_was_ok, refresh, new_messages, ) = self._handle_setconfig_command( gateway_id, params, refresh, sink, sink_id ) if len(new_messages) > 0: for msg in new_messages: messages.append(msg) elif command == self.HTTP_server_commands.get_info.value: ( command_was_ok, refresh, new_messages, ) = self._handle_info_command( command, gateway_id, refresh, response, sink, sink_id, ) if len(new_messages) > 0: for msg in new_messages: messages.append(msg) else: self._handle_unknown_command(response) break # Renews information about remote gateways if command_was_ok is True: if refresh: refresh = False self._send_get_config_request_to_gateways( gateway_id, config_messages ) else: self.logger.error( "HTTP command parsing (%s) failed", command ) # sends all messages if self.http_api_test_mode is False: if len(messages) > 0: self.logger.info( "Send %d MQTT data messages", len(messages) ) self._send_messages_to_mqtt(messages) if len(config_messages) > 0: self.logger.info( "Send %d MQTT config messages", len(config_messages) ) self._send_messages_to_mqtt(config_messages) else: self.logger.error( "HTTP API test test mode. " "Not sending MQTT messages." ) else: self._handle_empty_request(response) if ( response[self.HTTP_response_fields.code.value] != self.HTTP_server_response_codes.http_response_ok.value ): self.logger.error(response) else: self.logger.info("HTTP command ok") # send code and response message self._send_http_response(response) if self.debug_comms is True: self.logger.info("HTTP response body: %s", response) def _send_http_response(self, response): self.send_response( code=response[self.HTTP_response_fields.code.value], message=response[self.HTTP_response_fields.text.value], ) self.end_headers() def _handle_empty_request(self, response): self.logger.error("HTTP request was empty") response[self.HTTP_response_fields.text.value] = "Error: empty request" response[ self.HTTP_response_fields.code.value ] = ( self.HTTP_server_response_codes.http_response_code_unknown_command.value ) def _send_get_config_request_to_gateways(self, gateway_id, messages): message = self.mqtt_topics.request_message( "get_configs", **dict(gw_id=gateway_id) ) messages.append(message) def _send_messages_to_mqtt(self, messages): for message in messages: if len(message) > 0: if self.debug_comms is True: self.logger.info({message["topic"]: str(message["data"])}) self.http_tx_queue.put(message) else: self.logger.error("MQTT message size is 0") def _handle_unknown_command(self, response): response[ self.HTTP_response_fields.code.value ] = ( self.HTTP_server_response_codes.http_response_code_unknown_command.value ) self.logger.error("HTTP request command was unknown") response[self.HTTP_response_fields.text.value] = "Unknown command" def _find_sink(self, sink_node_address: int, gateways: dict): sink_node_address_belongs_network = False for gateway_id, sinks in gateways.items(): # Sends the command towards all the discovered sinks for sink_id, sink in sinks.items(): if ( sink[App_config_keys.app_config_node_address_key.value] == sink_node_address ): sink_node_address_belongs_network = True break if sink_node_address_belongs_network is True: break return sink_node_address_belongs_network def _handle_datatx_command( self, gateway_id, refresh, response, sink, sink_id, command, params, gateways: dict, ): command_was_ok: bool = True command_parse_was_ok: bool = False newMessages = list() message = None try: # When sending message to certain gateway/sink on network we need destination_node_address = int(params["destination"]) src_ep = int(params["source_ep"]) dst_ep = int(params["dest_ep"]) # QOS passed by HTTP request (int(params["qos"])) is not used # from now on. MQTT QOS is fixed to # MQTT_QOS_options.exactly_once.value qos = MQTT_QOS_options.exactly_once.value payload = binascii.unhexlify(params["payload"]) command_parse_was_ok = True except KeyError as error: response[ self.HTTP_response_fields.code.value ] = ( self.HTTP_server_response_codes.http_response_code_unknown_command.value ) response[ self.HTTP_response_fields.text ] = f"Missing field: {error}" command_was_ok = False except Exception as error: response[ self.HTTP_response_fields.code.value ] = ( self.HTTP_server_response_codes.http_response_code_unknown_command.value ) response[ self.HTTP_response_fields.text ] = f"Unknown error: {error}" command_was_ok = False if command_parse_was_ok is True: try: is_unack_csma_ca = params["fast"] in ["true", "1", "yes", "y"] except KeyError: is_unack_csma_ca = False try: hop_limit = int(params["hoplimit"]) except KeyError: hop_limit = 0 try: count = int(params["count"]) except KeyError: count = 1 # Expected behavior: # (1) If destination_node_address is any of gateway sink addresses, # send only to desired sink. # (2) If destination_node_address is not any of gateway sink # addresses, then send this to all sinks of gateways belonging # to this network # Assumptions # (1) each sink node address is unique to network send_message_to_sink: bool = False if self._find_sink(destination_node_address, gateways): if ( sink[App_config_keys.app_config_node_address_key.value] == destination_node_address ): # send only addressed sink send_message_to_sink = True if self.debug_comms is True: self.logger.info("Node address is sink address") else: # send to all sinks on network send_message_to_sink = True if send_message_to_sink is True: # sends a or multiple messages according to the count # parameter in the request while count: if self.debug_comms is True: self.logger.info( "Create message to be sent via %s/%s to " "nodeaddress=%s dst ep=%s payload=%s", gateway_id, sink_id, destination_node_address, dst_ep, binascii.hexlify(payload), ) count -= 1 message = self.mqtt_topics.request_message( "send_data", **dict( sink_id=sink_id, gw_id=gateway_id, dest_add=destination_node_address, src_ep=src_ep, dst_ep=dst_ep, qos=qos, payload=payload, is_unack_csma_ca=is_unack_csma_ca, hop_limit=hop_limit, ), ) newMessages.append(message) return command_was_ok, newMessages def _handle_info_command( self, command, gateway_id, refresh, response, sink, sink_id ): command_was_ok = True refresh = True newMessages = list() response[self.HTTP_response_fields.command.value] = command # Add rest of fields response["gateway"] = gateway_id response["sink"] = sink_id response["started"] = sink[ App_config_keys.app_config_started_key.value ] response["app_config_seq"] = str( sink[App_config_keys.app_config_seq_key.value] ) response["app_config_diag"] = str( sink[App_config_keys.app_config_diag_key.value] ) response["app_config_data"] = str( sink[App_config_keys.app_config_data_key.value] ) return command_was_ok, refresh, newMessages def _handle_setconfig_command( self, gateway_id, params, refresh, sink, sink_id ): command_was_ok = True refresh = True newMessages = list() try: seq = int(params["seq"]) except KeyError: if sink[App_config_keys.app_config_seq_key.value] == 254: seq = 1 else: seq = sink[App_config_keys.app_config_seq_key.value] + 1 try: diag = int(params["diag"]) except KeyError: diag = sink[App_config_keys.app_config_diag_key.value] try: data = bytes.fromhex(params["data"]) except KeyError: data = sink[App_config_keys.app_config_data_key.value] new_config = dict( app_config_diag=diag, app_config_data=data, app_config_seq=seq ) message = self.mqtt_topics.request_message( "set_config", **dict(sink_id=sink_id, gw_id=gateway_id, new_config=new_config), ) newMessages.append(message) return command_was_ok, refresh, newMessages def _handle_stop_command(self, gateway_id, refresh, sink_id): command_was_ok = True refresh = True newMessages = list() new_config = dict(started=False) message = self.mqtt_topics.request_message( "set_config", **dict(sink_id=sink_id, gw_id=gateway_id, new_config=new_config), ) newMessages.append(message) return command_was_ok, refresh, newMessages def _handle_start_command(self, gateway_id, refresh, sink_id): command_was_ok = True newMessages = list() new_config = dict(started=True) message = self.mqtt_topics.request_message( "set_config", **dict(sink_id=sink_id, gw_id=gateway_id, new_config=new_config), ) newMessages.append(message) refresh = True return command_was_ok, refresh, newMessages
def __init__( self, mqtt_settings, shared_state=None, data_queue=None, event_queue=None, gateway_id: str = "+", sink_id: str = "+", network_id: str = "+", source_endpoint: str = "+", destination_endpoint: str = "+", message_subscribe_handlers=None, publish_cb=None, network_parameters=None, **kwargs, ): try: tx_queue = kwargs["tx_queue"] except KeyError: tx_queue = None try: rx_queue = kwargs["rx_queue"] except KeyError: rx_queue = None try: logger = kwargs["logger"] except KeyError: logger = logging.getLogger(__name__) try: exit_signal = kwargs["exit_signal"] except KeyError: exit_signal = Signal(False) try: start_signal = kwargs["start_signal"] except KeyError: start_signal = Signal(True) try: allowed_endpoints = kwargs["allowed_endpoints"] except KeyError: allowed_endpoints = None # create subscription list for MQTT API self.mqtt_settings = mqtt_settings self.mqtt_topics = Topics() if network_parameters: self.network_parameters = network_parameters else: self.network_parameters = dict( gw_id=str(gateway_id), sink_id=str(sink_id), network_id=str(network_id), src_ep=str(source_endpoint), dst_ep=str(destination_endpoint), ) if message_subscribe_handlers: self.message_subscribe_handlers = message_subscribe_handlers else: self.message_subscribe_handlers = self.build_subscription() super(NetworkDiscovery, self).__init__( mqtt_settings=mqtt_settings, start_signal=start_signal, exit_signal=exit_signal, tx_queue=tx_queue, rx_queue=rx_queue, allowed_endpoints=allowed_endpoints, message_subscribe_handlers=self.message_subscribe_handlers, publish_cb=publish_cb, logger=logger, ) # This is to mimic the API style in terms of having a data, event # and request response path self.response_queue = self.tx_queue self.request_queue = self.rx_queue self.data_queue = data_queue self.event_queue = event_queue self.shared_state = shared_state self.device_manager = MeshManagement() self._debug_comms = False self._perioidicTimer = None # Set on notify where context is right self._timerRunning: bool = False # picklable self.data_event_flush_timer_interval_sec: float = 1.0 self._data_event_tx_queue = Queue()
class NetworkDiscovery(MQTTObserver): """ NetworkDiscovery Tracks the MQTT topics and generates an object representation of the devices present in a given network. It builds a map of gateways, sinks and devices. """ def __init__( self, mqtt_settings, shared_state=None, data_queue=None, event_queue=None, gateway_id: str = "+", sink_id: str = "+", network_id: str = "+", source_endpoint: str = "+", destination_endpoint: str = "+", message_subscribe_handlers=None, publish_cb=None, network_parameters=None, **kwargs, ): try: tx_queue = kwargs["tx_queue"] except KeyError: tx_queue = None try: rx_queue = kwargs["rx_queue"] except KeyError: rx_queue = None try: logger = kwargs["logger"] except KeyError: logger = logging.getLogger(__name__) try: exit_signal = kwargs["exit_signal"] except KeyError: exit_signal = Signal(False) try: start_signal = kwargs["start_signal"] except KeyError: start_signal = Signal(True) try: allowed_endpoints = kwargs["allowed_endpoints"] except KeyError: allowed_endpoints = None # create subscription list for MQTT API self.mqtt_settings = mqtt_settings self.mqtt_topics = Topics() if network_parameters: self.network_parameters = network_parameters else: self.network_parameters = dict( gw_id=str(gateway_id), sink_id=str(sink_id), network_id=str(network_id), src_ep=str(source_endpoint), dst_ep=str(destination_endpoint), ) if message_subscribe_handlers: self.message_subscribe_handlers = message_subscribe_handlers else: self.message_subscribe_handlers = self.build_subscription() super(NetworkDiscovery, self).__init__( mqtt_settings=mqtt_settings, start_signal=start_signal, exit_signal=exit_signal, tx_queue=tx_queue, rx_queue=rx_queue, allowed_endpoints=allowed_endpoints, message_subscribe_handlers=self.message_subscribe_handlers, publish_cb=publish_cb, logger=logger, ) # This is to mimic the API style in terms of having a data, event # and request response path self.response_queue = self.tx_queue self.request_queue = self.rx_queue self.data_queue = data_queue self.event_queue = event_queue self.shared_state = shared_state self.device_manager = MeshManagement() self._debug_comms = False self._perioidicTimer = None # Set on notify where context is right self._timerRunning: bool = False # picklable self.data_event_flush_timer_interval_sec: float = 1.0 self._data_event_tx_queue = Queue() def __data_event_perioid_flush_timeout(self): txList: list = [] while self._data_event_tx_queue.empty() is False: msg = self._data_event_tx_queue.get(True) txList.append(msg) self._data_event_tx_queue.task_done() if len(txList) > 0: self.data_queue.put(txList) self._perioidicTimer = Timer( self.data_event_flush_timer_interval_sec, self.__data_event_perioid_flush_timeout, ).start() def notify(self, message, path="response"): """ Puts the device on the queue""" if self.shared_state: self.shared_state["devices"] = self.device_manager if message: if "response" in path: self.response_queue.put(message) elif "data" in path and self.data_queue: # Data message rate is huge compared others. Handle it # different way # Put data to internal queue first. self._data_event_tx_queue.put(message) # Start on this call context. if self._timerRunning is False: self._timerRunning = True self._perioidicTimer = Timer( self.data_event_flush_timer_interval_sec, self.__data_event_perioid_flush_timeout, ).start() elif "event" in path and self.event_queue: self.event_queue.put(message) def build_subscription(self): """ Uses the network parameters to build a dictionary with topics as keys and callbacks as handlers. """ # track gateway events event_status = self.mqtt_topics.event("status", **self.network_parameters) event_received_data = self.mqtt_topics.event("received_data", **self.network_parameters) response_get_configs = self.mqtt_topics.response( "get_configs", **self.network_parameters) response_set_config = self.mqtt_topics.response( "set_config", **self.network_parameters) response_send_data = self.mqtt_topics.response( "send_data", **self.network_parameters) response_otap_status = self.mqtt_topics.response( "otap_status", **self.network_parameters) response_otap_load_scratchpad = self.mqtt_topics.response( "otap_load_scratchpad", **self.network_parameters) response_otap_process_scratchpad = self.mqtt_topics.response( "otap_process_scratchpad", **self.network_parameters) message_subscribe_handlers = { event_status: self.generate_gateway_status_event_cb(), event_received_data: self.generate_gateway_data_event_cb(), response_get_configs: self.generate_gateway_response_get_configs_cb(), response_set_config: self.generate_gateway_response_set_config_cb(), response_send_data: self.generate_gateway_data_response_cb(), response_otap_status: self.generate_gateway_otap_status_response_cb(), response_otap_load_scratchpad: self.generate_gateway_load_scratchpad_response_cb(), response_otap_process_scratchpad: self.generate_gateway_process_scratchpad_response_cb(), } return message_subscribe_handlers # Publishing def send_data(self, timeout: int, block: bool): """ Callback provided by the interface's cb generator Args: """ ret = super(NetworkDiscovery, self).send_data(timeout=timeout, block=block) if ret is not None: try: if self.shared_state: if self.shared_state["devices"] is not None: self.device_manager = self.shared_state["devices"] except KeyError: pass # Subscribing def generate_gateway_status_event_cb(self) -> callable: """ Returns a callback to handle a gateway status event """ @topic_message def on_gateway_status_event_cb(payload, topic: list): """ Decodes an incoming gateway status event """ message = self.mqtt_topics.constructor( "event", "status").from_payload(payload) gateway = self.device_manager.add(message.gw_id) gateway.state = message.state self.notify(message=message, path="event") return on_gateway_status_event_cb def generate_gateway_data_event_cb(self) -> callable: """ Returns a callback to handle a gateway data event """ @decode_topic_message def on_gateway_data_event_cb(data_message, topic: list): """ Decodes an incoming data event callback """ if self._debug_comms: self.logger.debug("data event: %s", data_message) self.device_manager.add_from_mqtt_topic( topic, data_message.source_address) self.notify(message=data_message, path="data") return on_gateway_data_event_cb def generate_gateway_response_get_configs_cb(self) -> callable: """ Returns a callback to handle a response with gateway configurations """ @topic_message def on_gateway_get_configs_cb(payload, topic: list): """ Decodes and incoming configuration response """ if self._debug_comms: self.logger.debug("configs response: %s", payload) message = self.mqtt_topics.constructor( "response", "get_configs").from_payload(payload) self.device_manager.add_from_mqtt_topic(topic) self.device_manager.update(message.gw_id, message.configs) self.notify(message, path="response") return on_gateway_get_configs_cb def generate_gateway_otap_status_response_cb(self) -> callable: """ Returns a callback to handle otap status responses """ @topic_message def on_gateway_otap_status_cb(payload, topic: list): """ Decodes an otap status response """ if self._debug_comms: self.logger.debug("otap status response: %s", payload) message = self.mqtt_topics.constructor( "response", "otap_status").from_payload(payload) self.notify(message, path="response") return on_gateway_otap_status_cb def generate_gateway_response_set_config_cb(self) -> callable: """ Returns a callback to handle responses to configuration set requests """ @topic_message def on_gateway_set_config_response_cb(payload, topic: list): """ Decodes a set config response """ if self._debug_comms: self.logger.debug("set config response: %s", payload) message = self.mqtt_topics.constructor( "response", "set_config").from_payload(payload) self.notify(message, path="response") return on_gateway_set_config_response_cb def generate_gateway_data_response_cb(self) -> callable: """ Returns a callback to handle data responses """ @topic_message def on_gateway_data_response_cb(payload, topic: list): """ Decodes a data response """ if self._debug_comms: self.logger.debug("send data response: %s", payload) self.device_manager.add_from_mqtt_topic(topic) message = self.mqtt_topics.constructor( "response", "send_data").from_payload(payload) self.notify(message, path="response") return on_gateway_data_response_cb def generate_gateway_load_scratchpad_response_cb(self) -> callable: """ Returns a callback to handle the loading of a scratchpad into the target sink """ @topic_message def on_gateway_load_scratchpad_response_cb(payload, topic: list): """ """ if self._debug_comms: self.logger.debug("load scratchpad response: %s", payload) message = self.mqtt_topics.constructor( "response", "otap_load_scratchpad").from_payload(payload) self.notify(message, path="response") return on_gateway_load_scratchpad_response_cb def generate_gateway_process_scratchpad_response_cb(self) -> callable: """ Returns a callback to handle a processed scratchpad response """ @topic_message def on_gateway_process_scratchpad_cb(payload, topic: list): """ """ if self._debug_comms: self.logger.debug("process scratchpad response: %s", payload) message = self.mqtt_topics.constructor( "response", "otap_process_scratchpad").from_payload(payload) self.notify(message, path="response") return on_gateway_process_scratchpad_cb