Sample 4 - Extending Objects and Properties

This sample application shows how to extend one of the basic objects, an Analog Value Object in this case, to provide a custom property, the present value. This type of code is used when the application is providing a BACnet interface to a collection of data. It assumes that almost all of the default behaviour of a BACpypes application is sufficient.

Note

The code in this description starts at the __main__ block and goes backward through the source file.

Constructing the Device

Initialization is simple, the simple BACnet/IP application, which includes the networking layer and communications layers all bundled in together is created like the other samples:

# make a sample application
thisApplication = BIPSimpleApplication(thisDevice, config.get('BACpypes','address'))

The only object this has by default is an instance of a object.LocalDeviceObject. The next step is to create a special Analog Value Object and add it to the application:

# make a random input object
raio = RandomAnalogValueObject(objectIdentifier=('analog-value', 1), objectName='Random')

# add it to the device
thisApplication.AddObject(raio)

Adding the object not only makes it addressable by BACnet clients, but also appends its object identifier to the list of object identifiers in the object-list property of the device object. This makes the object discoverable by BACnet configuration tools.

All objects must have an object identifier and an object name, so they are passed as keyword arguments. All of the keyword arguements are matched with an appropriate property in the object.Object.__init__() method, which will make sure the keyword argument value is appropriate for the property.

Extending the Analog Value Object

The definition of a new kind of Analog Value Object uses Python inhertiance, so it seems fairly simple:

class RandomAnalogValueObject(AnalogValueObject):
    properties = [
        RandomValueProperty('present-value'),
        ]

register_object_type(RandomAnalogInputObject)

But the call to register the object type has lots of side effects. First, there is already a call to register the built-in object.AnalogValueObject so this registration is going to override it.

Note

Overriding the same base type, like AnalogValueObject, in more than one way could be difficult in some kinds of gateway software which may require re-factoring the object.get_object_class() and the object.get_datatype() functionality. This will be addressed before BACpypes reaches v1.0.

The first part of object.register_object_type() builds a dictionary of a relationship between the property name and its associated instance. It will look for properties class attribtues in the entire inheritance tree (using the method resolution order) but only associate the first of two instances with the same name.

So in this case, the RandomValueProperty instance called ‘present-value’ will be bound to the object type before the built-in version it finds in the properties list in the object.AnalogValueObject.

A New Real Property

BACnet clients will expect that the ‘present-value’ of an Analog Value Object will be primitivedata.Real and returning some other datatype would seriously break interperabililty. The initialization is almost identical to the one for the built-in AnalogValueObject:

class RandomValueProperty(Property, Logging):

    def __init__(self, identifier):
        Property.__init__(self, identifier, Real, default=None,
            optional=True, mutable=False
            )

The only difference is mutable is False, which means BACnet clients will receive an error if they attempt to write a value to the property.

The core of the application is responding to a ReadPropertyRequest, which is mapped into a ReadProperty function call:

def ReadProperty(self, obj, arrayIndex=None):

    # access an array
    if arrayIndex is not None:
        raise Error(errorClass='property', errorCode='property-is-not-an-array')

The arrayIndex parameter will be some integer value if the BACnet client is accessing the property as an array, which is an error. Now it comes down to getting a random value and returning it:

# return a random value
value = random.random() * 100.0
RandomValueProperty._debug("    - value: %r", value)

return value

The value returned by this function will be passed as an initial value to construct a primitivedata.Real object, which will then be encoded into the apdu.ReadPropertyACK response and returned to the client.