Source Files

Most of the code for the application is included in a single main.py file which contains a single Application class to manage the running of the application and a main function to start it.

Much of the code is very similar to other examples and tutorials. We will focus on the parts that are specific to this example.

Relevant Modules

In addition to the usual modules for accessing command line arguments and GTK widgets, we use the Gio module to enable us to access D-Bus services.

import sys
import gi

gi.require_version('Gtk', '3.0')
from gi.repository import Gio, GLib, Gtk

The Gio and GLib modules are imported in the same way as the Gtk module.

Setting up the Application

The application provides an Application class with the usual methods to set up the application and perform tasks when it is run. In the __init__ method we set up the application title, ID and program name to ensure that it integrates into the environment:

class Application(Gtk.Application):

    def __init__(self):
        super().__init__(application_id='com.example.ambient_light')
        GLib.set_application_name(_('Ambient Light'))
        GLib.set_prgname('com.example.ambient_light')

In the do_activate method, we create the application window and show it, but we also set up a DBusProxy object that we use to communicate with a D-Bus service providing sensor data:

    def do_activate(self):

        window = Window(application=self)
        window.set_default_size(320, 512)
        window.show_all()

        # Request a proxy for accessing the sensor service.
        self.proxy = Gio.DBusProxy.new_for_bus_sync(
            Gio.BusType.SYSTEM, Gio.DBusProxyFlags.NONE, None,
            'net.hadess.SensorProxy',
            '/net/hadess/SensorProxy',
            'net.hadess.SensorProxy',
            None)

This proxy object emits the g-properties-changed signal when any of the properties exposed by the service change. We connect this signal to the properties_changed method in the window so that it can update the user interface with the latest sensor information:

        self.proxy.connect('g-properties-changed',
                           window.properties_changed, None)

The last task the do_activate does is to check for the presence of the ambient light sensor by reading the HasAmbientLight property of the D-Bus service. If this returns True we call the ClaimLight method of the D-Bus API to start receiving updates to the ambient light sensor.

        if self.proxy.get_cached_property('HasAmbientLight'):
            self.proxy.call_sync('ClaimLight', None, Gio.DBusCallFlags.NONE,
                                 -1, None)

The do_shutdown method of the application is called just before the application exits. In this method we call the ReleaseLight method of the D-Bus API to release the sensor for other applications:

    def do_shutdown(self):
        if self.proxy.get_cached_property('HasAmbientLight'):
            self.proxy.call_sync('ReleaseLight', None, Gio.DBusCallFlags.NONE,
                                 -1, None)

Applications should usually only claim the sensor when required and release it as soon as possible.

Creating the User Interface

The Window class is used to display a simple user interface that updates when new sensor data is available. We begin by creating some Gtk.Label widgets to show the information, using Gtk.Box boxes to organize them:

class Window(Gtk.ApplicationWindow):

    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.indicator_label = Gtk.Label()
        self.level_label = Gtk.Label()
        self.level_label.set_markup(
            _('<span size="large">Waiting for changes</span>'))

        inner_box = Gtk.Box(orientation='vertical')
        inner_box.add(self.level_label)
        inner_box.add(self.indicator_label)

        box = Gtk.Box(orientation='vertical')
        box.set_center_widget(inner_box)
        self.add(box)

The indicator label will be used to display a simple bar graph; the level label will show the measured quantity provided by the D-Bus service.

Responding to Updates

As described above, the properties_changed method of the Window class will be called when the properties exposed by the D-Bus service change:

    def properties_changed(self, proxy, changed, invalidated, user_data):

        level = proxy.get_cached_property('LightLevel').get_double()
        unit = proxy.get_cached_property('LightLevelUnit').get_string()

        blocks = '▩' * int(min((level * 16)**0.5, 16))

        self.indicator_label.set_markup(
            _('<span size="large">' + blocks + '</span>'))
        self.level_label.set_markup(
            _('<span size="large">%1.1f %s</span>' % (level, unit)))

Since we are only interested in the ambient light level, we simply request the values of the LightLevel and LightLevelUnit properties, updating the two labels with the new information.

Another approach to handling updated sensor information would involve examining the values passed in the changed and invalidated method parameters, and only changing the user interface if the ambient light data has changed.

Summary

Handling sensor data involves requesting information from the net.hadess.SensorProxy service on the system D-Bus. Applications can create a DBusProxy object to communicate with the service. When this is done, you can subscribe to property updates by

  • connecting the g-properties-changed signal to a callback method,

  • checking the HasAmbientLight property for the presence of the sensor, and

  • calling the service’s ClaimLight method to start receiving updates.

When the sensor is no longer needed, call the ReleaseLight method of the D-Bus API.