By Kai Kreuzer
When talking about the Internet of Things (IoT), you quickly get into the discussion about device abstraction. As a software engineer, it is natural that you want to represent a physical switch as a Boolean value and a sensor measurement as a simple number. It is hard to understand why every hardware manufacturer and every alliance comes up with yet another way to access their devices, when all there is to do is to send a few numbers across. In order to simplify this plethora of options, device abstraction layers were invented.
Actually, there is already a very common way to represent things (aka objects) in software: Object-oriented design! So let us create an abstract model of a television:
Has OOD failed, again?
The obvious problem here is that this is indeed “a” television. But this class is not usable for all TV sets that are out there – old CRTs have hardly any functionality and modern smart TVs include a triple-tuner, hard-disc recording etc. So how should this single representation suit them all? Well, you could simply add all potential functionality of a TV to this class and declare most of them optional. But this effectively means that your application has to deal with NotImplementedExceptions for 90% of the called methods in the end – the application degenerates into an exception handling facility…Somehow this reminds me of the fact that I was never convinced that OOD is the right thing for today’s problems.
As a consequence, a more clever approach is to split devices into their functionalities in order to have reusable (and thus abstracted) parts. A device then becomes an object with a set of functions, often also called capabilities. Steven Posick wrote a very good blogpost about why capabilities-based programming makes a lot of sense for IoT. Let’s see what our representation would look like in this way:
This is already much better as the commonalities are defined on a finer-grained level. Still, another problem creeps in from the opposite direction as well: The primitive types, which build the “atoms” on which the device abstraction is based. Are the usual primitive types a good choice? A magnetic sensor status can be represented as true/false – or rather false/true? A TV station can be a simple integer. But which integer is then CNN and which is ESPN? These questions suggest that there is too much ambiguity in such a model and hence the use of enumerations seemy to be a logical choice. On a closer look, we are now back at the same problem we had before: While modelling the functionality, such as the channel, as a primitive type (here integer), this is fine for more or less all devices. By introducing a channel list as an enumeration, this is all of a sudden very device-specific and hardly reusable for other TVs. This is the same effect for all other constraints on values: What is the frequency range of my tuner? Which step size does my thermostat support and what is its maximum value? All such constraints are very device-specific and hence should be treated as (specific) meta-data in the model.
A similar situation applies to units: Sensors provide values in a certain unit and this is often directly built into the hardware. Depending on their manufacturer and intended use case, a temperature sensor might deliver values as Kelvin, degrees Celsius or degrees Fahrenheit. It is therefore not a good idea to define the unit of a value on the abstraction layer – this would again reduce the possibility to find a generic “TemperatureSensor” abstraction or rather would introduce three different versions of it. The better choice is therefore to treat the unit as meta-data.
Very often, a huge part of a device’ functionality is exclusively used for its configuration. The configuration options of a device are highly hardware-specific and thus hard to abstract. Treating configuration functionality separate from “operational” functionality is therefore useful. A good example is a “simple” dimmer: Its operational functionality is fairly easy: It has a state between 0 and 100 and this can be set through an operation. But in order to configure this behavior, there is a multitude of different options: Individual dimming curves can be defined depending on the type of bulb. How quickly should the new value be reached, i.e. is there a smooth transition? Is it at all OK to dim the connected device or must it only be switched on and off? Should it turn to 100% when being switched on or shall it remember the state before it had been turned off? Some dimmers actually come with 20 and more options for their configuration – a really strong reason for not trying to generalize this.
Dimmers from different manufacturers
Contemplating about dimmers brings up yet another angle: Is it really the dimmer that the user or application is interested in? No, the dimmer is only a mean to change the light that is plugged into it. Likewise a magnetic contact is not in the focus itself – instead, it is the door where it is attached to. The temperature of a sensor is not interesting at all, but the temperature of the room it is in is. Ideally, we would like to work with concepts like “door”, “room”, “pet” in the application layer, not necessarily with the devices themselves. Such a collection of concepts is usually called an ontology. In an ontology, the relations between the concepts are described and lead to semantical meaning, through which implicit knowledge can be deduced.
For a device abstraction layer, such an ontology brings insurmountable obstacles: A device manufacturer simply cannot know about the concrete usage of the device and thus cannot provide the semantical classification. It is merely the installer or user who has the required knowledge. If he e.g. connects his fridge to a smart plug in order to measure its energy consumption, he clearly would not want it to be turned off by an “all-off” scenario. He therefore needs to teach the system about this, either explicitly or implicitly through his behavior. The latter means that the system can only learn through trial & error, which sounds nice when being called “self-learning” in marketing material, but which can lead to quite some frustration for the users – there is a great post on this by Johannes Ernst about the Nest thermostat. I fully agree that this is an easy hard problem.
Separating functions from the physical device also means that you might have to deal with multiple locations – if you want to “turn off all lights in your kitchen” then this should include the light that is controlled through the actuator which is located in your electrical cabinet. If this actuator has multiple channels, there is one physical location (the cabinet), but multiple functional locations. This again is an information that needs to be taught to the system. For the daily usage, the device and its location are hardly relevant, but the locations of the functions are.
Static Descriptions vs. Dynamic Behavior
Assuming you successfully managed to classify all your devices nicely and have a generalized description of them, a new problem comes up once you start using your system: The dynamic runtime behavior. A static description can quickly render useless when devices are operated:
- Changing the mode of your heating system between “manual” and “automatic” will radically change the set of available options for the current mode. So how could one describe availability of functions and options that depend on certain values of other functions?
- Sensor values can be pulled or pushed – how often does this happen? Is pushing done at a fixed rate or only upon a change in the value (and with which threshold)?
- When sending a command to a device, is this directly transferred or will it take a while as the device might be in a (battery-optimized) sleep mode? How long does the operation take to be fully completed?
- What error situations can occur and how can this be determined? Are there recovery options?
- Are state changes described through a state machine? Is there maybe even the need for transaction support?