In this tutorial we go over the steps needed to create a simple Elisa plugin to fetch and show weather reports.
We assume that you are familiar with Python and know a little about Twisted, the foundation framework used by Elisa.
The easiest way to get started hacking on Elisa is to create your own branch, work on it and then publish it so that users and developers can experiment with it. Elisa uses Launchpad and can be branched with Bazaar:
bzr branch http://bazaar.launchpad.net/~elisa-developers/elisa/0.5 weather_plugin
or alternatively, using the Launchpad name scheme:
bzr branch lp:~elisa-developers/elisa/0.5 weather_plugin
The above command(s) create a directory called weather_plugin/ containing a copy of the branch of Elisa called 0.5. The 0.5 branch is where all the cool things are happening, new developers are encouraged to base their work on it.
After branching you need to setup the environment so that you can run python code inside your newly created branch:
# go inside the branch
cd weather_plugin
export PYTHONPATH=$PWD/elisa-core:$PWD/elisa-plugins:$PYTHONPATH
python -c "import elisa; print elisa.__file__"
The output should point to a file in your newly created branch.
Creating a plugin requires writing a small amount of boilerplate code. As no one likes to write boilerplate code over and over, there are skeleton plugins that can be used as a starting point for writing new plugins. Here we base our work on top of the basic skeleton plugin:
cp -R elisa-plugins/elisa/plugins/skeleton/basic elisa-plugins/elisa/plugins/weather
Now that we've copied the skeleton we need to tweak the setup.py file. Open elisa/plugins/weather/setup.py, you should see this:
from setuptools import setup
from elisa.core.utils.dist import find_packages, TrialTest, Clean
raise 'Modify the file and then remove this line'
packages, package_dir = find_packages()
cmdclass = dict(test=TrialTest, clean=Clean)
setup(name='elisa-plugin-basic',
version='0.0.1',
description='My supercool new plugin',
license='GPL3',
author='Put your name here',
author_email='put your email address here',
namespace_packages=['elisa', 'elisa.plugins'],
packages=packages,
package_dir=package_dir,
cmdclass=cmdclass)
We modify it with some info about the plugin we're writing:
from setuptools import setup
from elisa.core.utils.dist import find_packages, TrialTest, Clean
packages, package_dir = find_packages()
cmdclass = dict(test=TrialTest, clean=Clean)
setup(name='elisa-plugin-weather',
version='0.0.1',
description='Weather plugin',
license='GPL3',
author='Elisa Developers',
author_email='elisa@lists.fluendo.com',
namespace_packages=['elisa', 'elisa.plugins'],
packages=packages,
package_dir=package_dir,
cmdclass=cmdclass)
As you can see, we changed name, description, author and author_email. You should always modify at least those fields. Note that we also removed the raise statement that was there to check that we modified the file before trying to execute it.
Now that we have a good setup.py we need to run:
# generate the directory
python elisa-plugins/elisa/plugins/weather/setup.py egg_info -e elisa-plugins/
to generate the egg-info directory for our plugin. The egg-info directory needs to be generated at the beginning and then regenerated every time setup.py changes.
To write a weather plugin we need to get the weather status from somewhere. In this plugin we are going to use METAR reports from weather.noaa.org. To parse the reports we use the pymetar library.
So let's create our first component, the METAR resource provider, that we put in elisa-plugins/elisa/plugins/weather/report_provider.py:
from elisa.core.components.resource_provider import ResourceProvider
from elisa.core.components.model import Model
from elisa.core.media_uri import MediaUri
from elisa.plugins.http_client.http_client import ElisaAdvancedHttpClient
from twisted.internet import defer
from twisted.web2 import responsecode
from twisted.web2.stream import BufferedStream
from pymetar import WeatherReport, ReportParser
class WeatherReportError(Exception):
pass
class WeatherReportModel(Model):
uri = None
report_uri = None
country = ''
city = ''
sky = ''
temperature = ''
image = ''
class WeatherReportProvider(ResourceProvider):
default_config = {'base_url':
'http://weather.noaa.gov/pub/data/observations/metar/decoded'}
config_doc = {'base_url': 'The URL where to get METAR data from'}
supported_uri = 'metar://.*'
def initialize(self):
self.server_uri = MediaUri(self.config['base_url'])
self.client = ElisaAdvancedHttpClient(host=self.server_uri.host,
port=self.server_uri.port or 80)
return defer.succeed(self)
def clean(self):
close_defer = self.client.close()
close_defer.addErrback(lambda x: None)
return close_defer
def get(self, uri, context_model=None):
model = WeatherReportModel()
model.uri = uri
station_file = model.uri.host + '.TXT'
model.report_uri = self.server_uri.join(station_file)
dfr = self.client.request(model.report_uri)
dfr.addCallback(self._request_callback, model)
return (model, dfr)
def _request_callback(self, response, model):
if response.code != responsecode.OK:
err = '%d error fetching report from %s' % (response.code,
model.report_uri)
return defer.fail(WeatherReportError(err))
dfr = BufferedStream(response.stream).readExactly()
dfr.addCallback(self._report_read_callback, model)
return dfr
def _report_read_callback(self, data, model):
station = model.uri.host
report = WeatherReport(station)
report.reporturl = str(model.report_uri)
report.fullreport = data
# parse the report
parser = ReportParser()
parser.ParseReport(report)
# populate the model
model.country = report.getStationCountry()
model.city = report.getStationCity()
model.sky = report.getSkyConditions()
model.temperature = report.getTemperatureCelsius()
model.humidity = report.getHumidity()
model.image = report.getPixmap()
return model
The first thing to notice here is the supported_uri variable, which denotes the family of URIs handled by the resource provider. The URI syntax that we use is metar://ICAO_STATION_CODE (see wikipedia for more info about ICAO codes and for the list of station ids). For example the URI to retrieve the weather status of Barcelona, Spain would be metar://LEBL, where LEBL is the ICAO station id of Barcelona.
As we want to just fetch data from weather.noaa.org, we only need to implement the get() method of the resource provider API. The method is quite simple. It uses the HTTP client API in Elisa to download weather reports, parse and put them into WeatherModel objects.
Now that we have our resource provider to retrieve weather models, the next step is to build a nice user interface to show them. Let's create a new file called elisa-plugins/elisa/plugins/weather/ui.py containing:
import pkg_resources
import pgm
from elisa.plugins.pigment.graph.image import Image
from elisa.plugins.pigment.widgets import Widget, Label, List
class WeatherWidget(Widget):
def __init__(self):
super(WeatherWidget, self).__init__()
y_offset = 0.3
x_margin = 0.05
y_margin = 0.01
self._city = Label()
self.add(self._city)
self._city.style.background_color = (0, 0, 0, 0)
self._city._text.weight = pgm.TEXT_WEIGHT_BOLD
self._city.width = 0.5
self._city.height = 0.04
self._city.x = x_margin
self._city.y = y_offset
y_offset += self._city.height + y_margin + 0.05
self._city.visible = True
self._sky = Label()
self.add(self._sky)
self._sky.style.background_color = (0, 0, 0, 0)
self._sky.width = 0.5
self._sky.height = 0.04
self._sky.x = x_margin
self._sky.y = y_offset
y_offset += self._sky.height + y_margin
self._sky.visible = True
self._temperature = Label()
self.add(self._temperature)
self._temperature.style.background_color = (0, 0, 0, 0)
self._temperature.width = 0.5
self._temperature.height = 0.04
self._temperature.x = x_margin
self._temperature.y = y_offset
y_offset += self._temperature.height + y_margin
self._temperature.visible = True
self._image = Image()
self.add(self._image)
self._image.bg_a = 0
self._image.height = 0.7
self._image.width = 0.4
self._image.x = 0.5
self._image.y = (1.0-self._image.height)/2.0
self._image.visible = True
self._image_file = None
def set_city(self, value):
self._city.text = value
def get_city(self):
return self._city.text
city = property(get_city, set_city)
def set_sky(self, value):
self._sky.text = value
def get_sky(self):
return self._sky.text
sky = property(get_sky, set_sky)
def set_temperature(self, value):
self._temperature.text = str(value) + " °C"
def get_temperature(self):
return self._temperature.text
temperature = property(get_temperature, set_temperature)
def set_image(self, value):
if value:
filename = pkg_resources.resource_filename('elisa.plugins.weather',
'data/%s.png' % value)
self._image.set_from_file(filename)
self._image_file = value
def get_image(self):
return self._image_file
image = property(get_image, set_image)
This is a really simple pigment widget to show weather status. It has an icon and three labels to print city, sky condition and temperature.
At this point we have reports and a widget to show them nicely. The only thing that's left is the code that creates the interface and periodically updates it with up to date reports. Let's put such code in elisa-plugins/elisa/plugins/weather/controller.py:
from elisa.core.components.controller import Controller
from elisa.core.components.model import Model
from elisa.extern.log.log import getFailureMessage
from elisa.core import common
from elisa.core.media_uri import MediaUri
from elisa.plugins.pigment.pigment_controller import PigmentController
from elisa.plugins.weather.report_widget import WeatherWidget
from elisa.plugins.weather.report_provider import WeatherReportModel
from twisted.internet import defer, task
from pgm.timing.implicit import *
class WeatherController(PigmentController):
default_config = {'interval': 60, 'station': 'LEBL'}
config_doc = {'interval': 'update interval',
'station': 'station id (defaults to Barcelona, Spain. Guess why.)'}
def initialize(self):
# create an empty model
self.model = WeatherReportModel()
# use a looping call to fill and update self.model at regular intervals
self.looping_call = task.LoopingCall(self._get_report)
return defer.succeed(self)
def clean(self):
if self.looping_call.running:
self.looping_call.stop()
def set_frontend(self, frontend):
# build the UI
widget = self.build_widget(self.model)
widget.visible = True
self.widget.add(widget)
self.animated = AnimatedObject(widget)
self.animated.setup_next_animations(repeat_count=INFINITE,
repeat_behavior=REVERSE,
duration=2000)
self.animated.y -= 0.1
# get the station id from the configuration file
station = self.config['station']
if station is not None:
interval = int(self.config['interval'])
self.debug('starting update at interval %s' % interval)
self.looping_call.start(interval)
def build_widget(self, model):
widget = WeatherWidget()
model.bind('city', widget, 'city')
model.bind('sky', widget, 'sky')
model.bind('temperature', widget, 'temperature')
model.bind('image', widget, 'image')
return widget
def _get_report(self):
# get a report via the ResourceManager
resource_manager = common.application.resource_manager
uri = MediaUri('metar://' + self.config['station'])
model, dfr = resource_manager.get(uri)
dfr.addCallback(self._get_report_callback)
dfr.addErrback(self._get_report_errback)
return dfr
def _get_report_callback(self, model):
# update self.model so that the changes will be reflected in the UI
for attribute in ('city', 'sky', 'temperature', 'image'):
setattr(self.model, attribute, str(getattr(model, attribute)))
return model
def _get_report_errback(self, failure):
self.warning('error updating weather status: %s' %
getFailureMessage(failure))
# ignore so that the looping call won't stop
return None
The controller object is pretty simple. In initialize() we create a LoopingCall to periodically fetch new reports. In set_frontend(), which is called when a controller object is loaded in Elisa, we create our widget and bind a weather model to it, so that when the model is updated the changes are automatically reflected in the widget.
It's time to see our code in action now. First we need to let Elisa know that we created a new controller. We do this by adding the controller_mappings keyword in elisa-plugins/elisa/plugins/weather/setup.py so that the setup() function looks like this:
setup(name='elisa-plugin-weather',
version='0.1',
description='Weather plugin',
license='GPL3',
author='Elisa Developers',
author_email='elisa@lists.fluendo.com',
keywords='',
namespace_packages=['elisa', 'elisa.plugins'],
packages=packages,
package_dir=package_dir,
package_data={'': ['*.png', '*.mo', '*.po']},
controller_mappings=[('/elisa/weather',
'elisa.plugins.weather.weather_controller:WeatherController')],
cmdclass=cmdclass)
and then we regenerate the egg-info directory:
python elisa-plugins/elisa/plugins/weather/setup.py egg_info -e elisa-plugins/
At this point everything is ready. We create an ad hoc elisa_weather.conf file to test our plugin:
[general]
resource_providers = ['weather.report_provider:WeatherReportProvider']
frontends = ['weather_frontend']
[weather_frontend]
frontend = 'pigment.pigment_frontend:PigmentFrontend'
controller_path = '/elisa/weather'
[weather.weather_controller:WeatherController]
station = 'LEBL'
and start Elisa with:
python elisa-core/elisa.py elisa_weather.conf
Now you know how's the weather in Barcelona.