jarvis.Jarvis

Copyright (c) 2020 by Philipp Scheer. All Rights Reserved.

The Jarvis class handles MQTT application access and is part of the open-jarvis pip package

View Source
"""
Copyright (c) 2020 by Philipp Scheer. All Rights Reserved.

The Jarvis class handles MQTT application access and is part of the <a href="https://pypi.org/project/open-jarvis">open-jarvis pip package</a>
"""

import json
import time
import random
import string
from jarvis import MQTT

APPLICATION_TOKEN_LENGTH = 32
"""Length of an application token"""
ONE_TIME_CHANNEL_LENGTH = 64
"""Length of the one-time MQTT reply channel"""


class Jarvis:
    """
    Jarvis provides an MQTT API wrapper for the <a href="https://github.com/open-jarvis/server">Jarvis server</a>
    """

    _responses = {}

    def __init__(self, host: str = "127.0.0.1", port: int = 1883, client_id: str = "mqtt_jarvis") -> None:
        """
        Create a Jarvis API instance  
        * `host` specifies the ip or hostname of the MQTT broker (default 127.0.0.1)  
        * `port` specifies the port number of the MQTT broker (default 1883)  
        * `client_id` specifies the MQTT client identifier
        """
        self.host = host
        self.port = port
        self.mqtt = MQTT.MQTT(host, port, client_id=client_id)
        self.mqtt.on_message(Jarvis._on_msg)
        self.mqtt.subscribe("#")
        self.faster = False
        self.token = None
        """The application token"""

    # TODO: how to verify the application?
    # TODO: no protection yet
    def register(self, name: str):
        """
        Registers an application with the Jarvis backend  
        * `name` specifies the application name  
        A random token will be generated and returned
        """
        self.token = "app:" + ''.join(random.choice(string.ascii_lowercase + string.digits)
                                      for _ in range(APPLICATION_TOKEN_LENGTH))
        return json.loads(self._send_and_receive("jarvis/api/register-device", {"name": name, "type": "app"}))

    def get_devices(self):
        """
        Returns a list of all registered devices and applications  
        """
        return json.loads(self._send_and_receive("jarvis/api/get-devices"))

    def get_property(self, property: str, target_token: str = None, or_else: any = None):
        """
        Return properties of target devices  
        * `property` tells the backend which property to search for  
        * `target_token` filters properties by token, can be None to return `property` from all devices
        """
        return json.loads(self._send_and_receive("jarvis/api/get-property", {"property": property,
                                                                             "target-token": target_token} if target_token is not None else {"property": property}))

    def set_property(self, property: str, value: object):
        """
        Set the property of the current application  
        * `property` to set  
        * `value` to set `property` to
        """
        return json.loads(self._send_and_receive("jarvis/api/set-property", {"property": property, "value": value}))

    def decision_ask(self, typ: str, title: str, infos: str, options: list):
        """
        Create a decision request  
        * `typ` specifies the type of the decision request (see <a href="https://open-jarvis.github.io/#id-call">the official docs</a> for more)  
        * `title` specifies a title or a general information  
        * `infos` sets a more detailled description string  
        * `options` is an array of objects with the following structure: ```[ {"text": "Accept call", "color": "red" | "#ff3f3f", "icon": "base64" | "material"}, ... ]```
        """
        return json.loads(self._send_and_receive("jarvis/api/decision/ask", {"type": typ, "title": title, "infos": infos, "options": options}))

    def decision_answer(self, id: str, option_index: int, description: str = None):
        """
        Answer a decision request  
        * `id` is the decision id to answer  
        * `option_index` is the index in the options array  
        * `description` is a short description why this decision was answered
        """
        return json.loads(self._send_and_receive("jarvis/api/decision/answer", {"id": id, "option": option_index} if description is None else {"id": id, "option": option_index, "description": description}))

    def decision_scan(self, target_token: str = None, typ: str = None):
        """
        Scan for decision requests  
        * `target_token` (optional) is a token of the target device or application to scan  
        * `typ` (optional) is a type to scan for (see <a href="https://open-jarvis.github.io/#id-call">the official docs</a> for more)
        """
        obj = {}
        if target_token is not None:
            obj["target-token"] = target_token
        if typ is not None:
            obj["type"] = typ
        return json.loads(self._send_and_receive("jarvis/api/decision/scan", obj))

    def decision_delete(self, id: str):
        """
        Delete an decision request
        * `id` specifies the decision id to delete
        """
        return json.loads(self._send_and_receive("jarvis/api/decision/delete", {"id": id}))

    def _send_and_receive(self, topic: str, message: object = {}):
        if self.token is None:
            raise AttributeError("self.token is None!")
        one_time_channel = "jarvis/reply/" + \
            ''.join(random.choice("0123456789abcdef")
                    for _ in range(ONE_TIME_CHANNEL_LENGTH))
        message["token"] = self.token
        if self.faster:
            self.mqtt.publish(topic, json.dumps(message))
            return "{}"
        else:
            message["reply-to"] = one_time_channel
            self.mqtt.publish(topic, json.dumps(message))
            while one_time_channel not in Jarvis._responses:
                time.sleep(0.1)
            response = Jarvis._responses[one_time_channel]
            del Jarvis._responses[one_time_channel]
            return response

    @staticmethod
    def _on_msg(client: object, userdata: object, message: object):
        topic = message.topic
        data = message.payload.decode()
        if topic.startswith("jarvis/reply/"):
            Jarvis._responses[topic] = data
#   APPLICATION_TOKEN_LENGTH = 32

Length of an application token

#   ONE_TIME_CHANNEL_LENGTH = 64

Length of the one-time MQTT reply channel

#   class Jarvis:
View Source
class Jarvis:
    """
    Jarvis provides an MQTT API wrapper for the <a href="https://github.com/open-jarvis/server">Jarvis server</a>
    """

    _responses = {}

    def __init__(self, host: str = "127.0.0.1", port: int = 1883, client_id: str = "mqtt_jarvis") -> None:
        """
        Create a Jarvis API instance  
        * `host` specifies the ip or hostname of the MQTT broker (default 127.0.0.1)  
        * `port` specifies the port number of the MQTT broker (default 1883)  
        * `client_id` specifies the MQTT client identifier
        """
        self.host = host
        self.port = port
        self.mqtt = MQTT.MQTT(host, port, client_id=client_id)
        self.mqtt.on_message(Jarvis._on_msg)
        self.mqtt.subscribe("#")
        self.faster = False
        self.token = None
        """The application token"""

    # TODO: how to verify the application?
    # TODO: no protection yet
    def register(self, name: str):
        """
        Registers an application with the Jarvis backend  
        * `name` specifies the application name  
        A random token will be generated and returned
        """
        self.token = "app:" + ''.join(random.choice(string.ascii_lowercase + string.digits)
                                      for _ in range(APPLICATION_TOKEN_LENGTH))
        return json.loads(self._send_and_receive("jarvis/api/register-device", {"name": name, "type": "app"}))

    def get_devices(self):
        """
        Returns a list of all registered devices and applications  
        """
        return json.loads(self._send_and_receive("jarvis/api/get-devices"))

    def get_property(self, property: str, target_token: str = None, or_else: any = None):
        """
        Return properties of target devices  
        * `property` tells the backend which property to search for  
        * `target_token` filters properties by token, can be None to return `property` from all devices
        """
        return json.loads(self._send_and_receive("jarvis/api/get-property", {"property": property,
                                                                             "target-token": target_token} if target_token is not None else {"property": property}))

    def set_property(self, property: str, value: object):
        """
        Set the property of the current application  
        * `property` to set  
        * `value` to set `property` to
        """
        return json.loads(self._send_and_receive("jarvis/api/set-property", {"property": property, "value": value}))

    def decision_ask(self, typ: str, title: str, infos: str, options: list):
        """
        Create a decision request  
        * `typ` specifies the type of the decision request (see <a href="https://open-jarvis.github.io/#id-call">the official docs</a> for more)  
        * `title` specifies a title or a general information  
        * `infos` sets a more detailled description string  
        * `options` is an array of objects with the following structure: ```[ {"text": "Accept call", "color": "red" | "#ff3f3f", "icon": "base64" | "material"}, ... ]```
        """
        return json.loads(self._send_and_receive("jarvis/api/decision/ask", {"type": typ, "title": title, "infos": infos, "options": options}))

    def decision_answer(self, id: str, option_index: int, description: str = None):
        """
        Answer a decision request  
        * `id` is the decision id to answer  
        * `option_index` is the index in the options array  
        * `description` is a short description why this decision was answered
        """
        return json.loads(self._send_and_receive("jarvis/api/decision/answer", {"id": id, "option": option_index} if description is None else {"id": id, "option": option_index, "description": description}))

    def decision_scan(self, target_token: str = None, typ: str = None):
        """
        Scan for decision requests  
        * `target_token` (optional) is a token of the target device or application to scan  
        * `typ` (optional) is a type to scan for (see <a href="https://open-jarvis.github.io/#id-call">the official docs</a> for more)
        """
        obj = {}
        if target_token is not None:
            obj["target-token"] = target_token
        if typ is not None:
            obj["type"] = typ
        return json.loads(self._send_and_receive("jarvis/api/decision/scan", obj))

    def decision_delete(self, id: str):
        """
        Delete an decision request
        * `id` specifies the decision id to delete
        """
        return json.loads(self._send_and_receive("jarvis/api/decision/delete", {"id": id}))

    def _send_and_receive(self, topic: str, message: object = {}):
        if self.token is None:
            raise AttributeError("self.token is None!")
        one_time_channel = "jarvis/reply/" + \
            ''.join(random.choice("0123456789abcdef")
                    for _ in range(ONE_TIME_CHANNEL_LENGTH))
        message["token"] = self.token
        if self.faster:
            self.mqtt.publish(topic, json.dumps(message))
            return "{}"
        else:
            message["reply-to"] = one_time_channel
            self.mqtt.publish(topic, json.dumps(message))
            while one_time_channel not in Jarvis._responses:
                time.sleep(0.1)
            response = Jarvis._responses[one_time_channel]
            del Jarvis._responses[one_time_channel]
            return response

    @staticmethod
    def _on_msg(client: object, userdata: object, message: object):
        topic = message.topic
        data = message.payload.decode()
        if topic.startswith("jarvis/reply/"):
            Jarvis._responses[topic] = data

Jarvis provides an MQTT API wrapper for the Jarvis server

#   Jarvis( self, host: str = '127.0.0.1', port: int = 1883, client_id: str = 'mqtt_jarvis' )
View Source
    def __init__(self, host: str = "127.0.0.1", port: int = 1883, client_id: str = "mqtt_jarvis") -> None:
        """
        Create a Jarvis API instance  
        * `host` specifies the ip or hostname of the MQTT broker (default 127.0.0.1)  
        * `port` specifies the port number of the MQTT broker (default 1883)  
        * `client_id` specifies the MQTT client identifier
        """
        self.host = host
        self.port = port
        self.mqtt = MQTT.MQTT(host, port, client_id=client_id)
        self.mqtt.on_message(Jarvis._on_msg)
        self.mqtt.subscribe("#")
        self.faster = False
        self.token = None
        """The application token"""

Create a Jarvis API instance

  • host specifies the ip or hostname of the MQTT broker (default 127.0.0.1)
  • port specifies the port number of the MQTT broker (default 1883)
  • client_id specifies the MQTT client identifier
#   token

The application token

#   def register(self, name: str):
View Source
    def register(self, name: str):
        """
        Registers an application with the Jarvis backend  
        * `name` specifies the application name  
        A random token will be generated and returned
        """
        self.token = "app:" + ''.join(random.choice(string.ascii_lowercase + string.digits)
                                      for _ in range(APPLICATION_TOKEN_LENGTH))
        return json.loads(self._send_and_receive("jarvis/api/register-device", {"name": name, "type": "app"}))

Registers an application with the Jarvis backend

  • name specifies the application name
    A random token will be generated and returned
#   def get_devices(self):
View Source
    def get_devices(self):
        """
        Returns a list of all registered devices and applications  
        """
        return json.loads(self._send_and_receive("jarvis/api/get-devices"))

Returns a list of all registered devices and applications

#   def get_property( self, property: str, target_token: str = None, or_else: <built-in function any> = None ):
View Source
    def get_property(self, property: str, target_token: str = None, or_else: any = None):
        """
        Return properties of target devices  
        * `property` tells the backend which property to search for  
        * `target_token` filters properties by token, can be None to return `property` from all devices
        """
        return json.loads(self._send_and_receive("jarvis/api/get-property", {"property": property,
                                                                             "target-token": target_token} if target_token is not None else {"property": property}))

Return properties of target devices

  • property tells the backend which property to search for
  • target_token filters properties by token, can be None to return property from all devices
#   def set_property(self, property: str, value: object):
View Source
    def set_property(self, property: str, value: object):
        """
        Set the property of the current application  
        * `property` to set  
        * `value` to set `property` to
        """
        return json.loads(self._send_and_receive("jarvis/api/set-property", {"property": property, "value": value}))

Set the property of the current application

  • property to set
  • value to set property to
#   def decision_ask(self, typ: str, title: str, infos: str, options: list):
View Source
    def decision_ask(self, typ: str, title: str, infos: str, options: list):
        """
        Create a decision request  
        * `typ` specifies the type of the decision request (see <a href="https://open-jarvis.github.io/#id-call">the official docs</a> for more)  
        * `title` specifies a title or a general information  
        * `infos` sets a more detailled description string  
        * `options` is an array of objects with the following structure: ```[ {"text": "Accept call", "color": "red" | "#ff3f3f", "icon": "base64" | "material"}, ... ]```
        """
        return json.loads(self._send_and_receive("jarvis/api/decision/ask", {"type": typ, "title": title, "infos": infos, "options": options}))

Create a decision request

  • typ specifies the type of the decision request (see jarvis.github.io/#id-call">the official docs for more)
  • title specifies a title or a general information
  • infos sets a more detailled description string
  • options is an array of objects with the following structure: [ {"text": "Accept call", "color": "red" | "#ff3f3f", "icon": "base64" | "material"}, ... ]
#   def decision_answer(self, id: str, option_index: int, description: str = None):
View Source
    def decision_answer(self, id: str, option_index: int, description: str = None):
        """
        Answer a decision request  
        * `id` is the decision id to answer  
        * `option_index` is the index in the options array  
        * `description` is a short description why this decision was answered
        """
        return json.loads(self._send_and_receive("jarvis/api/decision/answer", {"id": id, "option": option_index} if description is None else {"id": id, "option": option_index, "description": description}))

Answer a decision request

  • id is the decision id to answer
  • option_index is the index in the options array
  • description is a short description why this decision was answered
#   def decision_scan(self, target_token: str = None, typ: str = None):
View Source
    def decision_scan(self, target_token: str = None, typ: str = None):
        """
        Scan for decision requests  
        * `target_token` (optional) is a token of the target device or application to scan  
        * `typ` (optional) is a type to scan for (see <a href="https://open-jarvis.github.io/#id-call">the official docs</a> for more)
        """
        obj = {}
        if target_token is not None:
            obj["target-token"] = target_token
        if typ is not None:
            obj["type"] = typ
        return json.loads(self._send_and_receive("jarvis/api/decision/scan", obj))

Scan for decision requests

  • target_token (optional) is a token of the target device or application to scan
  • typ (optional) is a type to scan for (see jarvis.github.io/#id-call">the official docs for more)
#   def decision_delete(self, id: str):
View Source
    def decision_delete(self, id: str):
        """
        Delete an decision request
        * `id` specifies the decision id to delete
        """
        return json.loads(self._send_and_receive("jarvis/api/decision/delete", {"id": id}))

Delete an decision request

  • id specifies the decision id to delete