App Development Tutorial 3

Contents

Tutorial 3: Controlling Devices

In the previous tutorial we learned how to obtain a reference to a device and perform some action when the device emits a signal. In this tutorial we will progress onto the reception of more complex signals and controlling another device based on the contents of the signals.

This tutorial will cover:

  1. Receiving signals which contain data
  2. Connecting signals which contain data to methods
  3. Interrogating the signal's data in the method
  4. Actuating a device
  5. Maintaining a reference to a device through the lifetime of the app

Background

In the last tutorial we worked with a motion sensor type device. Signals from that type of device do not contain any data, they just indicate to the connected methods that the sensor has just fired. In this tutorial we shall use a sensor which includes data in it's signals.

Signals Containing Data

If we look at the door sensor on the main entrance door in the home simulator, we can see that this device is named "livingRoomDoorSensor" and it emits a signal referred to as stateChanged(bool):

DoorSensorTooltipOpened.png

This indicates that this device's stateChanged signal conveys a single boolean argument to the connected method. Compared to what we've previously encountered, there is a slightly different syntax for defining connections from signals which contain arguments.

As per the previous tutorial, connections from signals which don't contain arguments look like:

motionSensor = self.deviceList.getDevice("livingRoomMotionSensor")
motionSensor.motionDetected.connect(self.onMotionDetected)

whereas connections from signals which contain arguments look like:

doorSensor = self.deviceList.getDevice("livingRoomDoorSensor")
doorSensor.stateChanged[bool].connect(self.onSensorStateChanged)

The difference is that when the signal contains arguments the signal name must be appended with a list of expected argument types in square brackets. In this case, the stateChanged signal contains a single boolean argument, but it could contain any number of python data types, separated by commas. As long as the slot or method that the signal is connected to has a matching set of input arguments, the signal connection will be successful.

Receiving Signal Arguments in Methods

In the code snippet above we can see that the stateChanged signal, containing a boolean argument, is connected to the onSensorStateChanged method. That method must be created to accept the same number and types of arguments that the connected signal contains. An example of the definition of this method is:

def onSensorStateChanged(self, newState):
        self.print("The new sensor state is; " + str(newState))

In this case, the variable newState will be passed the boolean value which the emitted stateChanged signal contained. The arguments in the method can be given any names as long as the number of arguments, excluding the self argument, is the same as in the connected signal. For more background on how signals and slots behave in this Python-Qt framework feel free to read Signals and Slots in PySide | Qt Wiki | Qt Project.

Actuating a Device

At any point in our app, we can call a method on any device to actuate, or change the state of, the device. To achieve this we must get a reference to the device, then call the relevant method on that reference.

If we consider the light actuator in the living room:

LivingRoomLightActuatorTooltipOpened.png

we can see that the device's name is "livingRoomLight" and it contains a method named turnOn(). Hence, to turn on this device we can use the code:

light = self.deviceList.getDevice("livingRoomLight")
light.turnOn()


As a more functional example, if we wanted to turn on the living room light every time the living room door is opened, we could use the code:

class ChaosApp(BaseChaosApp):
        def run(self):
                ...
                #Connect devices to other devices and functions here...
                doorSensor = self.deviceList.getDevice("livingRoomDoorSensor")
                doorSensor.stateChanged[bool].connect(self.onSensorStateChanged)
               
        def onSensorStateChanged(self, newState):
                # If the new state is true, the door has just opened:          
                if newState:  # this is True when the door opens
                        light = self.deviceList.getDevice("livingRoomLight")
                        light.turnOn()


Maintaining the Reference to the Device

The previous code snippet is a perfectly functioning example. However, it has the one disadvantage that every time the door opens, we have to re-fetch the reference to the light object before actuating the light object. A better approach would be to fetch the reference to the light object once; when the app starts, and store it in an instance variable to be used again and again. Modifying the code to use this approach would give:

class ChaosApp(BaseChaosApp):
        def run(self):
                ...
                #Connect devices to other devices and functions here...
                doorSensor = self.deviceList.getDevice("livingRoomDoorSensor")
                doorSensor.stateChanged[bool].connect(self.onSensorStateChanged)
                self.light = self.deviceList.getDevice("livingRoomLight")
               
        def onSensorStateChanged(self, newState):
                # If the new state is true, the door is opened:        
                if newState:  # this is True when the door opens        
                        self.light.turnOn()

The only changes in this piece of code compared to the previous one are highlighted. Even though the changes are minor, this prevents unnecessary repeated fetching of references to a the same device by storing the reference in the instance variable self.light when the app starts. This instance variable is available for the lifetime of the app and can be accessed in any methods in the app.

Remember that if a variable is not prepended with a self. it will "go out of scope" at the end of the method and will need to be created next time it is needed. Therefore, it is good practice to declare all devices which you expect to use in other methods throughout the app as instance variables in the run() method.

The App

Let's develop a quick app to formalise what we've covered so far.

Objective

We want to write an app that:

  1. When the main door (labelled "livingRoomDoorSensor") opens, turn on the living room light (labelled "livingRoomLight")
  2. When the door closes, leave the light on
  3. The app should fetch references to devices as few times as possible to maximise efficiency
  4. The app should output text to the debug console to help the developer confirm the app's behaviour

To test, you can click the door sensor icon in the home simulator to toggle it's state from closed to opened and vice-versa. If you want to manually deactivate the light to test the app again, just click on the light icon.

Solution

Try programming the app for yourself, and when you're happy with it, compare it to the example solution.

Example Solution:

from __future__ import print_function
from ChaosApps import BaseChaosApp

class ChaosApp(BaseChaosApp):
        def run(self):
                self.appName='Tutorial 3'
                self.print("Skeleton App '"+self.appName+"' Running")

                #Connect devices to other devices and functions here...
                doorSensor = self.deviceList.getDevice("livingRoomDoorSensor")
                doorSensor.stateChanged[bool].connect(self.onSensorStateChanged)
               
                # Store the reference to the light in an instance variable so it can be accessed later:
                self.light = self.deviceList.getDevice("livingRoomLight")
               
        def onSensorStateChanged(self, newState):
                # If the new state is true, the door is opened:        
                if newState:
                        self.print("Door has just opened, turning on light")
                        self.light.turnOn()
                else:
                        self.print("Door has just closed, not doing anything")
               
        def initAppGui(self):
                self.print("'" + self.appName + "': CHAOS has asked for the current HTML of the phone app GUI for this CHAOS App")

                htmlString = """appGuiConstructionString"""

                return htmlString

        def appGuiInteractionCallback(self,someData):
                self.print(self.appName + ": Some information has arrived for this CHAOS App from the phone GUI ")

        def stop(self):
                self.print("Disconnecting any created signals and then stopping '" + self.appName + "'")

The highlighted lines are the lines we've added to the skeleton app to achieve this functionality.

Wrap Up

This tutorial has discussed how to intercept signals from devices which contain data and actuate other devices based on those signals. The next tutorial will be a quick one, explaining how we can save time and connect a signal from one device directly to a method or slot in another device.

<< - <Prev - Next>