A Raspberry Pi App Example

A few things about bridge-apps

Before looking at some code, it's worth knowing a few things about the way bridge-apps work in general. As discussed in the Overview, each app is connected to the bridge manager, the concentrator and a number of device adaptors. The bridge manager takes configuration that is derived from which devices a user has connected to an app and connects the corresponding adaptors.

Bridge-apps (and indeed everything running on a bridge) use an event-driven programming paradigm. Anyone who has done web or graphics programming should be familiar with this, but in the context of a bridge-app, it means that every time a message arrives from anything that is connected to the app, a method will be called. The method can do any processing that is necessary as a result of the message. Bridge-app methods can send messages to anything connected to the bridge-app at any point. Most bridge-apps will do relatively little processing, so there is unlikely to be any issue with not being able to process incoming messages fast enough and running out of time. For apps that do a lot of processing there are ways around this that are not covered in this introduction. The thing to note is that you should write a bride-app in such a way that methods respond to an input, do their processing, send off any resulting messages and then return. In particular, do not be tempted to insert artificial delays, for example using Python's time.sleep().

Some apps will need to do things like schedule tasks to be run at some point in the future. This can be achieved by directly using the communications framework of the bridge-app, Twisted (https://twistedmatrix.com/). Twisted is an event-driven network programming framework. This is not exposed directly to bridge-app developers, but is available for more advanced apps. To see how it's used in the app superclass, look in /opt/cbridge/bridge/lib/cbcommslib.py.

Example walk-through

We will choose a very simple example to start with. A number of sensors and a switch will be connected to the app. The sensors can be any devices that gives an off/on binary output, for example a PIR detector or a button. We will give the complete code for the app and then look at it step by step. This code is copied directly from here:

https://github.com/ContinuumBridge/demo_switch_app
#!/usr/bin/env python
# demo_switch_app_a.py
"""
Copyright (c) 2014 ContinuumBridge Limited
"""

import sys
import json
from cbcommslib import CbApp
from cbconfig import *

class App(CbApp):
    def __init__(self, argv):
        self.state = "stopped"
        self.switchState = "off"
        self.gotSwitch = False
        self.sensorsID = [] 
        self.switchID = ""
        # Super-class init must be called
        CbApp.__init__(self, argv)

    def setState(self, action):
        self.state = action
        msg = {"id": self.id,
               "status": "state",
               "state": self.state}
        self.sendManagerMessage(msg)

    def sendServiceResponse(self, characteristic, device):
        r = {"id": self.id,
             "request": "service",
             "service": [
                          {"characteristic": characteristic,
                           "interval": 0
                          }
                        ]
            }
        self.sendMessage(r, device)

    def sendCommand(self, state):
        r = {"id": self.id,
             "request": "command",
             "data": state
            }
        self.sendMessage(r, self.switchID)

    def onAdaptorService(self, message):
        self.cbLog("debug", "onAdaptorService, message: " + str(json.dumps(message, indent=4)))
        controller = None
        switch = False
        for s in message["service"]:
            if s["characteristic"] == "buttons" \
                or s["characteristic"] == "number_buttons" \
                or s["characteristic"] == "binary_sensor":
                controller = s["characteristic"]
            elif s["characteristic"] == "switch":
                self.switchID = message["id"]
                switch = True
                self.gotSwitch = True
        if controller and not switch:
            self.sensorsID.append(message["id"])
            self.sendServiceResponse(controller, message["id"])
        self.setState("running")

    def onAdaptorData(self, message):
        self.cbLog("debug", "onAdaptorData, message: " + str(json.dumps(message, indent=4)))
        if message["id"] in self.sensorsID:
            if self.gotSwitch:
                if message["characteristic"] == "binary_sensor":
                    self.switchState = message["data"]
                else:
                    if self.switchState == "off":
                        self.switchState = "on"
                    else:
                        self.switchState = "off"
                self.sendCommand(self.switchState)
            else:
                self.cbLog("debug", "Trying to turn on/off before switch connected")

    def onConfigureMessage(self, config):
        self.setState("starting")

if __name__ == '__main__':
    App(sys.argv)

Looking at this step by step, firstly, the headers:

#!/usr/bin/env python
# demo_switch_app.py
"""
Copyright (c) 2014 ContinuumBridge Limited
"""

import sys
import json
from cbcommslib import CbApp
from cbconfig import *

The main thing to note about the imports are that CbApp must be imported from the cbcommslib module and everything must be imported from cbconfig. Details of these are described later in this document, but it's not necessary to know this for this example.

Now, looking at the class declaration and init method for the app:

class App(CbApp):
    def __init__(self, argv):
        self.state = "stopped"
        self.switchState = "off"
        self.gotSwitch = False
        self.sensorsID = [] 
        self.switchID = ""
        # Super-class init must be called
        CbApp.__init__(self, argv)

Firstly, note that all apps must subclass CbApp. The CbApp super-class takes care of all communication with the bridge manager, adaptors and concentrator. Messages between these entities actually takes place by passing JSON over sockets, but by the time the messages arrive at the user's app they are provided as Python dictionaries. Similarly, messages are sent by passing Python dictionaries to the appropriate CbApp methods. A few variables that will be used later in the app are declared before calling the init method of the CbApp superclass.

When messages arrive from the bridge manager, the concentrator or from adaptors, the CbApp superclass calls various method. These are:

onAdaptorServicesThis is called when “service” messages (see below) arrive from adaptors.
onAdaptorDataThis is called when “data” messages arrive from adaptors.
onConfigureMessageThis is called when configuration messages arrive from the bridge manager.
onConcMessage. This is called when messages arrive from the concentrator.

This simple app does not have any concentrator messages and there is not really any configuration to do, but a method is provided that is called when a configuration message arrives:

def onConfigureMessage(self, config):
        self.setState("starting")

This just calls the setState method which sets the app state to “starting” and sends a message to the bridge manager informing it of the change of state.

An adaptor advertises the service that it performs to apps. When the associated message arrives from an adaptor, the onAdaptorService method is called with the message. Here is an example of a message that would arrive from an adaptor that connects to a device that provides temperature, relative humidity and luminance measurements and is also a binary sensor:

{
    "name": “Multi-sensor”,
    "id": “DID117”,
    "content": "service",
    "status": "ok",
    "service": [{"characteristic": "binary_sensor", "interval": 0},
                {"characteristic": "temperature", "interval": 300},
                {"characteristic": "luminance", "interval": 300},
                {"characteristic": "humidity", "interval": 300}],
}

The adaptor name is the description provided by the writer of the adaptor. This should not be used in any app logic as the temperature may be provided by any temperature sensor that has been connected to the app. The device ID is important, as this will be used to send messages back to the adaptor. In this example, the adaptor can provide temperature, humidity and luminance measurements at a minimum interval of 300 seconds. The code for the onAdaptorService method, below, shows how the app parses this message, finds the binary_sensor parameter and then sends a message back to the adaptor requesting that the binary_sensor information be sent. The interval is specified as 0 because a message is sent every time the sensor changes state rather than at a particular interval. The reason the onAdaptorService code is structured in this precise way is because some switches also have a binary_sensor characteristic, which can be used to determine the state the switch is in. We don't want to use this as an input or we could have a feedback loop with the switch turning itself on and off all the time!

def sendServiceResponse(self, characteristic, device):
        r = {"id": self.id,
             "request": "service",
             "service": [
                          {"characteristic": characteristic,
                           "interval": 0
                          }
                        ]
            }
        self.sendMessage(r, device

    def onAdaptorService(self, message):
        self.cbLog("debug", "onAdaptorService, message: " + str(json.dumps(message, indent=4)))
        controller = None
        switch = False
        for s in message["service"]:
            if s["characteristic"] == "buttons" \
                or s["characteristic"] == "number_buttons" \
                or s["characteristic"] == "binary_sensor":
                controller = s["characteristic"]
            elif s["characteristic"] == "switch":
                self.switchID = message["id"]
                switch = True
                self.gotSwitch = True
        if controller and not switch:
            self.sensorsID.append(message["id"])
            self.sendServiceResponse(controller, message["id"])
        self.setState("running")

📘

What's does the self.cbLog statement do?

It sends a message to the bridge manager that causes a message to be written to the log file. There's more about this later on in this example.

The message sent back to the adaptor, corresponding to the message received above, will therefore be:

{
    "id": "AID10",
    "request": "service",
    "service": [
        {"characteristic": "binary_sensor",
         "interval": 0
        }
    ]
}

The app ID is provided via the CbApp super-class. It is unique to this instance of this app and is included in all messages sent by the app.

This app can actually be connected to any number of binary sensor sources, the identities of which are stored in the self.sensorsID list, but only one switch.

The final thing to note about the onAdaptorService method is that the ID of the switch was stored in the self.switchID variable, so that the app knows where to send messages to turn the switch off and on.

Having said what it wants from the sensor adaptor, the app just sits back and waits for data to arrive. When it arrives, the onAdaptorData method is called:

def onAdaptorData(self, message):
        self.cbLog("debug", "onAdaptorData, message: " + str(json.dumps(message, indent=4)))
        if message["id"] in self.sensorsID:
            if self.gotSwitch:
                if message["characteristic"] == "binary_sensor":
                    self.switchState = message["data"]
                else:
                    if self.switchState == "off":
                        self.switchState = "on"
                    else:
                        self.switchState = "off"
                self.sendCommand(self.switchState)
            else:
                self.cbLog("debug", "Trying to turn on/off before switch connected")

    def onConfigureMessage(self, config):
        self.setState("starting")

It can be seen that the app looks at the source of the message. If it's from a sensor in the self.sensorsID list either sets the switch state to be the same as the input (in the case of a binary sensor) or toggles the switch state. The “if self.gotSwitch” and setting of self.gotSwitch allows for the fact that data from an an adaptor could arrive before the switch has been initialised (something that was found in debugging this example). There is more about the use of logging later in this document. For completeness, here is an example of a message that may have been received from the adaptor:

{
    "name": "Multi-sensor",
    "id": "DID117",
    "content": "characteristic",
    "characteristic": "biinary_sensor",
    "data": "on"
}

And here is the corresponding message that would be sent to the switch:

{
    "id": "AID10",
    "request": "command",
    "data": "on"
}

Running the example

Overview

This section explains how to run the app in development mode. Once the app is deployed the process is completely automatic.

To run the example in the real world you will need a binary sensor and a switch that have ContinuumBridge adaptors. To run the example you will need a device that outputs buttons, number_buttons or binary_sensor characteristics (see the list of devices), in addition to a supported switch. We used a Z-wave mains switch.

Accessing the source code

The source code to the Demo Switch App example and many other apps is on Github (https://github.com/ContinuumBridge/demo_switch_app). To access it, create a directory in the bridge home directory of your Raspberry Pi called apps_dev and clone the code, as follows:

cd ~
mkdir apps_dev
cd apps_dev
git clone https://github.com/ContinuumBridge/demo_switch_app.git

The source code for the app will now be in the directory ~/apps_dev/demo_switch_app, in the file demo_switch_app_a.py. (There is also a file called demo_switch_app.py. This is to force Python to create a .pyc file after the app has been run for the first time, which makes subsequent start-up of the app slightly more efficient).

For cbridge to use code in the apps_dev directory in preference to production code that it may have downloaded, you need to add the following line to /opt/cbridge/thisbridge/thisbridge.sh:

export CB_DEV_BRIDGE='True'
export CB_DEV_APPS="Demo Switch App"
export CB_USERNAME='bridge'

The first line tells cbridge that this Raspberry Pi is being used as a development bridge, and the second tells it to use the local code and not download code. The third line tell cbridge your user name. In this case this is "bridge". If you are using the default Raspberry Pi user, CB_USERNAME will be "pi".

Also for development, it is useful to put the following line in /opt/cbridge/thisbridge/thisbridge.sh:

export CB_LOG_ENVIRONMENT='DEBUG'

Valid values for the log options, apart from “debug”, are “info”, “warning” and “error”. These switch on various levels of logging, some of which can be seen in the Demo Switch App example.

You are now ready to connect the app to some devices, which is described in the ContinuumBridge Portal section.

Other examples

Other example apps can be found by browsing https://github.com/ContinuumBridge. Generally, repositories that contain apps are identified by the word “app” in their name.

Now that you've seen this example, you can: