Example #1
0
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()
Example #2
0
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
Example #3
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
        self.content_length = 0
Example #4
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