class BaseMqttReactor(MqttMessages): """ Base class for MQTT-based plugins. """ def __init__(self, host='localhost', port=1883, keepalive=60, base="microdrop"): self._host = host self._port = port self._keepalive = keepalive self.mqtt_client = mqtt.Client(client_id=self.client_id) self.mqtt_client.on_connect = self.on_connect self.mqtt_client.on_disconnect = self.on_disconnect self.mqtt_client.on_message = self.on_message self.should_exit = False self.router = PathRouter() self.subscriptions = [] self.base = base ########################################################################### # Attributes # ========== @property def host(self): return self._host @host.setter def host(self, value): self._host = value self._connect() @property def port(self): return self._port @port.setter def port(self, value): self._port = value self._connect() @property def keepalive(self): return self._keepalive @keepalive.setter def keepalive(self, value): self._keepalive = value self._connect() @property def plugin_path(self): """Get parent directory of class location""" return os.path.dirname( os.path.realpath(inspect.getfile(self.__class__))) @property def plugin_name(self): """Get plugin name via the basname of the plugin path """ return os.path.basename(self.plugin_path) @property def url_safe_plugin_name(self): """Make plugin name safe for mqtt and http requests""" return six.moves.urllib.parse.quote_plus(self.plugin_name) @property def client_id(self): """ ID used for mqtt client """ return (self.url_safe_plugin_name + ">>" + self.plugin_path + ">>" + datetime.datetime.now().isoformat().replace(">>", "")) def addGetRoute(self, route, handler): """Adds route along with corresponding subscription""" self.router.add_route(route, handler) # Replace characters between curly brackets with "+" wildcard self.subscriptions.append(re.sub(r"\{(.+?)\}", "+", route)) def sendMessage(self, topic, msg, retain=False, qos=0, dup=False): message = json.dumps(msg, cls=PandasJsonEncoder) self.mqtt_client.publish(topic, message, retain=retain, qos=qos) def subscribe(self): for subscription in self.subscriptions: self.mqtt_client.subscribe(subscription) ########################################################################### # Private methods # =============== def _connect(self): try: # Connect to MQTT broker. # TODO: Make connection parameters configurable. self.mqtt_client.connect(host=self.host, port=self.port, keepalive=self.keepalive) except socket.error: pass # logger.error('Error connecting to MQTT broker.') ########################################################################### # MQTT client handlers # ==================== def on_connect(self, client, userdata, flags, rc): self.addGetRoute("microdrop/" + self.url_safe_plugin_name + "/exit", self.exit) self.listen() self.subscribe() def on_disconnect(self, *args, **kwargs): # Startup Mqtt Loop after disconnected (unless should terminate) if self.should_exit: sys.exit() self._connect() self.mqtt_client.loop_forever() def on_message(self, client, userdata, msg): ''' Callback for when a ``PUBLISH`` message is received from the broker. ''' method, args = self.router.match(msg.topic) try: payload = json.loads(msg.payload, object_hook=pandas_object_hook) except ValueError: print("Message contains invalid json") print("topic: " + msg.topic) payload = None if method: method(payload, args) ########################################################################### # Control API # =========== def start(self): # Connect to MQTT broker. self._connect() # Start loop in background thread. signal.signal(signal.SIGINT, self.exit) self.mqtt_client.loop_forever() def exit(self, a=None, b=None): self.should_exit = True self.mqtt_client.disconnect() def stop(self): ''' Stop plugin thread. ''' # Stop client loop background thread (if running). self.mqtt_client.loop_stop()
class MicropedeClient(Topics): """ Python based client for Micropede Application Framework Used with the following broker: https://github.com/The-Brainery/SciCAD/blob/master/MoscaServer.js """ def __init__(self, app_name, host="localhost", port=None, name=None, version='0.0.0', loop=None): if (app_name is None): raise ("app_name is undefined") if (port is None): port = 1884 if (name is None): name = get_class_name(self) self.router = PathRouter() client_id = generate_client_id(name, app_name) self.__listen = _.noop self.app_name = app_name self.client_id = client_id self.name = name self.schema = {} self.subscriptions = [] self.host = host self.port = port self.version = version self.last_message = None self.loop = None self.safe = None self.client = None if (loop == None): # Create thread to run event loop def start_loop(x): x.loop = asyncio.new_event_loop() x.loop.call_soon_threadsafe(x.ready_event.set) x.loop.run_forever() # Initialize loop and pass reference to main thread class X(object): pass X.ready_event = threading.Event() t = Thread(target=start_loop, args=(X, )) t.start() X.ready_event.wait() self.loop = X.loop else: self.loop = loop self.safe = safe(self.loop) # Start client self.wait_for(self.connect_client(client_id, host, port)) def wait_for(self, f): if (isinstance(f, (asyncio.Future, types.CoroutineType))): asyncio.ensure_future(f, loop=self.loop) def wrap(self, func): return lambda *args, **kwargs: self.wait_for(func(*args, **kwargs)) @property def is_plugin(self): return not _.is_equal(self.listen, _.noop) @property def listen(self): return self.__listen @listen.setter def listen(self, val): self.__listen = val def exit(self, *args, **kwargs): pass def add_binding(self, channel, event, retain=False, qos=0, dup=False): return self.on( event, lambda d: self.send_message(channel, d, retain, qos, dup)) def get_schema(self, payload, name): LABEL = f'{self.app_name}::get_schema' return self.notify_sender(payload, self.schema, 'get-schema') def validate_schema(self, payload): return validate(payload, self.schema) def ping(self, payload, params): return self.notify_sender(payload, "pong", "ping") def add_subscription(self, channel, handler): path = channel_to_route_path(channel) sub = channel_to_subscription(channel) route_name = f'{uuid.uuid1()}-{uuid.uuid4()}' future = asyncio.Future(loop=self.loop) try: if self.client._state != mqtt_cs_connected: if (future.done() == False): future.set_exception( Exception( f'Failed to add subscription. ' + + 'Client is not connected {self.name}, {self.channel}' )) return future def add_sub(*args, **kwargs): self.client.on_unsubscribe = _.noop def on_sub(client, userdata, mid, granted_qos): self.client.on_subscribe = _.noop if (future.done() == False): future.set_result('done') self.client.on_subscribe = self.safe(on_sub) self.client.subscribe(sub) if sub in self.subscriptions: self.client.on_unsubscribe = self.safe(add_sub) self.client.unsubscribe(sub) else: self.subscriptions.append(sub) self.router.add_route(path, self.wrap(handler)) add_sub() except Exception as e: if (future.done() == False): future.set_exception(e) return future def remove_subscription(self, channel): sub = channel_to_subscription(channel) future = asyncio.Future(loop=self.loop) def on_unsub(client, userdata, mid): self.client.on_unsubscribe = _.noop _.pull(self.subscriptions, sub) if (future.done() == False): future.set_result('done') self.client.unsubscribe(sub) return future def _get_subscriptions(self, payload, name): LABEL = f'{self.app_name}::get_subscriptions' return self.notify_sender(payload, self.subscriptions, 'get-subscriptions') def notify_sender(self, payload, response, endpoint, status='success'): if (status != 'success'): response = _.flatten_deep(response) receiver = get_receiver(payload) self.send_message( f'{self.app_name}/{self.name}/notify/{receiver}/{endpoint}', wrap_data(None, { 'status': status, 'response': response }, self.name, self.version)) return response def connect_client(self, client_id, host, port, timeout=DEFAULT_TIMEOUT): self.client = mqtt.Client(client_id) future = asyncio.Future(loop=self.loop) def on_connect(client, userdata, flags, rc): if future.done(): return self.subscriptions = [] if self.is_plugin: def on_done_1(d): def on_done_2(d): if future.done(): return self.listen() self.default_sub_count = len(self.subscriptions) self.client.on_disconnect = self.safe(self.exit) future.set_result('done') f2 = self.on_trigger_msg("exit", self.safe(self.exit)) f2.add_done_callback(self.safe(on_done_2)) # TODO: Run futures sequentially self.on_trigger_msg("ping", self.safe(self.ping)) self.on_trigger_msg("update-version", self.safe(self.update_version)) f = self.on_trigger_msg("get-schema", self.safe(self.get_schema)) self.wait_for(self.set_state('schema', self.schema)) f1 = self.on_trigger_msg("get-subscriptions", self.safe(self._get_subscriptions)) f1.add_done_callback(self.safe(on_done_1)) else: self.listen() self.default_sub_count = 0 if (future.done() == False): future.set_result('done') self.client.on_connect = self.safe(on_connect) self.client.on_message = self.safe(self.on_message) self.client.connect(host=self.host, port=self.port) self.client.loop_start() def on_timeout(): if (future.done() == False): future.set_exception(Exception(f'timeout {timeout}ms')) set_timeout(self.safe(on_timeout), timeout) return future def disconnect_client(self, timeout=DEFAULT_TIMEOUT): future = asyncio.Future(loop=self.loop) self.subscriptions = [] self.router = PathRouter() def off(): if hasattr(self, '_on_off_events'): del self._on_off_events if (_.get(self, 'client._state') != mqtt_cs_connected): off() if (hasattr(self, 'client')): del self.client future.set_result('done') return future else: def on_disconnect(*args, **kwargs): if future.done(): return off() if (hasattr(self, 'client')): del self.client future.set_result('done') if hasattr(self, 'client'): self.client.on_disconnect = self.safe(on_disconnect) self.client.disconnect() set_timeout(self.safe(on_disconnect), timeout) else: if (future.done() == False): future.set_result('done') return future def on_message(self, client, userdata, msg): try: payload = json.loads(msg.payload) except ValueError: print("Message contains invalid json") print(f'topic: {msg.topic}') payload = None topic = msg.topic if (topic is None or topic is ''): return method, args = self.router.match(topic) if method: method(payload, args) def send_message(self, topic, msg={}, retain=False, qos=0, dup=False, timeout=DEFAULT_TIMEOUT): future = asyncio.Future(loop=self.loop) if (_.is_dict(msg) and _.get(msg, '__head__') is None): head = wrap_data(None, None, self.name, self.version)['__head__'] _.set_(msg, '__head__', head) message = json.dumps(msg) _mid = None def on_publish(client, userdata, mid): if (mid == _mid): if (future.done() == False): future.set_result('done') def on_timeout(): if (future.done() == False): future.set_exception(Exception(f'timeout {timeout}ms')) self.client.on_publish = self.safe(on_publish) (qos, _mid) = self.client.publish(topic, payload=message, qos=qos, retain=retain) set_timeout(self.safe(on_timeout), timeout) return future async def dangerously_set_state(self, key, value, plugin): """ Dangerously set the state of another plugin (skip validation) key: str value: any plugin: str """ plugin = plugin or self.name topic = f'{self.app_name}/{plugin}/state/{key}' await self.send_message(topic, value, True, 0, False) async def set_state(self, key, value): topic = f'{self.app_name}/{self.name}/state/{key}' await self.send_message(topic, value, True, 0, False) def update_version(self, payload, params): try: state = payload["state"] storage_version = payload["storageVersion"] plugin_version = payload["pluginVersion"] state = self._update_version(state, storage_version, plugin_version) return self.notify_sender(payload, state, "update-version") except Exception as e: stack = dump_stack(self.name, e) return self.notify_sender(payload, stack, "update-version", "failed") @staticmethod def _version(cls): # Override Me! return "0.0.0" @staticmethod def _update_version(cls, state, storageVersion, pluginVersion): # Override Me! return state
from io import BytesIO from functools import partial from urllib import parse from rit.core.web.wsgi import get_application from rit.core.web.urls import all_urls from wheezy.http.response import HTTP_STATUS from wheezy.http import HTTPResponse, HTTPRequest from wheezy.routing import PathRouter from rit.app.conf import settings application = get_application() router = PathRouter() for url in all_urls: router.add_route(*url) HTTP_STATUS_STRING_TO_CODE = dict(zip(HTTP_STATUS.values(), HTTP_STATUS.keys())) class FakePayload(object): """ A wrapper around BytesIO that restricts what can be read since data from the network can't be seeked and cannot be read outside of its content_type length. This makes sure that views can't do anything under the test client that wouldn't work in Real Life. """ def __init__(self, content=None): self.__content = BytesIO() self.__len = 0 self.read_started = False self.content_length = 0
from io import BytesIO from functools import partial from urllib import parse from rit.core.web.wsgi import get_application from rit.core.web.urls import all_urls from wheezy.http.response import HTTP_STATUS from wheezy.http import HTTPResponse, HTTPRequest from wheezy.routing import PathRouter from rit.app.conf import settings application = get_application() router = PathRouter() for url in all_urls: router.add_route(*url) HTTP_STATUS_STRING_TO_CODE = dict(zip(HTTP_STATUS.values(), HTTP_STATUS.keys())) class FakePayload(object): """ A wrapper around BytesIO that restricts what can be read since data from the network can't be seeked and cannot be read outside of its content_type length. This makes sure that views can't do anything under the test client that wouldn't work in Real Life. """ def __init__(self, content=None): self.__content = BytesIO() self.__len = 0 self.read_started = False