XMPP/Jabber for IoT Devices - Part 2

From the previous post we discussed nuts and bolts of the XMPP(or Jabber) and how we can make use of it in the context of Internet-of-Things(IoT). This blog post will demonstrate a sample implementation on XMPP using unofficial official IoT XEPs(XMPP Extensions). These XMPP extensions are still in "Experimental" state at the time of writing this blog post.

Today we will be using XEP0030(service discovery), XEP0323(IoT sensor data) and XEP325(IoT control) for this sample. We are using SleekXMPP python library as XMPP client which implements these XEPs.

STEP 1:
First we need to setup a XMPP server. If you haven't setup any XMPP server; please follow this blog post which will describe "How to Setting up XMPP Openfire Server and expose via RESTful APIs". Please note that you can use a XMPP Server from any XMPP vendor. Here we use Openfire since it shipped with Apache Open Source license. Once you have setup Openfire server you need to create two user accounts "bob" and "alice". Our user story is that "alice" has a sensor device called "firealarm" and "bob" needs to get sensor information (e.g. temperature) of the "firealarm":D. Please refer the following image.
How Bob can get sensor information from the Alice's Firealarm

STEP 2:
Copy paste below python scripts into two files "xmpp_server.py" and "xmpp_client.py". Or you can directly download both scripts from this zip archive.

STEP 3:
We need to install SleekXMPP library. Enter following to install SleekXMPP from PyPI (Please visit here to get more install options).
pip install sleekxmpp

STEP 4:
Once Openfire server is up and running you can issue following commands;
python xmpp_server.py -j "alice@127.0.0.1"  -p "password" -n "firealarm"
In another terminal;
python xmpp_client.py -j "bob@127.0.0.1"  -p "password" -c "alice@127.0.0.1/firealarm" -q


IoT Server Code(xmpp_server.py)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import sys
from optparse import OptionParser
import socket

import sleekxmpp

# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
    from sleekxmpp.util.misc_ops import setdefaultencoding

    setdefaultencoding('utf8')
else:
    raw_input = input

from sleekxmpp.plugins.xep_0323.device import Device

class IoT_Server(sleekxmpp.ClientXMPP):
    """
    A simple IoT device that can act as server.

    This script can act as a "server" an IoT device that can provide sensor information.

    Setup the command line arguments.

    python xmpp_client.py -j "alice@yourdomain.com" -p "password" -n "device1" {--[debug|quiet]}
    python xmpp_client.py -j "alice@127.0.0.1" -p "password" -n "device1" {--[debug|quiet]}
    """
    def __init__(self, jid, password):
        sleekxmpp.ClientXMPP.__init__(self, jid, password)
        self.add_event_handler("session_start", self.session_start)
        self.add_event_handler("message", self.message)
        self.device = None
        self.releaseMe = False

    def testForRelease(self):
        # todo thread safe
        return self.releaseMe

    def doReleaseMe(self):
        # todo thread safe
        self.releaseMe = True

    def addDevice(self, device):
        self.device = device

    def session_start(self, event):
        self.send_presence()
        self.get_roster()
        # tell your preffered friend that you are alive
        # self.send_message(mto='jocke@jabber.sust.se', mbody=self.boundjid.bare +' is now online use xep_323 stanza to talk to me')

    def message(self, msg):
        if msg['type'] in ('chat', 'normal'):
            logging.debug("got normal chat message" + str(msg))
            ip = socket.gethostbyname(socket.gethostname())
            msg.reply("Hi I am " + self.boundjid.full + " and I am on IP " + ip + " use xep_323 stanza to talk to me").send()
        else:
            logging.debug("got unknown message type %s", str(msg['type']))

class IoT_Device(Device):
    """
    This is the actual device object that you will use to get information from your real hardware
    You will be called in the refresh method when someone is requesting information from you
    """

    def __init__(self, nodeId):
        Device.__init__(self, nodeId)
        logging.debug("=========TheDevice.__init__ called==========")
        self.temperature = 25  # define default temperature value
        self.update_sensor_data()

    def refresh(self, fields):
        """
        the implementation of the refresh method
        """
        logging.debug("=========TheDevice.refresh called===========")
        self.temperature += 1  # increment default temperature value by one
        self.update_sensor_data()

    def update_sensor_data(self):
        logging.debug("=========TheDevice.update_sensor_data called===========")
        self._add_field(name="Temperature", typename="numeric", unit="C")
        self._set_momentary_timestamp(self._get_timestamp())
        self._add_field_momentary_data("Temperature", self.get_temperature(),
                                       flags={"automaticReadout": "true"})

    def get_temperature(self):
        return str(self.temperature)

if __name__ == '__main__':
    #-------------------------------------------------------------------------------------------
    #   Parsing Arguments
    #-------------------------------------------------------------------------------------------
    optp = OptionParser()

    # Output verbosity options.
    optp.add_option('-q', '--quiet', help='set logging to ERROR',
                    action='store_const', dest='loglevel',
                    const=logging.ERROR, default=logging.INFO)
    optp.add_option('-d', '--debug', help='set logging to DEBUG',
                    action='store_const', dest='loglevel',
                    const=logging.DEBUG, default=logging.INFO)

    # JID and password options.
    optp.add_option("-j", "--jid", dest="jid",
                    help="JID to use")
    optp.add_option("-p", "--password", dest="password",
                    help="password to use")

    # IoT device id
    optp.add_option("-n", "--nodeid", dest="nodeid",
                    help="I am a device get ready to be called", default=None)

    opts, args = optp.parse_args()

    # Setup logging.
    logging.basicConfig(level=opts.loglevel,
                        format='%(levelname)-8s %(message)s')

    if opts.jid is None or opts.password is None or opts.nodeid is None:
        optp.print_help()
        exit()

    #-------------------------------------------------------------------------------------------
    #   Starting XMPP with XEP0030, XEP0323, XEP0325
    #-------------------------------------------------------------------------------------------

    # we bounded resource into node_id; because client can always call using static jid
    # opts.jid + "/" + opts.nodeid
    xmpp = IoT_Server(opts.jid + "/" + opts.nodeid, opts.password)
    xmpp.register_plugin('xep_0030')
    xmpp.register_plugin('xep_0323')
    xmpp.register_plugin('xep_0325')

    if opts.nodeid:
        myDevice = IoT_Device(opts.nodeid)
        xmpp['xep_0323'].register_node(nodeId=opts.nodeid, device=myDevice, commTimeout=10)
        while not (xmpp.testForRelease()):
            xmpp.connect()
            xmpp.process(block=True)
            logging.debug("lost connection")

IoT Client Code(xmpp_client.py)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import logging
import sys
from optparse import OptionParser
import socket

import sleekxmpp
from sleekxmpp.exceptions import IqError, IqTimeout

# Python versions before 3.0 do not use UTF-8 encoding
# by default. To ensure that Unicode is handled properly
# throughout SleekXMPP, we will set the default encoding
# ourselves to UTF-8.
if sys.version_info < (3, 0):
    from sleekxmpp.util.misc_ops import setdefaultencoding

    setdefaultencoding('utf8')
else:
    raw_input = input

PRINT_HEADER_LENGTH = 40

class IoT_Client(sleekxmpp.ClientXMPP):
    """
    A simple IoT device that can act as client

    This script can act as a "client" an IoT device or other party that would like to get data from
    another device.

    Setup the command line arguments.

    python xmpp_client.py -j "bob@yourdomain.com" -p "password" -c "alice@yourdomain.com/device1" {--[debug|quiet]}
    python xmpp_client.py -j "bob@127.0.0.1" -p "password" -c "alice@127.0.0.1/device1" {--[debug|quiet]}
    """
    def __init__(self, jid, password, sensorjid):
        sleekxmpp.ClientXMPP.__init__(self, jid, password)
        self.add_event_handler("session_start", self.session_start)
        self.add_event_handler("message", self.message)
        self.device = None
        self.releaseMe = False
        self.target_jid = sensorjid

    def datacallback(self, from_jid, result, nodeId=None, timestamp=None, fields=None,
                     error_msg=None):
        """
        This method will be called when you ask another IoT device for data with the xep_0323
        se script below for the registration of the callback
        """
        logging.debug("we got data %s from %s", str(result), from_jid)
        if (result == "fields"):
            header = 'XEP 302 Sensor Data'
            print('-' * PRINT_HEADER_LENGTH)
            gap = ' '* ((PRINT_HEADER_LENGTH - len(header)) / 2)
            print(gap + header)
            print('-' * PRINT_HEADER_LENGTH)

            logging.debug("RECV:"+str(fields))

            if len(fields) > 0:
                print "Name\t\tType\tValue\tUnit"
            for field in fields:
                print "  - " + field["name"] + "\t" + field["typename"] + "\t" + field["value"] + "\t" + field["unit"]
            print ""
            self.disconnect()

    def testForRelease(self):
        # todo thread safe
        return self.releaseMe

    def doReleaseMe(self):
        # todo thread safe
        self.releaseMe = True

    def addDevice(self, device):
        self.device = device

    def session_start(self, event):
        self.send_presence()
        self.get_roster()
        # tell your preffered friend that you are alive using generic xmpp chat protocol
        # self.send_message(mto='jocke@jabber.sust.se', mbody=self.boundjid.bare +' is now online use xep_323 stanza to talk to me')

        #-------------------------------------------------------------------------------------------
        #   Service Discovery
        #-------------------------------------------------------------------------------------------
        try:
            # By using block=True, the result stanza will be
            # returned. Execution will block until the reply is
            # received. Non-blocking options would be to listen
            # for the disco_info event, or passing a handler
            # function using the callback parameter.
            info = self['xep_0030'].get_info(jid=self.target_jid,
                                             node=None,
                                             block=True)
        except IqError as e:
            logging.error("Entity returned an error: %s" % e.iq['error']['condition'])
        except IqTimeout:
            logging.error("No response received.")
        else:
            header = 'XMPP Service Discovery'
            print('-' * PRINT_HEADER_LENGTH)
            gap = ' '* ((PRINT_HEADER_LENGTH - len(header)) / 2)
            print(gap + header)
            print('-' * PRINT_HEADER_LENGTH)

            print "Device: %s" % self.target_jid

            for feature in info['disco_info']['features']:
                print('  - %s' % feature)

        #-------------------------------------------------------------------------------------------
        #   Requesting data through XEP0323
        #-------------------------------------------------------------------------------------------
        session = self['xep_0323'].request_data(self.boundjid.full, self.target_jid,
                                                self.datacallback, flags={"momentary": "true"})

    def message(self, msg):
        if msg['type'] in ('chat', 'normal'):
            logging.debug("got normal chat message" + str(msg))
            ip = socket.gethostbyname(socket.gethostname())
            msg.reply("Hi I am " + self.boundjid.full + " and I am on IP " + ip + " use xep_323 stanza to talk to me").send()
        else:
            logging.debug("got unknown message type %s", str(msg['type']))

if __name__ == '__main__':
    #-------------------------------------------------------------------------------------------
    #   Parsing Arguments
    #-------------------------------------------------------------------------------------------
    optp = OptionParser()

    # JID and password options.
    optp.add_option("-j", "--jid", dest="jid",
                    help="JID to use")
    optp.add_option("-p", "--password", dest="password",
                    help="password to use")

    # IoT sensor jid
    optp.add_option("-c", "--sensorjid", dest="sensorjid",
                    help="Another device to call for data on", default=None)

    # Output verbosity options.
    optp.add_option('-q', '--quiet', help='set logging to ERROR',
                    action='store_const', dest='loglevel',
                    const=logging.ERROR, default=logging.INFO)
    optp.add_option('-d', '--debug', help='set logging to DEBUG',
                    action='store_const', dest='loglevel',
                    const=logging.DEBUG, default=logging.INFO)

    opts, args = optp.parse_args()

    # Setup logging.
    logging.basicConfig(level=opts.loglevel,
                        format='%(levelname)-8s %(message)s')

    if opts.jid is None or opts.password is None or opts.sensorjid is None:
        optp.print_help()
        exit()

    #-------------------------------------------------------------------------------------------
    #   Starting XMPP with XEP0030, XEP0323, XEP0325
    #-------------------------------------------------------------------------------------------
    xmpp = IoT_Client(opts.jid, opts.password, opts.sensorjid)
    xmpp.register_plugin('xep_0030')
    xmpp.register_plugin('xep_0323')
    xmpp.register_plugin('xep_0325')

    if opts.sensorjid:
        logging.debug("will try to call another device for data")
        xmpp.connect()
        xmpp.process(block=True)
        logging.debug("ready ending")

    else:
        print "noopp didn't happen"
SHARE

Anonymous

  • Image
  • Image
  • Image
  • Image
  • Image
    Blogger Comment
    Facebook Comment