Programming Guide¶
PPMP is simple enough to be reasonably used from Python without an API at all. The simplest possible PPMP measurement payload, transmitting just one sensor reading for “temperature”, looks like this:
{
"content-spec":
"urn:spec://eclipse.org/unide/measurement-message#v2",
"device": {
"deviceID": "a4927dad-58d4-4580-b460-79cefd56775b"
},
"measurements": [{
"ts": "2002-05-30T09:30:10.123+02:00",
"series": {
"$_time": [ 0 ],
"temperature": [ 45.4231 ]
}
}
The main use cases for unide are handling and generating complex payloads programmatically, and parsing and validating incoming PPMP messages. unide is suitable for backend implementations receiving PPMP data, it can run on gateways supporting Python, and it is useful for quickly scripting PPMP applications and tools.
Getting Started¶
unide provides a Python class for every entity described in the PPMP specification. Classes have read-write attributes for each property in the specification. All properties can be passed directly into the class constructor using positional and named arguments.
Unset properties are None in the Python API, but will not be
serialized as ‘null’ into JSON, i.e. unset properties will not appear
in the JSON output at all. Strings are mapped to and from Python
Unicode strings (i.e. unicode for Python 2, and str for Python
3). Numeric values are mapped to Python float. Timestamps are mapped
to Python’s datetime
(see
Timestamps for details).
Every PPMP entity can be build separately, and re-used later to assemble a complete payload. A central entity in PPMP is the Device, that has just one mandatory property, its deviceID:
>>> from unide.common import Device
>>> device = Device("Device-001")
>>> print(device.deviceID == "Device-001")
Device-001
All other properties of device
are now None and can be assigned
a value:
>>> print(device.operationalStatus)
None
>>> device.operationalStatus = "running"
>>> print(device.operationalStatus)
running
PPMP objects can be printed:
>>> print(device)
Device(deviceID=Device-001, operationalStatus=running)
In PPMP, all messages originate from a device. The Device class
therefore has convenience APIs to quickly produce complete
payloads. The example below produces a simple MeasurementPayload
using Device.measurement()
:
>>> msg = device.measurement(temperature=36.7)
>>> print(msg)
{"device": {"deviceID": "Device-001", "operationalStatus": "running"}, "content-spec": "urn:spec://eclipse.org/unide/measurement-message#v2", "measurements": [{"ts": "2017-09-13T22:23:26.840407+02:00", "series": {"temperature": [36.7], "$_time": [0]}}]}
The other two types of PPMP messages are MessagePayload and
ProcessPayload and can be produced using Device.message()
and Device.process()
respectively.
We can create the same message using the lower-level APIs by building
each component separately. To do that, we have to create a
Series
object and explicitly
declare the dimension temperature
that we want to provide:
>>> from unide.measurement Series
>>> series = Series("temperature")
>>> series.add_sample(0, temperature=36.7)
Then, we create a Measurement
object and assemble a
MeasurementPayload
using the components we’ve just created:
>>> from unide.measurement import Measurement, MeasurementPayload
>>> from unide import util
>>> m = Measurement(ts=util.local_now(), series=series)
>>> payload = MeasurementPayload(device=device)
>>> payload.measurements.append(m)
The measurements property of the payload object is just a normal Python list of Measurement objects.
Finally, payload
can be converted to JSON by using
dumps()
from unide.util
. The
string returned by dumps can be send as a payload using a transport
protocol like HTTP/REST or MQTT. unide by itself does not implement
any transport protocol:
>>> from unide.util import loads
>>> print(dumps(payload, indent=4))
{
"device": {
"deviceID": "Device-001"
},
"content-spec": "urn:spec://eclipse.org/unide/measurement-message#v2",
"measurements": [
{
"ts": "2017-09-13T23:40:46.685521+02:00",
"series": {
"$_time": []
}
}
]
}
Validation and Parsing¶
The unide APIs validate inputs. For example, the maximum length for device identifiers is 36. Trying to assign a longer id raises a ValueError exception:
>>> device = Device("PPMP HAS A SIZE RESTRICTION FOR DEVICE IDs!")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "measurement.py", line 225, in __init__
self.deviceID = deviceID
File "schema.py", line 96, in set
value = check(self, name, value, constraint)
File "schema.py", line 84, in check
.format(name=name, value=value, classname=type(self).__name__))
ValueError: u'PPMP HAS A SIZE RESTRICTION FOR DEVICE IDs!' is not an appropriate value for 'Device.deviceID'
Parsing a PPMP message is done using loads()
:
>>> from unide.util import loads
>>> msg = loads(open("tests/message.json").read())
>>> print(msg)
MessagePayload(device=Device(operationalStatus=normal, deviceID=2ca5158b-8350-4592-bff9-755194497d4e, metaData={u'swVersion': u'2.0.3.13', u'swBuildID': u'41535'}), messages=[<unide.message.Message object at 0x1095938d0>, <unide.message.Message object at 0x109af6510>], content-spec=urn:spec://eclipse.org/unide/machine-message#v2)
loads()
automatically detects the payload type and returns
the appropriate unide object. If the payload type can not be
detected, an exception will be raised.
Besides trying to detect the PPMP type, parsed messages will not be
validated by default. Malformed messages can be parsed, and all
recognizable information can be accessed. A message can be validated
using problems()
after
loading it:
>>> msg = loads(open("tests/invalid.json").read())
>>> msg.problems()
[u"'xdevice' is not a valid key for 'MessagePayload' objects"]
problems()
returns a list of
issues. An empty list indicates a valid payload.
To validate a payload while parsing it, one can set the validate
flag for loads. When the payload is not valid, a ValidationError
exception is raised:
>>> msg = loads(open("tests/invalid.json").read(), validate=True)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "/Users/frank/Projects/unide/cslab/unide.python/src/unide/util.py", line 51, in loads
raise ValidationError(errors)
unide.util.ValidationError: 'xdevice' is not a valid key for 'MessagePayload' objects
Timestamps¶
All PPMP messages carry one or more timestamps. Timestamps are represented by unide as Python datetime.datetime objects. In Python, datetime objects come in two flavours: “naive” – without timezone information, and “aware” – including timezone information. While the PPMP specification is not explicit about this, unide automatically makes all timestamps “aware”. If you assign a “naive” datetime to a PPMP property, it will be made “aware” by adding the local timezone offset:
>>> from unide.measurement import Measurement
>>> import datetime
>>> now = datetime.datetime.now()
>>> m = Measurement(ts=now)
>>> print(now)
2017-09-13 22:56:59.329554
>>> print(m.ts)
2017-09-13 22:56:59.329554+02:00
>>>
Note the difference! “Naive” and “aware” timestamps are not even compatible in Python:
>>> now == m.ts
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't compare offset-naive and offset-aware datetimes
We therefore recommend to always use “aware” datetime objects to avoid awe and confusion.
unide provides two functions in its unide.util
module to
help with that: local_now()
computes
the timestamp for the current time including the local timezone
offset, and local_timezone(value)
converts any naive datetime to “aware”
using the offset of the local timezone.