How to Write Bridge-Apps
Before reading this section, we suggest you have a look at the A Raspberry Pi App Example section. This section contains more details of how bridge-apps work and advice for writing more advanced apps. When you come to write an app yourself, it's usually a good idea to copy an existing app and modify it. See How to Write Bridge-Apps for advice on a good way of doing about this to make you app easily deployable.
Event-driven programming
Event-driven programming will not be familiar to some people. When we first lean programming, we generally write programs that start, do a job and then stop. Bridge-apps start when the bridge is first turned on and then effectively run forever, but they are only active when there is something to do. This should be borne in mind when reading the following paragraphs.
Overview
To create a bridge-app, start by sub-classing the CbApp class, the definition of which can be found in cbcommslib, as follows:
from cbcommslib import CbApp
from cbconfig import *
class App(CbApp):
def __init__(self, argv):
# Super-class init must be called
CbApp.__init__(self, argv)
if __name__ == '__main__':
App(sys.argv)
Within class App, you must provide the following three methods, which are each called when messages arrive:
onAdaptorServices | This is called when “service” messages (see below) arrive from adaptors. |
onAdaptorData | This is called when “data” messages arrive from adaptors. |
onConfigureMessage | This is called when configuration messages arrive from the bridge manager. onConcMessage. This is called when messages arrive from the concentrator. |
The definition of the messages can be found in The Bridge-App API.
The configuration message
In many cases, your app will not need to do anything with the configuration message. The main thing that the app gets from this message is a list of the names of the adaptors it is connected to , together with their "friendly names" (the names that they were given when the devices were installed), and the superclass already creates a dictionary with these in it, called self.friendlyLookup, which has the following format (with one entry for each connected adaptor):
{"id": "friendly_name"}
By convention, you should set the state of your app to "starting" on receiving this message, as shown in A Raspberry Pi App Example.
The adaptor service message
Some time after being receiving a configuration message, an app will receive a service message from each adaptor that it is connected to, with onAdaptorServices being called once for each configuration message. A service message from an adaptor contains the id of the adaptor. It's important to store this as that's how your app will know what data messages to expect from the adaptor and what to do with them. The service message contains details of the service that an adaptor provides, with the service being composed of a number of characteristics, as detailed in The Bridge-App API. Details of the characteristics that are supported can be found in Characteristics.
An app should respond to the service message by sending a service request message back to the adaptor. This is again detailed in The Bridge-App API. A device, and hence it adaptor, often supplies several characteristics as part of its service and an app should select which ones it is interested in. For example, PIR sensors quite often supply characteristics such as temperature and luminance as well. A security app is likely to be only interested in the PIR (a binary_sensor) and will respond requesting this, but not temperature and luminance. An energy management system may use all three of these and respond requesting them all.
To send a message to an adaptor, an app uses the sendMessage method:
self.sendMessage(messsage, id)
where: message is the message to be send and id is the id of the adaptor to send it to. Here's a trivial example of an onAdaptorServices method that does this:
def onAdaptorService(self, message):
for p in message["service"]:
if p["characteristic"] == "temperature":
self.temperatures.append(message["id"]
request = {"id": self.id,
"request": "service",
"service": [
{"characteristic": "temperature",
"interval": 600
}
]
}
self.sendMessage(req, message["id"])
Here, self.temperatures is a list of the ids of adaptors that the app will receive temperature from. The app replied to any adaptor that can provide temperature by saying that it wants to receive it every 10 minutes (600 seconds). The message from the adaptor also has an "interval" key, which says the minimum interval at which the adaptor can provide the characteristic. As a lot of devices are battery-powered, you are strongly advised to use the maximum interval that is possible for your application, in order to save energy.
How do I know where the characteristic has come from?
The astute reader will have noticed that we have not provided any way for the app to know where a characteristic has come from. Is the temperature from the kitchen, from outside or a thermometer inside the dog? The next major upgrade of the ContinuumBridge platform will provide a way for apps to say what they want. When a user connects a device on the portal, there will be an input say, for example "Temperature inside dog", so that they know that's what they need to connect to it. In the meantime, you can use a combination of the "type" of the service, which goes with each characteristic and friendly names (so the user will need to use the words "inside dog", for example, when they install the device).
The adaptor data message
Once you have subscribed to characteristics from an adaptor, as described above, you app will receive adaptor data messages with the characteristics you have requested. This is best shown in a simple example:
def onAdaptorData(self, message):
if message["characteristic"] == "temperature":
sample = {message["timestamp"]: message["data"])
self.temperatures[message["id"].append(sample)
Here, self.temperatures is an list of dictionaries or the form {timestamp: temperature}. All the method does is to append each new sample to this list.
Quite often, you may want a call of onAdaptorData to do other things as well. For example, you could compare the temperature to a desired temperature and send an on/off message to a heating controller (using the self.sendMessage method) or call a method that sends data to a cloud platform. Example apps that do these and more can be found in the ContinuumBridge GitHub area.
Twisted time
In cbridge, apps and adaptors (and everything else) communicate over sockets. ContinuumBridge makes use of an event-driven networking engine called Twisted to call methods in apps when data arrives, and you can also use Twisted to control when things happen in your app.
Twisted
Although we've given a link to the Twisted Matix website, above, we don't actually recommend that you read it unless you're really keen. Twisted is a very powerful engine and some parts of it can be quite difficult to understand. We don't actually use that much of it in cbridge, and this documentation contains enough information to write comprehensive apps.
The core of Twisted is the reactor. As the name implies, the reactor reacts to things, such as data arriving at a port. When data arrives from an adaptor, for example, Twisted converts it into a Python dictionary and calls either onAdaptorService or onAdaptorData, whichever is appropriate. A call to self.sendMessage causes the message to be sent via the appropriate socket.
There are two main circumstances where you will need to call Twisted methods directly yourself:
Scheduling something to happen at some time in the future
You may want to do some processing every minute, for example, based on samples that have arrived from devices and some other information. The first thing you need in your code is:
from twisted.internet import reactor
Then you can write things like:
reactor.callLater(60, self.checkSensors)
This will cause the method checkSensors to be called in 60 seconds time. You could, for example include this in onConfigureMessage and then again at the end of the checkSensors method.
Threading
One thing to remember about writing apps is that, generally, if your code is waiting for something to happen, then no other methods can run. You may, for example, want to send some information to a cloud platform using an http POST. If the platform you are posting to is slow to respond, data from adaptors won't be processed until the POST has completed. This might be a problem, for example, if someone has pressed a button to turn on a light and nothing happens for three seconds. It's good programming practice to put all such tasks in threads, which can run "at the same time" as other things (or at least appear to - in reality the operating system is time-slicing between them). Twisted threads are essentially the same as standard Python threads, but we strongly recommend you use them in apps because Twisted understands them. To use them you need to:
from twisted.internet import threads
and then you can do things like:
reactor.callInThread(self.postValues)
Thread-safe?
All the usual rules about using threads and ensuring your program is thread-safe apply. Just remember that all threads in your program have access to exactly the same data structures, so if you add data that's arrived from an adaptor to a list at the same time as your are sending that list from a thread the results are likely to be indeterminate. You could get around this by calling the thread with the number of samples to send. eg: reactor.callInThread(self.postValues, 5).
Any method that you call from a thread will be part of the thread. Sending of messages, using self.sendMessage, if something that Twisted's reactor must handle, and that is not running in your thread, so you must use this construct:
reactor.callFromThread(self.sendMessage, msg, a).
Calling Twisted methods from threads
If you don't use reactor.callFromThread, as described above, your program can do very strange things, like appear to work a lot of the time and occasionally go wrong. This is difficult to debug!
Updated less than a minute ago